From 72fbcc7633248703d58f2dbf1b47cdfde945325a Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Fri, 11 Nov 2022 19:43:45 +0000 Subject: [PATCH] Fix color banding by dithering image before quantization (#5264) # Objective - Closes #5262 - Fix color banding caused by quantization. ## Solution - Adds dithering to the tonemapping node from #3425. - This is inspired by Godot's default "debanding" shader: https://gist.github.com/belzecue/ - Unlike Godot: - debanding happens after tonemapping. My understanding is that this is preferred, because we are running the debanding at the last moment before quantization (`[f32, f32, f32, f32]` -> `f32`). This ensures we aren't biasing the dithering strength by applying it in a different (linear) color space. - This code instead uses and reference the origin source, Valve at GDC 2015 ![Screenshot from 2022-11-10 13-44-46](https://user-images.githubusercontent.com/2632925/201218880-70f4cdab-a1ed-44de-a88c-8759e77197f1.png) ![Screenshot from 2022-11-10 13-41-11](https://user-images.githubusercontent.com/2632925/201218883-72393352-b162-41da-88bb-6e54a1e26853.png) ## Additional Notes Real time rendering to standard dynamic range outputs is limited to 8 bits of depth per color channel. Internally we keep everything in full 32-bit precision (`vec4`) inside passes and 16-bit between passes until the image is ready to be displayed, at which point the GPU implicitly converts our `vec4` into a single 32bit value per pixel, with each channel (rgba) getting 8 of those 32 bits. ### The Problem 8 bits of color depth is simply not enough precision to make each step invisible - we only have 256 values per channel! Human vision can perceive steps in luma to about 14 bits of precision. When drawing a very slight gradient, the transition between steps become visible because with a gradient, neighboring pixels will all jump to the next "step" of precision at the same time. ### The Solution One solution is to simply output in HDR - more bits of color data means the transition between bands will become smaller. However, not everyone has hardware that supports 10+ bit color depth. Additionally, 10 bit color doesn't even fully solve the issue, banding will result in coherent bands on shallow gradients, but the steps will be harder to perceive. The solution in this PR adds noise to the signal before it is "quantized" or resampled from 32 to 8 bits. Done naively, it's easy to add unneeded noise to the image. To ensure dithering is correct and absolutely minimal, noise is adding *within* one step of the output color depth. When converting from the 32bit to 8bit signal, the value is rounded to the nearest 8 bit value (0 - 255). Banding occurs around the transition from one value to the next, let's say from 50-51. Dithering will never add more than +/-0.5 bits of noise, so the pixels near this transition might round to 50 instead of 51 but will never round more than one step. This means that the output image won't have excess variance: - in a gradient from 49 to 51, there will be a step between each band at 49, 50, and 51. - Done correctly, the modified image of this gradient will never have a adjacent pixels more than one step (0-255) from each other. - I.e. when scanning across the gradient you should expect to see: ``` |-band-| |-band-| |-band-| Baseline: 49 49 49 50 50 50 51 51 51 Dithered: 49 50 49 50 50 51 50 51 51 Dithered (wrong): 49 50 51 49 50 51 49 51 50 ``` ![Screenshot from 2022-11-10 14-12-36](https://user-images.githubusercontent.com/2632925/201219075-ab3f46be-d4e9-4869-b66b-a92e1706f49e.png) ![Screenshot from 2022-11-10 14-11-48](https://user-images.githubusercontent.com/2632925/201219079-ec5d2add-817d-487a-8fc1-84569c9cda73.png) You can see from above how correct dithering "fuzzes" the transition between bands to reduce distinct steps in color, without adding excess noise. ### HDR The previous section (and this PR) assumes the final output is to an 8-bit texture, however this is not always the case. When Bevy adds HDR support, the dithering code will need to take the per-channel depth into account instead of assuming it to be 0-255. Edit: I talked with Rob about this and it seems like the current solution is okay. We may need to revisit once we have actual HDR final image output. --- ## Changelog ### Added - All pipelines now support deband dithering. This is enabled by default in 3D, and can be toggled in the `Tonemapping` component in camera bundles. Banding is a graphical artifact created when the rendered image is crunched from high precision (f32 per color channel) down to the final output (u8 per channel in SDR). This results in subtle gradients becoming blocky due to the reduced color precision. Deband dithering applies a small amount of noise to the signal before it is "crunched", which breaks up the hard edges of blocks (bands) of color. Note that this does not add excess noise to the image, as the amount of noise is less than a single step of a color channel - just enough to break up the transition between color blocks in a gradient. Co-authored-by: Carter Anderson --- .../src/core_2d/camera_2d.rs | 2 +- .../src/core_3d/camera_3d.rs | 4 +- .../bevy_core_pipeline/src/tonemapping/mod.rs | 101 +++++++++++++----- .../src/tonemapping/node.rs | 11 +- .../src/tonemapping/tonemapping.wgsl | 12 ++- .../src/tonemapping/tonemapping_shared.wgsl | 8 ++ .../bevy_core_pipeline/src/upscaling/mod.rs | 12 +-- .../bevy_core_pipeline/src/upscaling/node.rs | 6 +- crates/bevy_pbr/src/material.rs | 8 +- crates/bevy_pbr/src/render/mesh.rs | 6 ++ crates/bevy_pbr/src/render/pbr.wgsl | 3 + crates/bevy_pbr/src/render/pbr_functions.wgsl | 6 ++ crates/bevy_sprite/src/mesh2d/material.rs | 8 +- crates/bevy_sprite/src/mesh2d/mesh.rs | 6 ++ crates/bevy_sprite/src/render/mod.rs | 14 ++- 15 files changed, 156 insertions(+), 51 deletions(-) diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs index 711fda3a57a2a..86906240b467c 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -75,7 +75,7 @@ impl Camera2dBundle { global_transform: Default::default(), camera: Camera::default(), camera_2d: Camera2d::default(), - tonemapping: Tonemapping { is_enabled: false }, + tonemapping: Tonemapping::Disabled, } } } diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index 20a0001457a9f..057f45c53a72c 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -74,7 +74,9 @@ impl Default for Camera3dBundle { fn default() -> Self { Self { camera_render_graph: CameraRenderGraph::new(crate::core_3d::graph::NAME), - tonemapping: Tonemapping { is_enabled: true }, + tonemapping: Tonemapping::Enabled { + deband_dither: true, + }, camera: Default::default(), projection: Default::default(), visible_entities: Default::default(), diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 3af2ecedccb18..3b417e8c82af9 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -12,7 +12,7 @@ use bevy_render::camera::Camera; use bevy_render::extract_component::{ExtractComponent, ExtractComponentPlugin}; use bevy_render::renderer::RenderDevice; use bevy_render::view::ViewTarget; -use bevy_render::{render_resource::*, RenderApp}; +use bevy_render::{render_resource::*, RenderApp, RenderStage}; const TONEMAPPING_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 17015368199668024512); @@ -42,7 +42,10 @@ impl Plugin for TonemappingPlugin { app.add_plugin(ExtractComponentPlugin::::default()); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.init_resource::(); + render_app + .init_resource::() + .init_resource::>() + .add_system_to_stage(RenderStage::Queue, queue_view_tonemapping_pipelines); } } } @@ -50,7 +53,40 @@ impl Plugin for TonemappingPlugin { #[derive(Resource)] pub struct TonemappingPipeline { texture_bind_group: BindGroupLayout, - tonemapping_pipeline_id: CachedRenderPipelineId, +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct TonemappingPipelineKey { + deband_dither: bool, +} + +impl SpecializedRenderPipeline for TonemappingPipeline { + type Key = TonemappingPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut shader_defs = Vec::new(); + if key.deband_dither { + shader_defs.push("DEBAND_DITHER".to_string()); + } + RenderPipelineDescriptor { + label: Some("tonemapping pipeline".into()), + layout: Some(vec![self.texture_bind_group.clone()]), + vertex: fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: TONEMAPPING_SHADER_HANDLE.typed(), + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: ViewTarget::TEXTURE_FORMAT_HDR, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + } + } } impl FromWorld for TonemappingPipeline { @@ -79,37 +115,50 @@ impl FromWorld for TonemappingPipeline { ], }); - let tonemap_descriptor = RenderPipelineDescriptor { - label: Some("tonemapping pipeline".into()), - layout: Some(vec![tonemap_texture_bind_group.clone()]), - vertex: fullscreen_shader_vertex_state(), - fragment: Some(FragmentState { - shader: TONEMAPPING_SHADER_HANDLE.typed(), - shader_defs: vec![], - entry_point: "fragment".into(), - targets: vec![Some(ColorTargetState { - format: ViewTarget::TEXTURE_FORMAT_HDR, - blend: None, - write_mask: ColorWrites::ALL, - })], - }), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - }; - - let mut cache = render_world.resource_mut::(); TonemappingPipeline { texture_bind_group: tonemap_texture_bind_group, - tonemapping_pipeline_id: cache.queue_render_pipeline(tonemap_descriptor), + } + } +} + +#[derive(Component)] +pub struct ViewTonemappingPipeline(CachedRenderPipelineId); + +pub fn queue_view_tonemapping_pipelines( + mut commands: Commands, + mut pipeline_cache: ResMut, + mut pipelines: ResMut>, + upscaling_pipeline: Res, + view_targets: Query<(Entity, &Tonemapping)>, +) { + for (entity, tonemapping) in view_targets.iter() { + if let Tonemapping::Enabled { deband_dither } = tonemapping { + let key = TonemappingPipelineKey { + deband_dither: *deband_dither, + }; + let pipeline = pipelines.specialize(&mut pipeline_cache, &upscaling_pipeline, key); + + commands + .entity(entity) + .insert(ViewTonemappingPipeline(pipeline)); } } } #[derive(Component, Clone, Reflect, Default)] #[reflect(Component)] -pub struct Tonemapping { - pub is_enabled: bool, +pub enum Tonemapping { + #[default] + Disabled, + Enabled { + deband_dither: bool, + }, +} + +impl Tonemapping { + pub fn is_enabled(&self) -> bool { + matches!(self, Tonemapping::Enabled { .. }) + } } impl ExtractComponent for Tonemapping { diff --git a/crates/bevy_core_pipeline/src/tonemapping/node.rs b/crates/bevy_core_pipeline/src/tonemapping/node.rs index 3a41a22025f12..f9edf882c73fa 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/node.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/node.rs @@ -1,6 +1,6 @@ use std::sync::Mutex; -use crate::tonemapping::{Tonemapping, TonemappingPipeline}; +use crate::tonemapping::{TonemappingPipeline, ViewTonemappingPipeline}; use bevy_ecs::prelude::*; use bevy_ecs::query::QueryState; use bevy_render::{ @@ -15,7 +15,7 @@ use bevy_render::{ }; pub struct TonemappingNode { - query: QueryState<(&'static ViewTarget, Option<&'static Tonemapping>), With>, + query: QueryState<(&'static ViewTarget, &'static ViewTonemappingPipeline), With>, cached_texture_bind_group: Mutex>, } @@ -54,14 +54,11 @@ impl Node for TonemappingNode { Err(_) => return Ok(()), }; - let tonemapping_enabled = tonemapping.map_or(false, |t| t.is_enabled); - if !tonemapping_enabled || !target.is_hdr() { + if !target.is_hdr() { return Ok(()); } - let pipeline = match pipeline_cache - .get_render_pipeline(tonemapping_pipeline.tonemapping_pipeline_id) - { + let pipeline = match pipeline_cache.get_render_pipeline(tonemapping.0) { Some(pipeline) => pipeline, None => return Ok(()), }; diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl index e18ae8a026f40..a4bc5d4be4364 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl @@ -10,5 +10,15 @@ var hdr_sampler: sampler; fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { let hdr_color = textureSample(hdr_texture, hdr_sampler, in.uv); - return vec4(reinhard_luminance(hdr_color.rgb), hdr_color.a); + var output_rgb = reinhard_luminance(hdr_color.rgb); + +#ifdef DEBAND_DITHER + output_rgb = pow(output_rgb.rgb, vec3(1.0 / 2.2)); + output_rgb = output_rgb + screen_space_dither(in.position.xy); + // This conversion back to linear space is required because our output texture format is + // SRGB; the GPU will assume our output is linear and will apply an SRGB conversion. + output_rgb = pow(output_rgb.rgb, vec3(2.2)); +#endif + + return vec4(output_rgb, hdr_color.a); } diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl index d71dd12f08f32..deafac0750d84 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl @@ -27,3 +27,11 @@ fn reinhard_luminance(color: vec3) -> vec3 { let l_new = l_old / (1.0 + l_old); return tonemapping_change_luminance(color, l_new); } + +// Source: Advanced VR Rendering, GDC 2015, Alex Vlachos, Valve, Slide 49 +// https://media.steampowered.com/apps/valve/2015/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf +fn screen_space_dither(frag_coord: vec2) -> vec3 { + var dither = vec3(dot(vec2(171.0, 231.0), frag_coord)).xxx; + dither = fract(dither.rgb / vec3(103.0, 71.0, 97.0)); + return (dither - 0.5) / 255.0; +} \ No newline at end of file diff --git a/crates/bevy_core_pipeline/src/upscaling/mod.rs b/crates/bevy_core_pipeline/src/upscaling/mod.rs index 565ebad7c15f0..139d060eaf7c7 100644 --- a/crates/bevy_core_pipeline/src/upscaling/mod.rs +++ b/crates/bevy_core_pipeline/src/upscaling/mod.rs @@ -31,7 +31,7 @@ impl Plugin for UpscalingPlugin { render_app .init_resource::() .init_resource::>() - .add_system_to_stage(RenderStage::Queue, queue_upscaling_bind_groups); + .add_system_to_stage(RenderStage::Queue, queue_view_upscaling_pipelines); } } } @@ -110,11 +110,9 @@ impl SpecializedRenderPipeline for UpscalingPipeline { } #[derive(Component)] -pub struct UpscalingTarget { - pub pipeline: CachedRenderPipelineId, -} +pub struct ViewUpscalingPipeline(CachedRenderPipelineId); -fn queue_upscaling_bind_groups( +fn queue_view_upscaling_pipelines( mut commands: Commands, mut pipeline_cache: ResMut, mut pipelines: ResMut>, @@ -128,6 +126,8 @@ fn queue_upscaling_bind_groups( }; let pipeline = pipelines.specialize(&mut pipeline_cache, &upscaling_pipeline, key); - commands.entity(entity).insert(UpscalingTarget { pipeline }); + commands + .entity(entity) + .insert(ViewUpscalingPipeline(pipeline)); } } diff --git a/crates/bevy_core_pipeline/src/upscaling/node.rs b/crates/bevy_core_pipeline/src/upscaling/node.rs index fd785a46477fa..895c3e5e1b0a2 100644 --- a/crates/bevy_core_pipeline/src/upscaling/node.rs +++ b/crates/bevy_core_pipeline/src/upscaling/node.rs @@ -13,10 +13,10 @@ use bevy_render::{ view::{ExtractedView, ViewTarget}, }; -use super::{UpscalingPipeline, UpscalingTarget}; +use super::{UpscalingPipeline, ViewUpscalingPipeline}; pub struct UpscalingNode { - query: QueryState<(&'static ViewTarget, &'static UpscalingTarget), With>, + query: QueryState<(&'static ViewTarget, &'static ViewUpscalingPipeline), With>, cached_texture_bind_group: Mutex>, } @@ -89,7 +89,7 @@ impl Node for UpscalingNode { } }; - let pipeline = match pipeline_cache.get_render_pipeline(upscaling_target.pipeline) { + let pipeline = match pipeline_cache.get_render_pipeline(upscaling_target.0) { Some(pipeline) => pipeline, None => return Ok(()), }; diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index d294b7eeb386a..2dc0e0f3ed59e 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -363,9 +363,13 @@ pub fn queue_material_meshes( let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples) | MeshPipelineKey::from_hdr(view.hdr); - if let Some(tonemapping) = tonemapping { - if tonemapping.is_enabled && !view.hdr { + if let Some(Tonemapping::Enabled { deband_dither }) = tonemapping { + if !view.hdr { view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; + + if *deband_dither { + view_key |= MeshPipelineKey::DEBAND_DITHER; + } } } let rangefinder = view.rangefinder3d(); diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index b89e521eff824..73e7eea7b0c5f 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -518,6 +518,7 @@ bitflags::bitflags! { const TRANSPARENT_MAIN_PASS = (1 << 0); const HDR = (1 << 1); const TONEMAP_IN_SHADER = (1 << 2); + const DEBAND_DITHER = (1 << 3); const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; } @@ -636,6 +637,11 @@ impl SpecializedMeshPipeline for MeshPipeline { if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { shader_defs.push("TONEMAP_IN_SHADER".to_string()); + + // Debanding is tied to tonemapping in the shader, cannot run without it. + if key.contains(MeshPipelineKey::DEBAND_DITHER) { + shader_defs.push("DEBAND_DITHER".to_string()); + } } let format = match key.contains(MeshPipelineKey::HDR) { diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 716ef89a4350b..6f5d94edfbaf3 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -97,6 +97,9 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { #ifdef TONEMAP_IN_SHADER output_color = tone_mapping(output_color); +#endif +#ifdef DEBAND_DITHER + output_color = dither(output_color, in.frag_coord.xy); #endif return output_color; } diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 3e90463534163..4f4c5495dbaa3 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -262,3 +262,9 @@ fn tone_mapping(in: vec4) -> vec4 { } #endif +#ifdef DEBAND_DITHER +fn dither(color: vec4, pos: vec2) -> vec4 { + return vec4(color.rgb + screen_space_dither(pos.xy), color.a); +} +#endif + diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 2e4e03179b216..72dac1b39095e 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -328,9 +328,13 @@ pub fn queue_material2d_meshes( let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples) | Mesh2dPipelineKey::from_hdr(view.hdr); - if let Some(tonemapping) = tonemapping { - if tonemapping.is_enabled && !view.hdr { + if let Some(Tonemapping::Enabled { deband_dither }) = tonemapping { + if !view.hdr { view_key |= Mesh2dPipelineKey::TONEMAP_IN_SHADER; + + if *deband_dither { + view_key |= Mesh2dPipelineKey::DEBAND_DITHER; + } } } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 43a932f53d565..5483c2b98d972 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -288,6 +288,7 @@ bitflags::bitflags! { const NONE = 0; const HDR = (1 << 0); const TONEMAP_IN_SHADER = (1 << 1); + const DEBAND_DITHER = (1 << 2); const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; } @@ -376,6 +377,11 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { if key.contains(Mesh2dPipelineKey::TONEMAP_IN_SHADER) { shader_defs.push("TONEMAP_IN_SHADER".to_string()); + + // Debanding is tied to tonemapping in the shader, cannot run without it. + if key.contains(Mesh2dPipelineKey::DEBAND_DITHER) { + shader_defs.push("DEBAND_DITHER".to_string()); + } } let vertex_buffer_layout = layout.get_layout(&vertex_attributes)?; diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 931f99ac6c97b..8b71558813973 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -151,6 +151,7 @@ bitflags::bitflags! { const COLORED = (1 << 0); const HDR = (1 << 1); const TONEMAP_IN_SHADER = (1 << 2); + const DEBAND_DITHER = (1 << 3); const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; } } @@ -212,6 +213,11 @@ impl SpecializedRenderPipeline for SpritePipeline { if key.contains(SpritePipelineKey::TONEMAP_IN_SHADER) { shader_defs.push("TONEMAP_IN_SHADER".to_string()); + + // Debanding is tied to tonemapping in the shader, cannot run without it. + if key.contains(SpritePipelineKey::DEBAND_DITHER) { + shader_defs.push("DEBAND_DITHER".to_string()); + } } let format = match key.contains(SpritePipelineKey::HDR) { @@ -508,9 +514,13 @@ pub fn queue_sprites( for (mut transparent_phase, visible_entities, view, tonemapping) in &mut views { let mut view_key = SpritePipelineKey::from_hdr(view.hdr) | msaa_key; - if let Some(tonemapping) = tonemapping { - if tonemapping.is_enabled && !view.hdr { + if let Some(Tonemapping::Enabled { deband_dither }) = tonemapping { + if !view.hdr { view_key |= SpritePipelineKey::TONEMAP_IN_SHADER; + + if *deband_dither { + view_key |= SpritePipelineKey::DEBAND_DITHER; + } } } let pipeline = pipelines.specialize(