diff --git a/Cargo.toml b/Cargo.toml index 2306477cc4012..6de010585267e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1256,6 +1256,16 @@ description = "Various test cases for hierarchy and transform propagation perfor category = "Stress Tests" wasm = true +[[example]] +name = "color_banding" +path = "examples/stress_tests/color_banding.rs" + +[package.metadata.example.color_banding] +name = "Color Banding" +description = "Test cases for extreme color banding" +category = "Stress Tests" +wasm = false + # Tools [[example]] name = "scene_viewer" diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl index 6e712fca4ffa8..3ed2247b685e4 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping.wgsl @@ -10,5 +10,11 @@ var hdr_sampler: sampler; fn fs_main(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 = vec3(reinhard_luminance(hdr_color.rgb)); + 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)); + 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..c3105aa51b6df 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 / 255.0; +} \ No newline at end of file diff --git a/examples/stress_tests/color_banding.rs b/examples/stress_tests/color_banding.rs new file mode 100644 index 0000000000000..7a02de63336f5 --- /dev/null +++ b/examples/stress_tests/color_banding.rs @@ -0,0 +1,61 @@ +//! Useful for stress-testing rendering cases that exhibit extreme color banding. + +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(AmbientLight { + color: Color::BLACK, + brightness: 0.0, + }) + .add_startup_system(setup) + .run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let colors = [Color::WHITE, Color::RED, Color::GREEN, Color::BLUE]; + let x = [-5.0, 5.0, -5.0, 5.0]; + let z = [-5.0, -5.0, 5.0, 5.0]; + let mesh = meshes.add(Mesh::from(shape::Plane { size: 10.0 })); + + for i in 0..4 { + // plane + commands.spawn_bundle(PbrBundle { + mesh: mesh.clone(), + material: materials.add(custom_material(colors[i])), + transform: Transform::from_xyz(x[i], 0.0, z[i]), + ..default() + }); + } + // light + commands.spawn_bundle(PointLightBundle { + point_light: PointLight { + intensity: 50.0, + shadows_enabled: false, + ..default() + }, + transform: Transform::from_xyz(0.0, 5.0, 0.0), + ..default() + }); + // camera + commands.spawn_bundle(Camera3dBundle { + transform: Transform::from_xyz(0.0, 6.0, 0.0).looking_at(Vec3::default(), -Vec3::Z), + ..default() + }); +} + +fn custom_material(color: Color) -> StandardMaterial { + StandardMaterial { + base_color: color, + perceptual_roughness: 1.0, + metallic: 0.0, + reflectance: 0.0, + ..default() + } +}