From b610866f5e4d826f1127af3e8d6dca6e0ec62b91 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Fri, 26 Dec 2025 00:22:30 +0100 Subject: [PATCH 1/3] Use PCSS blocker search radius that scales only with cascade resolution This results in the PCSS soft shadows being harder close to the contact of the object with the receiving surface, and softer when there is more distance between them. --- crates/bevy_pbr/src/render/shadow_sampling.wgsl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index d4f0751e17e68..8a6de632d0151 100644 --- a/crates/bevy_pbr/src/render/shadow_sampling.wgsl +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -222,11 +222,9 @@ fn search_for_blockers_in_shadow_map( light_local: vec2, depth: f32, array_index: i32, - texel_size: f32, - search_size: f32, ) -> f32 { let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); - let uv_offset_scale = search_size / (texel_size * shadow_map_size); + let uv_offset_scale = PENUMBRA_FILTER_SIZE / shadow_map_size; let offset0 = D3D_SAMPLE_POINT_POSITIONS[0] * uv_offset_scale; let offset1 = D3D_SAMPLE_POINT_POSITIONS[1] * uv_offset_scale; @@ -296,8 +294,7 @@ fn sample_shadow_map_pcss( light_size: f32, ) -> f32 { // Determine the average Z value of the closest blocker. - let z_blocker = search_for_blockers_in_shadow_map( - light_local, depth, array_index, texel_size, light_size); + let z_blocker = search_for_blockers_in_shadow_map(light_local, depth, array_index); // Don't let the blur size go below 0.5, or shadows will look unacceptably aliased. let blur_size = max((z_blocker - depth) * light_size / depth, 0.5); From c9854680c7a47ed00254e71e7f10d5c308a4611b Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Fri, 26 Dec 2025 00:23:06 +0100 Subject: [PATCH 2/3] Randomly rotate the PCSS blocker search offsets This removes some 'ghost' shadows that otherwise appear. --- .../bevy_pbr/src/render/shadow_sampling.wgsl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index 8a6de632d0151..f4d0879ca071d 100644 --- a/crates/bevy_pbr/src/render/shadow_sampling.wgsl +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -208,6 +208,8 @@ fn sample_shadow_map_jimenez_fourteen( return sum / 8.0; } +const PENUMBRA_FILTER_SIZE: vec2 = vec2(128.0f); + // Performs the blocker search portion of percentage-closer soft shadows (PCSS). // This is the variation used for directional lights. // @@ -224,16 +226,17 @@ fn search_for_blockers_in_shadow_map( array_index: i32, ) -> f32 { let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + let rotation_matrix = random_rotation_matrix(light_local * shadow_map_size, false); let uv_offset_scale = PENUMBRA_FILTER_SIZE / shadow_map_size; - let offset0 = D3D_SAMPLE_POINT_POSITIONS[0] * uv_offset_scale; - let offset1 = D3D_SAMPLE_POINT_POSITIONS[1] * uv_offset_scale; - let offset2 = D3D_SAMPLE_POINT_POSITIONS[2] * uv_offset_scale; - let offset3 = D3D_SAMPLE_POINT_POSITIONS[3] * uv_offset_scale; - let offset4 = D3D_SAMPLE_POINT_POSITIONS[4] * uv_offset_scale; - let offset5 = D3D_SAMPLE_POINT_POSITIONS[5] * uv_offset_scale; - let offset6 = D3D_SAMPLE_POINT_POSITIONS[6] * uv_offset_scale; - let offset7 = D3D_SAMPLE_POINT_POSITIONS[7] * uv_offset_scale; + let offset0 = rotation_matrix * D3D_SAMPLE_POINT_POSITIONS[0] * uv_offset_scale; + let offset1 = rotation_matrix * D3D_SAMPLE_POINT_POSITIONS[1] * uv_offset_scale; + let offset2 = rotation_matrix * D3D_SAMPLE_POINT_POSITIONS[2] * uv_offset_scale; + let offset3 = rotation_matrix * D3D_SAMPLE_POINT_POSITIONS[3] * uv_offset_scale; + let offset4 = rotation_matrix * D3D_SAMPLE_POINT_POSITIONS[4] * uv_offset_scale; + let offset5 = rotation_matrix * D3D_SAMPLE_POINT_POSITIONS[5] * uv_offset_scale; + let offset6 = rotation_matrix * D3D_SAMPLE_POINT_POSITIONS[6] * uv_offset_scale; + let offset7 = rotation_matrix * D3D_SAMPLE_POINT_POSITIONS[7] * uv_offset_scale; var sum = vec2(0.0); sum += search_for_blockers_in_shadow_map_hardware(light_local + offset0, depth, array_index); From a2998dac6e69a28decbc432e9b54265822725832 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Tue, 6 Jan 2026 14:31:16 +0100 Subject: [PATCH 3/3] Use screen UV for interleaved gradient noise random rotations --- crates/bevy_pbr/src/render/shadow_sampling.wgsl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index f4d0879ca071d..4706665a53a4a 100644 --- a/crates/bevy_pbr/src/render/shadow_sampling.wgsl +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -209,6 +209,9 @@ fn sample_shadow_map_jimenez_fourteen( } const PENUMBRA_FILTER_SIZE: vec2 = vec2(128.0f); +// Offset for use with interleaved gradient noise and similar to avoid using the +// same noise pattern for multiple algorithms +const BLOCKER_SEARCH_OFFSET: vec2 = vec2(17.23f); // Performs the blocker search portion of percentage-closer soft shadows (PCSS). // This is the variation used for directional lights. @@ -224,9 +227,10 @@ fn search_for_blockers_in_shadow_map( light_local: vec2, depth: f32, array_index: i32, + frag_coord_xy: vec2, ) -> f32 { let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); - let rotation_matrix = random_rotation_matrix(light_local * shadow_map_size, false); + let rotation_matrix = random_rotation_matrix(frag_coord_xy + BLOCKER_SEARCH_OFFSET, false); let uv_offset_scale = PENUMBRA_FILTER_SIZE / shadow_map_size; let offset0 = rotation_matrix * D3D_SAMPLE_POINT_POSITIONS[0] * uv_offset_scale; @@ -297,7 +301,7 @@ fn sample_shadow_map_pcss( light_size: f32, ) -> f32 { // Determine the average Z value of the closest blocker. - let z_blocker = search_for_blockers_in_shadow_map(light_local, depth, array_index); + let z_blocker = search_for_blockers_in_shadow_map(light_local, depth, array_index, frag_coord_xy); // Don't let the blur size go below 0.5, or shadows will look unacceptably aliased. let blur_size = max((z_blocker - depth) * light_size / depth, 0.5);