bevy/crates/bevy_pbr/src/render/shadow_sampling.wgsl
JMS55 1f95a484ed
PCF For DirectionalLight/SpotLight Shadows (#8006)
# Objective

- Improve antialiasing for non-point light shadow edges.
- Very partially addresses
https://github.com/bevyengine/bevy/issues/3628.

## Solution

- Implements "The Witness"'s shadow map sampling technique.
  - Ported from @superdump's old branch, all credit to them :)
- Implements "Call of Duty: Advanced Warfare"'s stochastic shadow map
sampling technique when the velocity prepass is enabled, for use with
TAA.
- Uses interleaved gradient noise to generate a random angle, and then
averages 8 samples in a spiral pattern, rotated by the random angle.
- I also tried spatiotemporal blue noise, but it was far too noisy to be
filtered by TAA alone. In the future, we should try spatiotemporal blue
noise + a specialized shadow denoiser such as
https://gpuopen.com/fidelityfx-denoiser/#shadow. This approach would
also be useful for hybrid rasterized applications with raytraced
shadows.
- The COD presentation has an interesting temporal dithering of the
noise for use with temporal supersampling that we should revisit when we
get DLSS/FSR/other TSR.

---

## Changelog

* Added `ShadowFilteringMethod`. Improved directional light and
spotlight shadow edges to be less aliased.

## Migration Guide

* Shadows cast by directional lights or spotlights now have smoother
edges. To revert to the old behavior, add
`ShadowFilteringMethod::Hardware2x2` to your cameras.

---------

Co-authored-by: IceSentry <c.giguere42@gmail.com>
Co-authored-by: Daniel Chia <danstryder@gmail.com>
Co-authored-by: robtfm <50659922+robtfm@users.noreply.github.com>
Co-authored-by: Brandon Dyer <brandondyer64@gmail.com>
Co-authored-by: Edgar Geier <geieredgar@gmail.com>
Co-authored-by: Robert Swain <robert.swain@gmail.com>
Co-authored-by: Elabajaba <Elabajaba@users.noreply.github.com>
Co-authored-by: IceSentry <IceSentry@users.noreply.github.com>
2023-10-07 17:13:29 +00:00

132 lines
6.4 KiB
WebGPU Shading Language

#define_import_path bevy_pbr::shadow_sampling
#import bevy_pbr::mesh_view_bindings as view_bindings
#import bevy_pbr::utils PI
// Do the lookup, using HW 2x2 PCF and comparison
fn sample_shadow_map_hardware(light_local: vec2<f32>, depth: f32, array_index: i32) -> f32 {
#ifdef NO_ARRAY_TEXTURES_SUPPORT
return textureSampleCompareLevel(
view_bindings::directional_shadow_textures,
view_bindings::directional_shadow_textures_sampler,
light_local,
depth,
);
#else
return textureSampleCompareLevel(
view_bindings::directional_shadow_textures,
view_bindings::directional_shadow_textures_sampler,
light_local,
array_index,
depth,
);
#endif
}
// https://web.archive.org/web/20230210095515/http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1
fn sample_shadow_map_castano_thirteen(light_local: vec2<f32>, depth: f32, array_index: i32) -> f32 {
let shadow_map_size = vec2<f32>(textureDimensions(view_bindings::directional_shadow_textures));
let inv_shadow_map_size = 1.0 / shadow_map_size;
let uv = light_local * shadow_map_size;
var base_uv = floor(uv + 0.5);
let s = (uv.x + 0.5 - base_uv.x);
let t = (uv.y + 0.5 - base_uv.y);
base_uv -= 0.5;
base_uv *= inv_shadow_map_size;
let uw0 = (4.0 - 3.0 * s);
let uw1 = 7.0;
let uw2 = (1.0 + 3.0 * s);
let u0 = (3.0 - 2.0 * s) / uw0 - 2.0;
let u1 = (3.0 + s) / uw1;
let u2 = s / uw2 + 2.0;
let vw0 = (4.0 - 3.0 * t);
let vw1 = 7.0;
let vw2 = (1.0 + 3.0 * t);
let v0 = (3.0 - 2.0 * t) / vw0 - 2.0;
let v1 = (3.0 + t) / vw1;
let v2 = t / vw2 + 2.0;
var sum = 0.0;
sum += uw0 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u0, v0) * inv_shadow_map_size), depth, array_index);
sum += uw1 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u1, v0) * inv_shadow_map_size), depth, array_index);
sum += uw2 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u2, v0) * inv_shadow_map_size), depth, array_index);
sum += uw0 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u0, v1) * inv_shadow_map_size), depth, array_index);
sum += uw1 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u1, v1) * inv_shadow_map_size), depth, array_index);
sum += uw2 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u2, v1) * inv_shadow_map_size), depth, array_index);
sum += uw0 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u0, v2) * inv_shadow_map_size), depth, array_index);
sum += uw1 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u1, v2) * inv_shadow_map_size), depth, array_index);
sum += uw2 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u2, v2) * inv_shadow_map_size), depth, array_index);
return sum * (1.0 / 144.0);
}
// https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence
fn interleaved_gradient_noise(pixel_coordinates: vec2<f32>) -> f32 {
let frame = f32(view_bindings::globals.frame_count % 64u);
let xy = pixel_coordinates + 5.588238 * frame;
return fract(52.9829189 * fract(0.06711056 * xy.x + 0.00583715 * xy.y));
}
fn map(min1: f32, max1: f32, min2: f32, max2: f32, value: f32) -> f32 {
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
}
fn sample_shadow_map_jimenez_fourteen(light_local: vec2<f32>, depth: f32, array_index: i32, texel_size: f32) -> f32 {
let shadow_map_size = vec2<f32>(textureDimensions(view_bindings::directional_shadow_textures));
let random_angle = 2.0 * PI * interleaved_gradient_noise(light_local * shadow_map_size);
let m = vec2(sin(random_angle), cos(random_angle));
let rotation_matrix = mat2x2(
m.y, -m.x,
m.x, m.y
);
// Empirically chosen fudge factor to make PCF look better across different CSM cascades
let f = map(0.00390625, 0.022949219, 0.015, 0.035, texel_size);
let uv_offset_scale = f / (texel_size * shadow_map_size);
// https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135)
let sample_offset1 = (rotation_matrix * vec2(-0.7071, 0.7071)) * uv_offset_scale;
let sample_offset2 = (rotation_matrix * vec2(-0.0000, -0.8750)) * uv_offset_scale;
let sample_offset3 = (rotation_matrix * vec2( 0.5303, 0.5303)) * uv_offset_scale;
let sample_offset4 = (rotation_matrix * vec2(-0.6250, -0.0000)) * uv_offset_scale;
let sample_offset5 = (rotation_matrix * vec2( 0.3536, -0.3536)) * uv_offset_scale;
let sample_offset6 = (rotation_matrix * vec2(-0.0000, 0.3750)) * uv_offset_scale;
let sample_offset7 = (rotation_matrix * vec2(-0.1768, -0.1768)) * uv_offset_scale;
let sample_offset8 = (rotation_matrix * vec2( 0.1250, 0.0000)) * uv_offset_scale;
var sum = 0.0;
sum += sample_shadow_map_hardware(light_local + sample_offset1, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset2, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset3, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset4, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset5, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset6, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset7, depth, array_index);
sum += sample_shadow_map_hardware(light_local + sample_offset8, depth, array_index);
return sum / 8.0;
}
fn sample_shadow_map(light_local: vec2<f32>, depth: f32, array_index: i32, texel_size: f32) -> f32 {
#ifdef SHADOW_FILTER_METHOD_CASTANO_13
return sample_shadow_map_castano_thirteen(light_local, depth, array_index);
#else ifdef SHADOW_FILTER_METHOD_JIMENEZ_14
return sample_shadow_map_jimenez_fourteen(light_local, depth, array_index, texel_size);
#else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2
return sample_shadow_map_hardware(light_local, depth, array_index);
#else
// This needs a default return value to avoid shader compilation errors if it's compiled with no SHADOW_FILTER_METHOD_* defined.
// (eg. if the normal prepass is enabled it ends up compiling this due to the normal prepass depending on pbr_functions, which depends on shadows)
// This should never actually get used, as anyone using bevy's lighting/shadows should always have a SHADOW_FILTER_METHOD defined.
// Set to 0 to make it obvious that something is wrong.
return 0.0;
#endif
}