Implement percentage-closer filtering (PCF) for point lights. (#12910)

I ported the two existing PCF techniques to the cubemap domain as best I
could. Generally, the technique is to create a 2D orthonormal basis
using Gram-Schmidt normalization, then apply the technique over that
basis. The results look fine, though the shadow bias often needs
adjusting.

For comparison, Unity uses a 4-tap pattern for PCF on point lights of
(1, 1, 1), (-1, -1, 1), (-1, 1, -1), (1, -1, -1). I tried this but
didn't like the look, so I went with the design above, which ports the
2D techniques to the 3D domain. There's surprisingly little material on
point light PCF.

I've gone through every example using point lights and verified that the
shadow maps look fine, adjusting biases as necessary.

Fixes #3628.

---

## Changelog

### Added
* Shadows from point lights now support percentage-closer filtering
(PCF), and as a result look less aliased.

### Changed
* `ShadowFilteringMethod::Castano13` and
`ShadowFilteringMethod::Jimenez14` have been renamed to
`ShadowFilteringMethod::Gaussian` and `ShadowFilteringMethod::Temporal`
respectively.

## Migration Guide

* `ShadowFilteringMethod::Castano13` and
`ShadowFilteringMethod::Jimenez14` have been renamed to
`ShadowFilteringMethod::Gaussian` and `ShadowFilteringMethod::Temporal`
respectively.
This commit is contained in:
Patrick Walton 2024-04-10 15:16:08 -05:00 committed by GitHub
parent ddcbb3cc80
commit d59b1e71ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 282 additions and 68 deletions

View file

@ -308,10 +308,10 @@ impl SpecializedRenderPipeline for DeferredLightingLayout {
key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS);
if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 {
shader_defs.push("SHADOW_FILTER_METHOD_HARDWARE_2X2".into());
} else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13 {
shader_defs.push("SHADOW_FILTER_METHOD_CASTANO_13".into());
} else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14 {
shader_defs.push("SHADOW_FILTER_METHOD_JIMENEZ_14".into());
} else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN {
shader_defs.push("SHADOW_FILTER_METHOD_GAUSSIAN".into());
} else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL {
shader_defs.push("SHADOW_FILTER_METHOD_TEMPORAL".into());
}
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
@ -489,11 +489,11 @@ pub fn prepare_deferred_lighting_pipelines(
ShadowFilteringMethod::Hardware2x2 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2;
}
ShadowFilteringMethod::Castano13 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13;
ShadowFilteringMethod::Gaussian => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN;
}
ShadowFilteringMethod::Jimenez14 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14;
ShadowFilteringMethod::Temporal => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL;
}
}

View file

@ -460,8 +460,6 @@ pub struct TransmittedShadowReceiver;
///
/// The different modes use different approaches to
/// [Percentage Closer Filtering](https://developer.nvidia.com/gpugems/gpugems/part-ii-lighting-and-shadows/chapter-11-shadow-map-antialiasing).
///
/// Currently does not affect point lights.
#[derive(Component, ExtractComponent, Reflect, Clone, Copy, PartialEq, Eq, Default)]
#[reflect(Component, Default)]
pub enum ShadowFilteringMethod {
@ -469,20 +467,29 @@ pub enum ShadowFilteringMethod {
///
/// Fast but poor quality.
Hardware2x2,
/// Method by Ignacio Castaño for The Witness using 9 samples and smart
/// filtering to achieve the same as a regular 5x5 filter kernel.
/// Approximates a fixed Gaussian blur, good when TAA isn't in use.
///
/// Good quality, good performance.
///
/// For directional and spot lights, this uses a [method by Ignacio Castaño
/// for *The Witness*] using 9 samples and smart filtering to achieve the same
/// as a regular 5x5 filter kernel.
///
/// [method by Ignacio Castaño for *The Witness*]: https://web.archive.org/web/20230210095515/http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/
#[default]
Castano13,
/// Method by Jorge Jimenez for Call of Duty: Advanced Warfare using 8
/// samples in spiral pattern, randomly-rotated by interleaved gradient
/// noise with spatial variation.
Gaussian,
/// A randomized filter that varies over time, good when TAA is in use.
///
/// Good quality when used with
/// [`TemporalAntiAliasSettings`](bevy_core_pipeline::experimental::taa::TemporalAntiAliasSettings)
/// and good performance.
Jimenez14,
///
/// For directional and spot lights, this uses a [method by Jorge Jimenez for
/// *Call of Duty: Advanced Warfare*] using 8 samples in spiral pattern,
/// randomly-rotated by interleaved gradient noise with spatial variation.
///
/// [method by Jorge Jimenez for *Call of Duty: Advanced Warfare*]: https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/
Temporal,
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]

View file

@ -51,6 +51,6 @@ impl Default for PointLight {
}
impl PointLight {
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.08;
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6;
}

View file

@ -618,11 +618,11 @@ pub fn queue_material_meshes<M: Material>(
ShadowFilteringMethod::Hardware2x2 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2;
}
ShadowFilteringMethod::Castano13 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13;
ShadowFilteringMethod::Gaussian => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN;
}
ShadowFilteringMethod::Jimenez14 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14;
ShadowFilteringMethod::Temporal => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL;
}
}

