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