Skip to content

Commit

Permalink
PCF For DirectionalLight/SpotLight Shadows (bevyengine#8006)
Browse files Browse the repository at this point in the history
# Objective

- Improve antialiasing for non-point light shadow edges.
- Very partially addresses
bevyengine#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>
  • Loading branch information
9 people authored and Thomas Wilgenbus committed Oct 13, 2023
1 parent 42a595d commit aebacd1
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 40 deletions.
17 changes: 13 additions & 4 deletions crates/bevy_pbr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle};
use bevy_ecs::prelude::*;
use bevy_render::{
camera::CameraUpdateSystem, extract_resource::ExtractResourcePlugin, prelude::Color,
render_asset::prepare_assets, render_graph::RenderGraph, render_phase::sort_phase_system,
render_resource::Shader, texture::Image, view::VisibilitySystems, ExtractSchedule, Render,
RenderApp, RenderSet,
camera::CameraUpdateSystem, extract_component::ExtractComponentPlugin,
extract_resource::ExtractResourcePlugin, prelude::Color, render_asset::prepare_assets,
render_graph::RenderGraph, render_phase::sort_phase_system, render_resource::Shader,
texture::Image, view::VisibilitySystems, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::TransformSystem;
use environment_map::EnvironmentMapPlugin;
Expand All @@ -69,6 +69,7 @@ pub const UTILS_HANDLE: Handle<Shader> = Handle::weak_from_u128(1900548483293416
pub const CLUSTERED_FORWARD_HANDLE: Handle<Shader> = Handle::weak_from_u128(166852093121196815);
pub const PBR_LIGHTING_HANDLE: Handle<Shader> = Handle::weak_from_u128(14170772752254856967);
pub const SHADOWS_HANDLE: Handle<Shader> = Handle::weak_from_u128(11350275143789590502);
pub const SHADOW_SAMPLING_HANDLE: Handle<Shader> = Handle::weak_from_u128(3145627513789590502);
pub const PBR_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(4805239651767701046);
pub const PBR_PREPASS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(9407115064344201137);
pub const PBR_FUNCTIONS_HANDLE: Handle<Shader> = Handle::weak_from_u128(16550102964439850292);
Expand Down Expand Up @@ -124,6 +125,12 @@ impl Plugin for PbrPlugin {
"render/shadows.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
SHADOW_SAMPLING_HANDLE,
"render/shadow_sampling.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
PBR_FUNCTIONS_HANDLE,
Expand Down Expand Up @@ -168,6 +175,7 @@ impl Plugin for PbrPlugin {
.register_type::<PointLight>()
.register_type::<PointLightShadowMap>()
.register_type::<SpotLight>()
.register_type::<ShadowFilteringMethod>()
.init_resource::<AmbientLight>()
.init_resource::<GlobalVisiblePointLights>()
.init_resource::<DirectionalLightShadowMap>()
Expand All @@ -182,6 +190,7 @@ impl Plugin for PbrPlugin {
EnvironmentMapPlugin,
ExtractResourcePlugin::<AmbientLight>::default(),
FogPlugin,
ExtractComponentPlugin::<ShadowFilteringMethod>::default(),
))
.configure_sets(
PostUpdate,
Expand Down
31 changes: 31 additions & 0 deletions crates/bevy_pbr/src/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use bevy_reflect::prelude::*;
use bevy_render::{
camera::Camera,
color::Color,
extract_component::ExtractComponent,
extract_resource::ExtractResource,
prelude::Projection,
primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, HalfSpace, Sphere},
Expand Down Expand Up @@ -606,6 +607,36 @@ pub struct NotShadowCaster;
#[reflect(Component, Default)]
pub struct NotShadowReceiver;

/// Add this component to a [`Camera3d`](bevy_core_pipeline::core_3d::Camera3d)
/// to control how to anti-alias shadow edges.
///
/// 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 {
/// Hardware 2x2.
///
/// 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.
///
/// Good quality, good performance.
#[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.
///
/// Good quality when used with
/// [`TemporalAntiAliasSettings`](bevy_core_pipeline::experimental::taa::TemporalAntiAliasSettings)
/// and good performance.
Jimenez14,
}

#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum SimulationLightSystems {
AddClusters,
Expand Down
17 changes: 16 additions & 1 deletion crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
render, AlphaMode, DrawMesh, DrawPrepass, EnvironmentMapLight, MeshPipeline, MeshPipelineKey,
PrepassPipelinePlugin, PrepassPlugin, RenderMeshInstances, ScreenSpaceAmbientOcclusionSettings,
SetMeshBindGroup, SetMeshViewBindGroup, Shadow,
SetMeshBindGroup, SetMeshViewBindGroup, Shadow, ShadowFilteringMethod,
};
use bevy_app::{App, Plugin};
use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, AssetServer, Assets, Handle};
Expand Down Expand Up @@ -440,6 +440,7 @@ pub fn queue_material_meshes<M: Material>(
Option<&Tonemapping>,
Option<&DebandDither>,
Option<&EnvironmentMapLight>,
Option<&ShadowFilteringMethod>,
Option<&ScreenSpaceAmbientOcclusionSettings>,
Option<&NormalPrepass>,
Option<&TemporalAntiAliasSettings>,
Expand All @@ -456,6 +457,7 @@ pub fn queue_material_meshes<M: Material>(
tonemapping,
dither,
environment_map,
shadow_filter_method,
ssao,
normal_prepass,
taa_settings,
Expand All @@ -482,6 +484,19 @@ pub fn queue_material_meshes<M: Material>(
if environment_map_loaded {
view_key |= MeshPipelineKey::ENVIRONMENT_MAP;
}

match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) {
ShadowFilteringMethod::Hardware2x2 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2;
}
ShadowFilteringMethod::Castano13 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_CASTANO_13;
}
ShadowFilteringMethod::Jimenez14 => {
view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_JIMENEZ_14;
}
}

if !view.hdr {
if let Some(tonemapping) = tonemapping {
view_key |= MeshPipelineKey::TONEMAP_IN_SHADER;
Expand Down
35 changes: 29 additions & 6 deletions crates/bevy_pbr/src/render/mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,24 +659,35 @@ bitflags::bitflags! {
const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS;
const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS;
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;
}
}

impl MeshPipelineKey {
const MSAA_MASK_BITS: u32 = 0b111;
const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones();

const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111;
const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 =
Self::MSAA_SHIFT_BITS - Self::PRIMITIVE_TOPOLOGY_MASK_BITS.count_ones();

const BLEND_MASK_BITS: u32 = 0b11;
const BLEND_SHIFT_BITS: u32 =
Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS - Self::BLEND_MASK_BITS.count_ones();

const TONEMAP_METHOD_MASK_BITS: u32 = 0b111;
const TONEMAP_METHOD_SHIFT_BITS: u32 =
Self::BLEND_SHIFT_BITS - Self::TONEMAP_METHOD_MASK_BITS.count_ones();

const SHADOW_FILTER_METHOD_MASK_BITS: u32 = 0b11;
const SHADOW_FILTER_METHOD_SHIFT_BITS: u32 =
Self::TONEMAP_METHOD_SHIFT_BITS - Self::SHADOW_FILTER_METHOD_MASK_BITS.count_ones();

pub fn from_msaa_samples(msaa_samples: u32) -> Self {
let msaa_bits =
(msaa_samples.trailing_zeros() & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS;
Expand Down Expand Up @@ -904,6 +915,16 @@ impl SpecializedMeshPipeline for MeshPipeline {
shader_defs.push("TAA".into());
}

let shadow_filter_method =
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());
}

let format = if key.contains(MeshPipelineKey::HDR) {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
Expand Down Expand Up @@ -1069,10 +1090,12 @@ pub fn prepare_mesh_view_bind_groups(
Option<&EnvironmentMapLight>,
&Tonemapping,
)>,
images: Res<RenderAssets<Image>>,
mut fallback_images: FallbackImagesMsaa,
mut fallback_depths: FallbackImagesDepth,
fallback_cubemap: Res<FallbackImageCubemap>,
(images, mut fallback_images, mut fallback_depths, fallback_cubemap): (
Res<RenderAssets<Image>>,
FallbackImagesMsaa,
FallbackImagesDepth,
Res<FallbackImageCubemap>,
),
msaa: Res<Msaa>,
globals_buffer: Res<GlobalsBuffer>,
tonemapping_luts: Res<TonemappingLuts>,
Expand Down
132 changes: 132 additions & 0 deletions crates/bevy_pbr/src/render/shadow_sampling.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#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
}
Loading

0 comments on commit aebacd1

Please sign in to comment.