View file

@ -113,11 +113,11 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass<M: Material>(
ShadowFilteringMethod::Hardware2x2 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2;
}
ShadowFilteringMethod::Castano13 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13;
ShadowFilteringMethod::Gaussian => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN;
}
ShadowFilteringMethod::Jimenez14 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14;
ShadowFilteringMethod::Temporal => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL;
}
}

View file

@ -1068,8 +1068,8 @@ bitflags::bitflags! {
const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS;
const SHADOW_FILTER_METHOD_RESERVED_BITS = Self::SHADOW_FILTER_METHOD_MASK_BITS << Self::SHADOW_FILTER_METHOD_SHIFT_BITS;
const SHADOW_FILTER_METHOD_HARDWARE_2X2 = 0 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS;
const SHADOW_FILTER_METHOD_CASTANO_13 = 1 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS;
const SHADOW_FILTER_METHOD_JIMENEZ_14 = 2 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS;
const SHADOW_FILTER_METHOD_GAUSSIAN = 1 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS;
const SHADOW_FILTER_METHOD_TEMPORAL = 2 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS;
const VIEW_PROJECTION_RESERVED_BITS = Self::VIEW_PROJECTION_MASK_BITS << Self::VIEW_PROJECTION_SHIFT_BITS;
const VIEW_PROJECTION_NONSTANDARD = 0 << Self::VIEW_PROJECTION_SHIFT_BITS;
const VIEW_PROJECTION_PERSPECTIVE = 1 << Self::VIEW_PROJECTION_SHIFT_BITS;
@ -1397,10 +1397,10 @@ impl SpecializedMeshPipeline for MeshPipeline {
key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS);
if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 {
shader_defs.push("SHADOW_FILTER_METHOD_HARDWARE_2X2".into());
} else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13 {
shader_defs.push("SHADOW_FILTER_METHOD_CASTANO_13".into());
} else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14 {
shader_defs.push("SHADOW_FILTER_METHOD_JIMENEZ_14".into());
} else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN {
shader_defs.push("SHADOW_FILTER_METHOD_GAUSSIAN".into());
} else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL {
shader_defs.push("SHADOW_FILTER_METHOD_TEMPORAL".into());
}
let blur_quality =

View file

@ -5,6 +5,7 @@
utils::{PI, interleaved_gradient_noise},
utils,
}
#import bevy_render::maths::orthonormalize
// Do the lookup, using HW 2x2 PCF and comparison
fn sample_shadow_map_hardware(light_local: vec2<f32>, depth: f32, array_index: i32) -> f32 {
@ -26,6 +27,40 @@ fn sample_shadow_map_hardware(light_local: vec2<f32>, depth: f32, array_index: i
#endif
}
// Numbers determined by trial and error that gave nice results.
const SPOT_SHADOW_TEXEL_SIZE: f32 = 0.0134277345;
const POINT_SHADOW_SCALE: f32 = 0.003;
const POINT_SHADOW_TEMPORAL_OFFSET_SCALE: f32 = 0.5;
// These are the standard MSAA sample point positions from D3D. They were chosen
// to get a reasonable distribution that's not too regular.
//
// https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_standard_multisample_quality_levels?redirectedfrom=MSDN
const D3D_SAMPLE_POINT_POSITIONS: array<vec2<f32>, 8> = array(
vec2( 0.125, -0.375),
vec2(-0.125, 0.375),
vec2( 0.625, 0.125),
vec2(-0.375, -0.625),
vec2(-0.625, 0.625),
vec2(-0.875, -0.125),
vec2( 0.375, 0.875),
vec2( 0.875, -0.875),
);
// And these are the coefficients corresponding to the probability distribution
// function of a 2D Gaussian lobe with zero mean and the identity covariance
// matrix at those points.
const D3D_SAMPLE_POINT_COEFFS: array<f32, 8> = array(
0.157112,
0.157112,
0.138651,
0.130251,
0.114946,
0.114946,
0.107982,
0.079001,
);
// 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));
@ -75,31 +110,39 @@ 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, view_bindings::globals.frame_count);
// Creates a random rotation matrix using interleaved gradient noise.
//
// See: https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/
fn random_rotation_matrix(scale: vec2<f32>) -> mat2x2<f32> {
let random_angle = 2.0 * PI * interleaved_gradient_noise(
scale, view_bindings::globals.frame_count);
let m = vec2(sin(random_angle), cos(random_angle));
let rotation_matrix = mat2x2(
return mat2x2(
m.y, -m.x,
m.x, m.y
);
}
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 rotation_matrix = random_rotation_matrix(light_local * shadow_map_size);
// 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 * utils::SPIRAL_OFFSET_0_) * uv_offset_scale;
let sample_offset2 = (rotation_matrix * utils::SPIRAL_OFFSET_1_) * uv_offset_scale;
let sample_offset3 = (rotation_matrix * utils::SPIRAL_OFFSET_2_) * uv_offset_scale;
let sample_offset4 = (rotation_matrix * utils::SPIRAL_OFFSET_3_) * uv_offset_scale;
let sample_offset5 = (rotation_matrix * utils::SPIRAL_OFFSET_4_) * uv_offset_scale;
let sample_offset6 = (rotation_matrix * utils::SPIRAL_OFFSET_5_) * uv_offset_scale;
let sample_offset7 = (rotation_matrix * utils::SPIRAL_OFFSET_6_) * uv_offset_scale;
let sample_offset8 = (rotation_matrix * utils::SPIRAL_OFFSET_7_) * uv_offset_scale;
let sample_offset0 = (rotation_matrix * utils::SPIRAL_OFFSET_0_) * uv_offset_scale;
let sample_offset1 = (rotation_matrix * utils::SPIRAL_OFFSET_1_) * uv_offset_scale;
let sample_offset2 = (rotation_matrix * utils::SPIRAL_OFFSET_2_) * uv_offset_scale;
let sample_offset3 = (rotation_matrix * utils::SPIRAL_OFFSET_3_) * uv_offset_scale;
let sample_offset4 = (rotation_matrix * utils::SPIRAL_OFFSET_4_) * uv_offset_scale;
let sample_offset5 = (rotation_matrix * utils::SPIRAL_OFFSET_5_) * uv_offset_scale;
let sample_offset6 = (rotation_matrix * utils::SPIRAL_OFFSET_6_) * uv_offset_scale;
let sample_offset7 = (rotation_matrix * utils::SPIRAL_OFFSET_7_) * uv_offset_scale;
var sum = 0.0;
sum += sample_shadow_map_hardware(light_local + sample_offset0, depth, array_index);
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);
@ -107,14 +150,13 @@ fn sample_shadow_map_jimenez_fourteen(light_local: vec2<f32>, depth: f32, array_
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
#ifdef SHADOW_FILTER_METHOD_GAUSSIAN
return sample_shadow_map_castano_thirteen(light_local, depth, array_index);
#else ifdef SHADOW_FILTER_METHOD_JIMENEZ_14
#else ifdef SHADOW_FILTER_METHOD_TEMPORAL
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);
@ -126,3 +168,160 @@ fn sample_shadow_map(light_local: vec2<f32>, depth: f32, array_index: i32, texel
return 0.0;
#endif
}
// NOTE: Due to the non-uniform control flow in `shadows::fetch_point_shadow`,
// we must use the Level variant of textureSampleCompare to avoid undefined
// behavior due to some of the fragments in a quad (2x2 fragments) being
// processed not being sampled, and this messing with mip-mapping functionality.
// The shadow maps have no mipmaps so Level just samples from LOD 0.
fn sample_shadow_cubemap_hardware(light_local: vec3<f32>, depth: f32, light_id: u32) -> f32 {
#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT
return textureSampleCompare(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, light_local, depth);
#else
return textureSampleCompareLevel(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, light_local, i32(light_id), depth);
#endif
}
fn sample_shadow_cubemap_at_offset(
position: vec2<f32>,
coeff: f32,
x_basis: vec3<f32>,
y_basis: vec3<f32>,
light_local: vec3<f32>,
depth: f32,
light_id: u32,
) -> f32 {
return sample_shadow_cubemap_hardware(
light_local + position.x * x_basis + position.y * y_basis,
depth,
light_id
) * coeff;
}
// This more or less does what Castano13 does, but in 3D space. Castano13 is
// essentially an optimized 2D Gaussian filter that takes advantage of the
// bilinear filtering hardware to reduce the number of samples needed. This
// trick doesn't apply to cubemaps, so we manually apply a Gaussian filter over
// the standard 8xMSAA pattern instead.
fn sample_shadow_cubemap_gaussian(
light_local: vec3<f32>,
depth: f32,
scale: f32,
distance_to_light: f32,
light_id: u32,
) -> f32 {
// Create an orthonormal basis so we can apply a 2D sampling pattern to a
// cubemap.
var up = vec3(0.0, 1.0, 0.0);
if (dot(up, normalize(light_local)) > 0.99) {
up = vec3(1.0, 0.0, 0.0); // Avoid creating a degenerate basis.
}
let basis = orthonormalize(light_local, up) * scale * distance_to_light;
var sum: f32 = 0.0;
sum += sample_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[0], D3D_SAMPLE_POINT_COEFFS[0],
basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[1], D3D_SAMPLE_POINT_COEFFS[1],
basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[2], D3D_SAMPLE_POINT_COEFFS[2],
basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[3], D3D_SAMPLE_POINT_COEFFS[3],
basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[4], D3D_SAMPLE_POINT_COEFFS[4],
basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[5], D3D_SAMPLE_POINT_COEFFS[5],
basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[6], D3D_SAMPLE_POINT_COEFFS[6],
basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
D3D_SAMPLE_POINT_POSITIONS[7], D3D_SAMPLE_POINT_COEFFS[7],
basis[0], basis[1], light_local, depth, light_id);
return sum;
}
// This is a port of the Jimenez14 filter above to the 3D space. It jitters the
// points in the spiral pattern after first creating a 2D orthonormal basis
// along the principal light direction.
fn sample_shadow_cubemap_temporal(
light_local: vec3<f32>,
depth: f32,
scale: f32,
distance_to_light: f32,
light_id: u32,
) -> f32 {
// Create an orthonormal basis so we can apply a 2D sampling pattern to a
// cubemap.
var up = vec3(0.0, 1.0, 0.0);
if (dot(up, normalize(light_local)) > 0.99) {
up = vec3(1.0, 0.0, 0.0); // Avoid creating a degenerate basis.
}
let basis = orthonormalize(light_local, up) * scale * distance_to_light;
let rotation_matrix = random_rotation_matrix(vec2(1.0));
let sample_offset0 = rotation_matrix * utils::SPIRAL_OFFSET_0_ *
POINT_SHADOW_TEMPORAL_OFFSET_SCALE;
let sample_offset1 = rotation_matrix * utils::SPIRAL_OFFSET_1_ *
POINT_SHADOW_TEMPORAL_OFFSET_SCALE;
let sample_offset2 = rotation_matrix * utils::SPIRAL_OFFSET_2_ *
POINT_SHADOW_TEMPORAL_OFFSET_SCALE;
let sample_offset3 = rotation_matrix * utils::SPIRAL_OFFSET_3_ *
POINT_SHADOW_TEMPORAL_OFFSET_SCALE;
let sample_offset4 = rotation_matrix * utils::SPIRAL_OFFSET_4_ *
POINT_SHADOW_TEMPORAL_OFFSET_SCALE;
let sample_offset5 = rotation_matrix * utils::SPIRAL_OFFSET_5_ *
POINT_SHADOW_TEMPORAL_OFFSET_SCALE;
let sample_offset6 = rotation_matrix * utils::SPIRAL_OFFSET_6_ *
POINT_SHADOW_TEMPORAL_OFFSET_SCALE;
let sample_offset7 = rotation_matrix * utils::SPIRAL_OFFSET_7_ *
POINT_SHADOW_TEMPORAL_OFFSET_SCALE;
var sum: f32 = 0.0;
sum += sample_shadow_cubemap_at_offset(
sample_offset0, 0.125, basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
sample_offset1, 0.125, basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
sample_offset2, 0.125, basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
sample_offset3, 0.125, basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
sample_offset4, 0.125, basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
sample_offset5, 0.125, basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
sample_offset6, 0.125, basis[0], basis[1], light_local, depth, light_id);
sum += sample_shadow_cubemap_at_offset(
sample_offset7, 0.125, basis[0], basis[1], light_local, depth, light_id);
return sum;
}
fn sample_shadow_cubemap(
light_local: vec3<f32>,
distance_to_light: f32,
depth: f32,
light_id: u32,
) -> f32 {
#ifdef SHADOW_FILTER_METHOD_GAUSSIAN
return sample_shadow_cubemap_gaussian(
light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id);
#else ifdef SHADOW_FILTER_METHOD_TEMPORAL
return sample_shadow_cubemap_temporal(
light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id);
#else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2
return sample_shadow_cubemap_hardware(light_local, depth, light_id);
#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
}

