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 58eb7023e7243..de2e83e4a4681 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(