bevy_core_pipeline/tonemapping/
mod.rs

1use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
2use bevy_app::prelude::*;
3use bevy_asset::{load_internal_asset, weak_handle, Assets, Handle};
4use bevy_ecs::prelude::*;
5use bevy_image::{CompressedImageFormats, Image, ImageSampler, ImageType};
6use bevy_reflect::{std_traits::ReflectDefault, Reflect};
7use bevy_render::{
8    camera::Camera,
9    extract_component::{ExtractComponent, ExtractComponentPlugin},
10    extract_resource::{ExtractResource, ExtractResourcePlugin},
11    render_asset::{RenderAssetUsages, RenderAssets},
12    render_resource::{
13        binding_types::{sampler, texture_2d, texture_3d, uniform_buffer},
14        *,
15    },
16    renderer::RenderDevice,
17    texture::{FallbackImage, GpuImage},
18    view::{ExtractedView, ViewTarget, ViewUniform},
19    Render, RenderApp, RenderSet,
20};
21use bitflags::bitflags;
22#[cfg(not(feature = "tonemapping_luts"))]
23use tracing::error;
24
25mod node;
26
27use bevy_utils::default;
28pub use node::TonemappingNode;
29
30const TONEMAPPING_SHADER_HANDLE: Handle<Shader> =
31    weak_handle!("e239c010-c25c-42a1-b4e8-08818764d667");
32
33const TONEMAPPING_SHARED_SHADER_HANDLE: Handle<Shader> =
34    weak_handle!("61dbc544-4b30-4ca9-83bd-4751b5cfb1b1");
35
36const TONEMAPPING_LUT_BINDINGS_SHADER_HANDLE: Handle<Shader> =
37    weak_handle!("d50e3a70-c85e-4725-a81e-72fc83281145");
38
39/// 3D LUT (look up table) textures used for tonemapping
40#[derive(Resource, Clone, ExtractResource)]
41pub struct TonemappingLuts {
42    blender_filmic: Handle<Image>,
43    agx: Handle<Image>,
44    tony_mc_mapface: Handle<Image>,
45}
46
47pub struct TonemappingPlugin;
48
49impl Plugin for TonemappingPlugin {
50    fn build(&self, app: &mut App) {
51        load_internal_asset!(
52            app,
53            TONEMAPPING_SHADER_HANDLE,
54            "tonemapping.wgsl",
55            Shader::from_wgsl
56        );
57        load_internal_asset!(
58            app,
59            TONEMAPPING_SHARED_SHADER_HANDLE,
60            "tonemapping_shared.wgsl",
61            Shader::from_wgsl
62        );
63        load_internal_asset!(
64            app,
65            TONEMAPPING_LUT_BINDINGS_SHADER_HANDLE,
66            "lut_bindings.wgsl",
67            Shader::from_wgsl
68        );
69
70        if !app.world().is_resource_added::<TonemappingLuts>() {
71            let mut images = app.world_mut().resource_mut::<Assets<Image>>();
72
73            #[cfg(feature = "tonemapping_luts")]
74            let tonemapping_luts = {
75                TonemappingLuts {
76                    blender_filmic: images.add(setup_tonemapping_lut_image(
77                        include_bytes!("luts/Blender_-11_12.ktx2"),
78                        ImageType::Extension("ktx2"),
79                    )),
80                    agx: images.add(setup_tonemapping_lut_image(
81                        include_bytes!("luts/AgX-default_contrast.ktx2"),
82                        ImageType::Extension("ktx2"),
83                    )),
84                    tony_mc_mapface: images.add(setup_tonemapping_lut_image(
85                        include_bytes!("luts/tony_mc_mapface.ktx2"),
86                        ImageType::Extension("ktx2"),
87                    )),
88                }
89            };
90
91            #[cfg(not(feature = "tonemapping_luts"))]
92            let tonemapping_luts = {
93                let placeholder = images.add(lut_placeholder());
94                TonemappingLuts {
95                    blender_filmic: placeholder.clone(),
96                    agx: placeholder.clone(),
97                    tony_mc_mapface: placeholder,
98                }
99            };
100
101            app.insert_resource(tonemapping_luts);
102        }
103
104        app.add_plugins(ExtractResourcePlugin::<TonemappingLuts>::default());
105
106        app.register_type::<Tonemapping>();
107        app.register_type::<DebandDither>();
108
109        app.add_plugins((
110            ExtractComponentPlugin::<Tonemapping>::default(),
111            ExtractComponentPlugin::<DebandDither>::default(),
112        ));
113
114        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
115            return;
116        };
117        render_app
118            .init_resource::<SpecializedRenderPipelines<TonemappingPipeline>>()
119            .add_systems(
120                Render,
121                prepare_view_tonemapping_pipelines.in_set(RenderSet::Prepare),
122            );
123    }
124
125    fn finish(&self, app: &mut App) {
126        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
127            return;
128        };
129        render_app.init_resource::<TonemappingPipeline>();
130    }
131}
132
133#[derive(Resource)]
134pub struct TonemappingPipeline {
135    texture_bind_group: BindGroupLayout,
136    sampler: Sampler,
137}
138
139/// Optionally enables a tonemapping shader that attempts to map linear input stimulus into a perceptually uniform image for a given [`Camera`] entity.
140#[derive(
141    Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq,
142)]
143#[extract_component_filter(With<Camera>)]
144#[reflect(Component, Debug, Hash, Default, PartialEq)]
145pub enum Tonemapping {
146    /// Bypass tonemapping.
147    None,
148    /// Suffers from lots hue shifting, brights don't desaturate naturally.
149    /// Bright primaries and secondaries don't desaturate at all.
150    Reinhard,
151    /// Suffers from hue shifting. Brights don't desaturate much at all across the spectrum.
152    ReinhardLuminance,
153    /// Same base implementation that Godot 4.0 uses for Tonemap ACES.
154    /// <https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl>
155    /// Not neutral, has a very specific aesthetic, intentional and dramatic hue shifting.
156    /// Bright greens and reds turn orange. Bright blues turn magenta.
157    /// Significantly increased contrast. Brights desaturate across the spectrum.
158    AcesFitted,
159    /// By Troy Sobotka
160    /// <https://github.com/sobotka/AgX>
161    /// Very neutral. Image is somewhat desaturated when compared to other tonemappers.
162    /// Little to no hue shifting. Subtle [Abney shifting](https://en.wikipedia.org/wiki/Abney_effect).
163    /// NOTE: Requires the `tonemapping_luts` cargo feature.
164    AgX,
165    /// By Tomasz Stachowiak
166    /// Has little hue shifting in the darks and mids, but lots in the brights. Brights desaturate across the spectrum.
167    /// Is sort of between Reinhard and `ReinhardLuminance`. Conceptually similar to reinhard-jodie.
168    /// Designed as a compromise if you want e.g. decent skin tones in low light, but can't afford to re-do your
169    /// VFX to look good without hue shifting.
170    SomewhatBoringDisplayTransform,
171    /// Current Bevy default.
172    /// By Tomasz Stachowiak
173    /// <https://github.com/h3r2tic/tony-mc-mapface>
174    /// Very neutral. Subtle but intentional hue shifting. Brights desaturate across the spectrum.
175    /// Comment from author:
176    /// Tony is a display transform intended for real-time applications such as games.
177    /// It is intentionally boring, does not increase contrast or saturation, and stays close to the
178    /// input stimulus where compression isn't necessary.
179    /// Brightness-equivalent luminance of the input stimulus is compressed. The non-linearity resembles Reinhard.
180    /// Color hues are preserved during compression, except for a deliberate [Bezold–Brücke shift](https://en.wikipedia.org/wiki/Bezold%E2%80%93Br%C3%BCcke_shift).
181    /// To avoid posterization, selective desaturation is employed, with care to avoid the [Abney effect](https://en.wikipedia.org/wiki/Abney_effect).
182    /// NOTE: Requires the `tonemapping_luts` cargo feature.
183    #[default]
184    TonyMcMapface,
185    /// Default Filmic Display Transform from blender.
186    /// Somewhat neutral. Suffers from hue shifting. Brights desaturate across the spectrum.
187    /// NOTE: Requires the `tonemapping_luts` cargo feature.
188    BlenderFilmic,
189}
190
191impl Tonemapping {
192    pub fn is_enabled(&self) -> bool {
193        *self != Tonemapping::None
194    }
195}
196
197bitflags! {
198    /// Various flags describing what tonemapping needs to do.
199    ///
200    /// This allows the shader to skip unneeded steps.
201    #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
202    pub struct TonemappingPipelineKeyFlags: u8 {
203        /// The hue needs to be changed.
204        const HUE_ROTATE                = 0x01;
205        /// The white balance needs to be adjusted.
206        const WHITE_BALANCE             = 0x02;
207        /// Saturation/contrast/gamma/gain/lift for one or more sections
208        /// (shadows, midtones, highlights) need to be adjusted.
209        const SECTIONAL_COLOR_GRADING   = 0x04;
210    }
211}
212
213#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
214pub struct TonemappingPipelineKey {
215    deband_dither: DebandDither,
216    tonemapping: Tonemapping,
217    flags: TonemappingPipelineKeyFlags,
218}
219
220impl SpecializedRenderPipeline for TonemappingPipeline {
221    type Key = TonemappingPipelineKey;
222
223    fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
224        let mut shader_defs = Vec::new();
225
226        shader_defs.push(ShaderDefVal::UInt(
227            "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(),
228            3,
229        ));
230        shader_defs.push(ShaderDefVal::UInt(
231            "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(),
232            4,
233        ));
234
235        if let DebandDither::Enabled = key.deband_dither {
236            shader_defs.push("DEBAND_DITHER".into());
237        }
238
239        // Define shader flags depending on the color grading options in use.
240        if key.flags.contains(TonemappingPipelineKeyFlags::HUE_ROTATE) {
241            shader_defs.push("HUE_ROTATE".into());
242        }
243        if key
244            .flags
245            .contains(TonemappingPipelineKeyFlags::WHITE_BALANCE)
246        {
247            shader_defs.push("WHITE_BALANCE".into());
248        }
249        if key
250            .flags
251            .contains(TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING)
252        {
253            shader_defs.push("SECTIONAL_COLOR_GRADING".into());
254        }
255
256        match key.tonemapping {
257            Tonemapping::None => shader_defs.push("TONEMAP_METHOD_NONE".into()),
258            Tonemapping::Reinhard => shader_defs.push("TONEMAP_METHOD_REINHARD".into()),
259            Tonemapping::ReinhardLuminance => {
260                shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into());
261            }
262            Tonemapping::AcesFitted => shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()),
263            Tonemapping::AgX => {
264                #[cfg(not(feature = "tonemapping_luts"))]
265                error!(
266                    "AgX tonemapping requires the `tonemapping_luts` feature.
267                    Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
268                    or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
269                );
270                shader_defs.push("TONEMAP_METHOD_AGX".into());
271            }
272            Tonemapping::SomewhatBoringDisplayTransform => {
273                shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into());
274            }
275            Tonemapping::TonyMcMapface => {
276                #[cfg(not(feature = "tonemapping_luts"))]
277                error!(
278                    "TonyMcMapFace tonemapping requires the `tonemapping_luts` feature.
279                    Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
280                    or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
281                );
282                shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into());
283            }
284            Tonemapping::BlenderFilmic => {
285                #[cfg(not(feature = "tonemapping_luts"))]
286                error!(
287                    "BlenderFilmic tonemapping requires the `tonemapping_luts` feature.
288                    Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
289                    or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
290                );
291                shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into());
292            }
293        }
294        RenderPipelineDescriptor {
295            label: Some("tonemapping pipeline".into()),
296            layout: vec![self.texture_bind_group.clone()],
297            vertex: fullscreen_shader_vertex_state(),
298            fragment: Some(FragmentState {
299                shader: TONEMAPPING_SHADER_HANDLE,
300                shader_defs,
301                entry_point: "fragment".into(),
302                targets: vec![Some(ColorTargetState {
303                    format: ViewTarget::TEXTURE_FORMAT_HDR,
304                    blend: None,
305                    write_mask: ColorWrites::ALL,
306                })],
307            }),
308            primitive: PrimitiveState::default(),
309            depth_stencil: None,
310            multisample: MultisampleState::default(),
311            push_constant_ranges: Vec::new(),
312            zero_initialize_workgroup_memory: false,
313        }
314    }
315}
316
317impl FromWorld for TonemappingPipeline {
318    fn from_world(render_world: &mut World) -> Self {
319        let mut entries = DynamicBindGroupLayoutEntries::new_with_indices(
320            ShaderStages::FRAGMENT,
321            (
322                (0, uniform_buffer::<ViewUniform>(true)),
323                (
324                    1,
325                    texture_2d(TextureSampleType::Float { filterable: false }),
326                ),
327                (2, sampler(SamplerBindingType::NonFiltering)),
328            ),
329        );
330        let lut_layout_entries = get_lut_bind_group_layout_entries();
331        entries =
332            entries.extend_with_indices(((3, lut_layout_entries[0]), (4, lut_layout_entries[1])));
333
334        let render_device = render_world.resource::<RenderDevice>();
335        let tonemap_texture_bind_group = render_device
336            .create_bind_group_layout("tonemapping_hdr_texture_bind_group_layout", &entries);
337
338        let sampler = render_device.create_sampler(&SamplerDescriptor::default());
339
340        TonemappingPipeline {
341            texture_bind_group: tonemap_texture_bind_group,
342            sampler,
343        }
344    }
345}
346
347#[derive(Component)]
348pub struct ViewTonemappingPipeline(CachedRenderPipelineId);
349
350pub fn prepare_view_tonemapping_pipelines(
351    mut commands: Commands,
352    pipeline_cache: Res<PipelineCache>,
353    mut pipelines: ResMut<SpecializedRenderPipelines<TonemappingPipeline>>,
354    upscaling_pipeline: Res<TonemappingPipeline>,
355    view_targets: Query<
356        (
357            Entity,
358            &ExtractedView,
359            Option<&Tonemapping>,
360            Option<&DebandDither>,
361        ),
362        With<ViewTarget>,
363    >,
364) {
365    for (entity, view, tonemapping, dither) in view_targets.iter() {
366        // As an optimization, we omit parts of the shader that are unneeded.
367        let mut flags = TonemappingPipelineKeyFlags::empty();
368        flags.set(
369            TonemappingPipelineKeyFlags::HUE_ROTATE,
370            view.color_grading.global.hue != 0.0,
371        );
372        flags.set(
373            TonemappingPipelineKeyFlags::WHITE_BALANCE,
374            view.color_grading.global.temperature != 0.0 || view.color_grading.global.tint != 0.0,
375        );
376        flags.set(
377            TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING,
378            view.color_grading
379                .all_sections()
380                .any(|section| *section != default()),
381        );
382
383        let key = TonemappingPipelineKey {
384            deband_dither: *dither.unwrap_or(&DebandDither::Disabled),
385            tonemapping: *tonemapping.unwrap_or(&Tonemapping::None),
386            flags,
387        };
388        let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key);
389
390        commands
391            .entity(entity)
392            .insert(ViewTonemappingPipeline(pipeline));
393    }
394}
395/// Enables a debanding shader that applies dithering to mitigate color banding in the final image for a given [`Camera`] entity.
396#[derive(
397    Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq,
398)]
399#[extract_component_filter(With<Camera>)]
400#[reflect(Component, Debug, Hash, Default, PartialEq)]
401pub enum DebandDither {
402    #[default]
403    Disabled,
404    Enabled,
405}
406
407pub fn get_lut_bindings<'a>(
408    images: &'a RenderAssets<GpuImage>,
409    tonemapping_luts: &'a TonemappingLuts,
410    tonemapping: &Tonemapping,
411    fallback_image: &'a FallbackImage,
412) -> (&'a TextureView, &'a Sampler) {
413    let image = match tonemapping {
414        // AgX lut texture used when tonemapping doesn't need a texture since it's very small (32x32x32)
415        Tonemapping::None
416        | Tonemapping::Reinhard
417        | Tonemapping::ReinhardLuminance
418        | Tonemapping::AcesFitted
419        | Tonemapping::AgX
420        | Tonemapping::SomewhatBoringDisplayTransform => &tonemapping_luts.agx,
421        Tonemapping::TonyMcMapface => &tonemapping_luts.tony_mc_mapface,
422        Tonemapping::BlenderFilmic => &tonemapping_luts.blender_filmic,
423    };
424    let lut_image = images.get(image).unwrap_or(&fallback_image.d3);
425    (&lut_image.texture_view, &lut_image.sampler)
426}
427
428pub fn get_lut_bind_group_layout_entries() -> [BindGroupLayoutEntryBuilder; 2] {
429    [
430        texture_3d(TextureSampleType::Float { filterable: true }),
431        sampler(SamplerBindingType::Filtering),
432    ]
433}
434
435#[expect(clippy::allow_attributes, reason = "`dead_code` is not always linted.")]
436#[allow(
437    dead_code,
438    reason = "There is unused code when the `tonemapping_luts` feature is disabled."
439)]
440fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image {
441    let image_sampler = ImageSampler::Descriptor(bevy_image::ImageSamplerDescriptor {
442        label: Some("Tonemapping LUT sampler".to_string()),
443        address_mode_u: bevy_image::ImageAddressMode::ClampToEdge,
444        address_mode_v: bevy_image::ImageAddressMode::ClampToEdge,
445        address_mode_w: bevy_image::ImageAddressMode::ClampToEdge,
446        mag_filter: bevy_image::ImageFilterMode::Linear,
447        min_filter: bevy_image::ImageFilterMode::Linear,
448        mipmap_filter: bevy_image::ImageFilterMode::Linear,
449        ..default()
450    });
451    Image::from_buffer(
452        #[cfg(all(debug_assertions, feature = "dds"))]
453        "Tonemapping LUT sampler".to_string(),
454        bytes,
455        image_type,
456        CompressedImageFormats::NONE,
457        false,
458        image_sampler,
459        RenderAssetUsages::RENDER_WORLD,
460    )
461    .unwrap()
462}
463
464pub fn lut_placeholder() -> Image {
465    let format = TextureFormat::Rgba8Unorm;
466    let data = vec![255, 0, 255, 255];
467    Image {
468        data: Some(data),
469        texture_descriptor: TextureDescriptor {
470            size: Extent3d {
471                width: 1,
472                height: 1,
473                depth_or_array_layers: 1,
474            },
475            format,
476            dimension: TextureDimension::D3,
477            label: None,
478            mip_level_count: 1,
479            sample_count: 1,
480            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
481            view_formats: &[],
482        },
483        sampler: ImageSampler::Default,
484        texture_view_descriptor: None,
485        asset_usage: RenderAssetUsages::RENDER_WORLD,
486    }
487}