View file

@ -4,7 +4,7 @@
mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE,
mesh_view_bindings as view_bindings,
utils::hsv2rgb,
shadow_sampling::sample_shadow_map
shadow_sampling::{SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_map}
}
const flip_z: vec3<f32> = vec3<f32>(1.0, 1.0, -1.0);
@ -39,16 +39,7 @@ fn fetch_point_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: v
// Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed coordinate space,
// so we have to flip the z-axis when sampling.
// NOTE: Due to the non-uniform control flow above, we must use the Level variant of
// textureSampleCompare to avoid undefined behavior due to some of the fragments in
// a quad (2x2 fragments) being processed not being sampled, and this messing with
// mip-mapping functionality. The shadow maps have no mipmaps so Level just samples
// from LOD 0.
#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT
return textureSampleCompare(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, frag_ls * flip_z, depth);
#else
return textureSampleCompareLevel(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, frag_ls * flip_z, i32(light_id), depth);
#endif
return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_id);
}
fn fetch_spot_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>) -> f32 {
@ -99,9 +90,12 @@ fn fetch_spot_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: ve
// 0.1 must match POINT_LIGHT_NEAR_Z
let depth = 0.1 / -projected_position.z;
// Number determined by trial and error that gave nice results.
let texel_size = 0.0134277345;
return sample_shadow_map(shadow_uv, depth, i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset, texel_size);
return sample_shadow_map(
shadow_uv,
depth,
i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset,
SPOT_SHADOW_TEXEL_SIZE
);
}
fn get_cascade_index(light_id: u32, view_z: f32) -> u32 {

View file

@ -51,3 +51,16 @@ fn inverse_affine3(affine: mat4x3<f32>) -> mat4x3<f32> {
let inv_matrix3 = inverse_mat3x3(matrix3);
return mat4x3<f32>(inv_matrix3[0], inv_matrix3[1], inv_matrix3[2], -(inv_matrix3 * affine[3]));
}
// Creates an orthonormal basis given a Z vector and an up vector (which becomes
// Y after orthonormalization).
//
// The results are equivalent to the Gram-Schmidt process [1].
//
// [1]: https://math.stackexchange.com/a/1849294
fn orthonormalize(z_unnormalized: vec3<f32>, up: vec3<f32>) -> mat3x3<f32> {
let z_basis = normalize(z_unnormalized);
let x_basis = normalize(cross(z_basis, up));
let y_basis = cross(z_basis, x_basis);
return mat3x3(x_basis, y_basis, z_basis);
}

View file

@ -70,6 +70,7 @@ fn setup(
shadows_enabled: true,
intensity: 10_000_000.,
range: 100.0,
shadow_depth_bias: 0.2,
..default()
},
transform: Transform::from_xyz(8.0, 16.0, 8.0),

View file

@ -252,14 +252,14 @@ fn cycle_filter_methods(
let filter_method_string;
*filter_method = match *filter_method {
ShadowFilteringMethod::Hardware2x2 => {
filter_method_string = "Castano13".to_string();
ShadowFilteringMethod::Castano13
filter_method_string = "Gaussian".to_string();
ShadowFilteringMethod::Gaussian
}
ShadowFilteringMethod::Castano13 => {
filter_method_string = "Jimenez14".to_string();
ShadowFilteringMethod::Jimenez14
ShadowFilteringMethod::Gaussian => {
filter_method_string = "Temporal".to_string();
ShadowFilteringMethod::Temporal
}
ShadowFilteringMethod::Jimenez14 => {
ShadowFilteringMethod::Temporal => {
filter_method_string = "Hardware2x2".to_string();
ShadowFilteringMethod::Hardware2x2
}