From 00ef49da90bd44f580b90ed243db6106d8d71a9e Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sat, 18 May 2024 17:55:13 -0700 Subject: [PATCH] Implement percentage-closer soft shadows (PCSS). [*Percentage-closer soft shadows*] are a technique from 2004 that allow shadows to become blurrier farther from the objects that cast them. It works by introducing a *blocker search* step that runs before the normal shadow map sampling. The blocker search step detects the difference between the depth of the fragment being rasterized and the depth of the nearby samples in the depth buffer. Larger depth differences result in a larger penumbra and therefore a blurrier shadow. To enable PCSS, fill in the `soft_shadow_size` value in `DirectionalLight` or `PointLight`. This shadow size value represents the size of the light and should be tuned as appropriate for your scene. Higher values result in a wider penumbra (i.e. blurrier shadows). When using PCSS, temporal shadow maps (`ShadowFilteringMethod::Temporal`) are recommended. If you don't use `ShadowFilteringMethod::Temporal` and instead use `ShadowFilteringMethod::Gaussian`, Bevy will use the same technique as `Temporal`, but the result won't vary over time. This produces a rather noisy result. Doing better would likely require downsampling the shadow map, which would be complex and slower (and would require PR #13003 to land first). A new example, `pcss`, has been added. It demonstrates the percentage-closer soft shadow technique with directional lights, point lights, non-temporal operation, and temporal operation. The assets are my original work. Fixes #3631. [*Percentage-closer soft shadows*]: https://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf --- Cargo.toml | 11 + assets/models/PalmTree/PalmTree.bin | Bin 0 -> 60812 bytes assets/models/PalmTree/PalmTree.gltf | 931 ++++++++++++++++++ .../src/tonemapping/tonemapping_shared.wgsl | 4 +- .../bevy_pbr/src/light/directional_light.rs | 44 +- crates/bevy_pbr/src/render/light.rs | 50 +- .../bevy_pbr/src/render/mesh_view_bindings.rs | 94 +- .../src/render/mesh_view_bindings.wgsl | 72 +- .../bevy_pbr/src/render/mesh_view_types.wgsl | 1 + .../bevy_pbr/src/render/shadow_sampling.wgsl | 126 ++- crates/bevy_pbr/src/render/shadows.wgsl | 21 +- examples/3d/pcss.rs | 262 +++++ 12 files changed, 1499 insertions(+), 117 deletions(-) create mode 100644 assets/models/PalmTree/PalmTree.bin create mode 100644 assets/models/PalmTree/PalmTree.gltf create mode 100644 examples/3d/pcss.rs diff --git a/Cargo.toml b/Cargo.toml index f5f95239f8dc42..ba35635b35e556 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3059,6 +3059,17 @@ description = "Demonstrates volumetric fog and lighting" category = "3D Rendering" wasm = true +[[example]] +name = "pcss" +path = "examples/3d/pcss.rs" +doc-scrape-examples = true + +[package.metadata.example.pcss] +name = "Percentage-closer soft shadows" +description = "Demonstrates percentage-closer soft shadows (PCSS)" +category = "3D Rendering" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/models/PalmTree/PalmTree.bin b/assets/models/PalmTree/PalmTree.bin new file mode 100644 index 0000000000000000000000000000000000000000..89cb189b23ae0f4e4ea1f8781c888149110f6444 GIT binary patch literal 60812 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 Hk@; @group(0) @binding(4) var dt_lut_sampler: sampler; #else - @group(0) @binding(19) var dt_lut_texture: texture_3d; - @group(0) @binding(20) var dt_lut_sampler: sampler; + @group(0) @binding(21) var dt_lut_texture: texture_3d; + @group(0) @binding(22) var dt_lut_sampler: sampler; #endif // Half the size of the crossfade region between shadows and midtones and diff --git a/crates/bevy_pbr/src/light/directional_light.rs b/crates/bevy_pbr/src/light/directional_light.rs index 9c74971ca15a56..1770641991e832 100644 --- a/crates/bevy_pbr/src/light/directional_light.rs +++ b/crates/bevy_pbr/src/light/directional_light.rs @@ -50,7 +50,11 @@ use super::*; #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default)] pub struct DirectionalLight { + /// The color of the light. + /// + /// By default, this is white. pub color: Color, + /// Illuminance in lux (lumens per square meter), representing the amount of /// light projected onto surfaces by this light source. Lux is used here /// instead of lumens because a directional light illuminates all surfaces @@ -58,10 +62,45 @@ pub struct DirectionalLight { /// can only be specified for light sources which emit light from a specific /// area. pub illuminance: f32, + + /// Whether this light casts shadows. + /// + /// Note that shadows are rather expensive and become more so with every + /// light that casts them. In general, it's best to aggressively limit the + /// number of lights with shadows enabled to one or two at most. pub shadows_enabled: bool, + + /// Whether soft shadows are enabled, and if so, the size of the light. + /// + /// Soft shadows, also known as *percentage-closer soft shadows* or PCSS, + /// cause shadows to become blurrier (i.e. their penumbra increases in + /// radius) as they extend away from objects. The blurriness of the shadow + /// depends on the size of the light; larger lights result in larger + /// penumbras and therefore blurrier shadows. + /// + /// Currently, soft shadows are rather noisy if not using the temporal mode. + /// If you enable soft shadows, consider choosing + /// [`ShadowFilteringMethod::Temporal`] and enabling temporal antialiasing + /// (TAA) to smooth the noise out over time. + /// + /// Note that soft shadows are significantly more expensive to render than + /// hard shadows. + pub soft_shadow_size: Option, + + /// A value that adjusts the tradeoff between self-shadowing artifacts and + /// promixity of shadows to their casters. + /// + /// This value frequently must be tuned to the specific scene; this is + /// normal and a well-known part of the shadow mapping workflow. If set too + /// low, unsightly shadow patterns appear on objects not in shadow as + /// objects incorrectly cast shadows on themselves, known as *shadow acne*. + /// If set too high, shadows detach from the objects casting them and seem + /// to "fly" off the objects, known as *Peter Panning*. pub shadow_depth_bias: f32, - /// A bias applied along the direction of the fragment's surface normal. It is scaled to the - /// shadow map's texel size so that it is automatically adjusted to the orthographic projection. + + /// A bias applied along the direction of the fragment's surface normal. It + /// is scaled to the shadow map's texel size so that it is automatically + /// adjusted to the orthographic projection. pub shadow_normal_bias: f32, } @@ -71,6 +110,7 @@ impl Default for DirectionalLight { color: Color::WHITE, illuminance: light_consts::lux::AMBIENT_DAYLIGHT, shadows_enabled: false, + soft_shadow_size: None, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index b425a251aad491..a361c1ea27c747 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -20,6 +20,7 @@ use bevy_render::{ Extract, }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; +use bevy_utils::prelude::default; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use bevy_utils::tracing::{error, warn}; @@ -48,6 +49,7 @@ pub struct ExtractedDirectionalLight { pub transform: GlobalTransform, pub shadows_enabled: bool, pub volumetric: bool, + pub soft_shadow_size: Option, pub shadow_depth_bias: f32, pub shadow_normal_bias: f32, pub cascade_shadow_config: CascadeShadowConfig, @@ -170,6 +172,7 @@ pub struct GpuDirectionalLight { color: Vec4, dir_to_light: Vec3, flags: u32, + soft_shadow_size: f32, shadow_depth_bias: f32, shadow_normal_bias: f32, num_cascades: u32, @@ -228,8 +231,10 @@ pub const MAX_CASCADES_PER_LIGHT: usize = 1; #[derive(Resource, Clone)] pub struct ShadowSamplers { - pub point_light_sampler: Sampler, - pub directional_light_sampler: Sampler, + pub point_light_comparison_sampler: Sampler, + pub point_light_linear_sampler: Sampler, + pub directional_light_comparison_sampler: Sampler, + pub directional_light_linear_sampler: Sampler, } // TODO: this pattern for initializing the shaders / pipeline isn't ideal. this should be handled by the asset system @@ -237,27 +242,30 @@ impl FromWorld for ShadowSamplers { fn from_world(world: &mut World) -> Self { let render_device = world.resource::(); + let base_sampler_descriptor = SamplerDescriptor { + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + address_mode_w: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Nearest, + ..default() + }; + ShadowSamplers { - point_light_sampler: render_device.create_sampler(&SamplerDescriptor { - address_mode_u: AddressMode::ClampToEdge, - address_mode_v: AddressMode::ClampToEdge, - address_mode_w: AddressMode::ClampToEdge, - mag_filter: FilterMode::Linear, - min_filter: FilterMode::Linear, - mipmap_filter: FilterMode::Nearest, - compare: Some(CompareFunction::GreaterEqual), - ..Default::default() - }), - directional_light_sampler: render_device.create_sampler(&SamplerDescriptor { - address_mode_u: AddressMode::ClampToEdge, - address_mode_v: AddressMode::ClampToEdge, - address_mode_w: AddressMode::ClampToEdge, - mag_filter: FilterMode::Linear, - min_filter: FilterMode::Linear, - mipmap_filter: FilterMode::Nearest, + point_light_comparison_sampler: render_device.create_sampler(&SamplerDescriptor { compare: Some(CompareFunction::GreaterEqual), - ..Default::default() + ..base_sampler_descriptor }), + point_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor), + directional_light_comparison_sampler: render_device.create_sampler( + &SamplerDescriptor { + compare: Some(CompareFunction::GreaterEqual), + ..base_sampler_descriptor + }, + ), + directional_light_linear_sampler: render_device + .create_sampler(&base_sampler_descriptor), } } } @@ -487,6 +495,7 @@ pub fn extract_lights( illuminance: directional_light.illuminance, transform: *transform, volumetric: volumetric_light.is_some(), + soft_shadow_size: directional_light.soft_shadow_size, shadows_enabled: directional_light.shadows_enabled, shadow_depth_bias: directional_light.shadow_depth_bias, // The factor of SQRT_2 is for the worst-case diagonal offset @@ -942,6 +951,7 @@ pub fn prepare_lights( // direction is negated to be ready for N.L dir_to_light: light.transform.back().into(), flags: flags.bits(), + soft_shadow_size: light.soft_shadow_size.unwrap_or_default(), shadow_depth_bias: light.shadow_depth_bias, shadow_normal_bias: light.shadow_normal_bias, num_cascades: num_cascades as u32, diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 89c5557fbe088d..dad06eb242bbe7 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -212,11 +212,13 @@ fn layout_entries( ))] texture_cube(TextureSampleType::Depth), ), - // Point Shadow Texture Array Sampler + // Point Shadow Texture Array Comparison Sampler (3, sampler(SamplerBindingType::Comparison)), + // Point Shadow Texture Array Linear Sampler + (4, sampler(SamplerBindingType::Filtering)), // Directional Shadow Texture Array ( - 4, + 5, #[cfg(any( not(feature = "webgl"), not(target_arch = "wasm32"), @@ -226,11 +228,13 @@ fn layout_entries( #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] texture_2d(TextureSampleType::Depth), ), - // Directional Shadow Texture Array Sampler - (5, sampler(SamplerBindingType::Comparison)), + // Directional Shadow Texture Array Comparison Sampler + (6, sampler(SamplerBindingType::Comparison)), + // Directional Shadow Texture Array Linear Sampler + (7, sampler(SamplerBindingType::Filtering)), // PointLights ( - 6, + 8, buffer_layout( clustered_forward_buffer_binding_type, false, @@ -241,7 +245,7 @@ fn layout_entries( ), // ClusteredLightIndexLists ( - 7, + 9, buffer_layout( clustered_forward_buffer_binding_type, false, @@ -252,7 +256,7 @@ fn layout_entries( ), // ClusterOffsetsAndCounts ( - 8, + 10, buffer_layout( clustered_forward_buffer_binding_type, false, @@ -263,16 +267,16 @@ fn layout_entries( ), // Globals ( - 9, + 11, uniform_buffer::(false).visibility(ShaderStages::VERTEX_FRAGMENT), ), // Fog - (10, uniform_buffer::(true)), + (12, uniform_buffer::(true)), // Light probes - (11, uniform_buffer::(true)), + (13, uniform_buffer::(true)), // Visibility ranges ( - 12, + 14, buffer_layout( visibility_ranges_buffer_binding_type, false, @@ -282,7 +286,7 @@ fn layout_entries( ), // Screen space ambient occlusion texture ( - 13, + 15, texture_2d(TextureSampleType::Float { filterable: false }), ), ), @@ -291,9 +295,9 @@ fn layout_entries( // EnvironmentMapLight let environment_map_entries = environment_map::get_bind_group_layout_entries(render_device); entries = entries.extend_with_indices(( - (14, environment_map_entries[0]), - (15, environment_map_entries[1]), - (16, environment_map_entries[2]), + (16, environment_map_entries[0]), + (17, environment_map_entries[1]), + (18, environment_map_entries[2]), )); // Irradiance volumes @@ -301,16 +305,16 @@ fn layout_entries( let irradiance_volume_entries = irradiance_volume::get_bind_group_layout_entries(render_device); entries = entries.extend_with_indices(( - (17, irradiance_volume_entries[0]), - (18, irradiance_volume_entries[1]), + (19, irradiance_volume_entries[0]), + (20, irradiance_volume_entries[1]), )); } // Tonemapping let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); entries = entries.extend_with_indices(( - (19, tonemapping_lut_entries[0]), - (20, tonemapping_lut_entries[1]), + (21, tonemapping_lut_entries[0]), + (22, tonemapping_lut_entries[1]), )); // Prepass @@ -320,7 +324,7 @@ fn layout_entries( { for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key) .iter() - .zip([21, 22, 23, 24]) + .zip([23, 24, 25, 26]) { if let Some(entry) = entry { entries = entries.extend_with_indices(((binding as u32, *entry),)); @@ -331,10 +335,10 @@ fn layout_entries( // View Transmission Texture entries = entries.extend_with_indices(( ( - 25, + 27, texture_2d(TextureSampleType::Float { filterable: true }), ), - (26, sampler(SamplerBindingType::Filtering)), + (28, sampler(SamplerBindingType::Filtering)), )); entries.to_vec() @@ -515,17 +519,19 @@ pub fn prepare_mesh_view_bind_groups( (0, view_binding.clone()), (1, light_binding.clone()), (2, &shadow_bindings.point_light_depth_texture_view), - (3, &shadow_samplers.point_light_sampler), - (4, &shadow_bindings.directional_light_depth_texture_view), - (5, &shadow_samplers.directional_light_sampler), - (6, point_light_binding.clone()), - (7, cluster_bindings.light_index_lists_binding().unwrap()), - (8, cluster_bindings.offsets_and_counts_binding().unwrap()), - (9, globals.clone()), - (10, fog_binding.clone()), - (11, light_probes_binding.clone()), - (12, visibility_ranges_buffer.as_entire_binding()), - (13, ssao_view), + (3, &shadow_samplers.point_light_comparison_sampler), + (4, &shadow_samplers.point_light_linear_sampler), + (5, &shadow_bindings.directional_light_depth_texture_view), + (6, &shadow_samplers.directional_light_comparison_sampler), + (7, &shadow_samplers.directional_light_linear_sampler), + (8, point_light_binding.clone()), + (9, cluster_bindings.light_index_lists_binding().unwrap()), + (10, cluster_bindings.offsets_and_counts_binding().unwrap()), + (11, globals.clone()), + (12, fog_binding.clone()), + (13, light_probes_binding.clone()), + (14, visibility_ranges_buffer.as_entire_binding()), + (15, ssao_view), )); let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get( @@ -542,9 +548,9 @@ pub fn prepare_mesh_view_bind_groups( sampler, } => { entries = entries.extend_with_indices(( - (14, diffuse_texture_view), - (15, specular_texture_view), - (16, sampler), + (16, diffuse_texture_view), + (17, specular_texture_view), + (18, sampler), )); } RenderViewEnvironmentMapBindGroupEntries::Multiple { @@ -553,9 +559,9 @@ pub fn prepare_mesh_view_bind_groups( sampler, } => { entries = entries.extend_with_indices(( - (14, diffuse_texture_views.as_slice()), - (15, specular_texture_views.as_slice()), - (16, sampler), + (16, diffuse_texture_views.as_slice()), + (17, specular_texture_views.as_slice()), + (18, sampler), )); } } @@ -576,21 +582,21 @@ pub fn prepare_mesh_view_bind_groups( texture_view, sampler, }) => { - entries = entries.extend_with_indices(((17, texture_view), (18, sampler))); + entries = entries.extend_with_indices(((19, texture_view), (20, sampler))); } Some(RenderViewIrradianceVolumeBindGroupEntries::Multiple { ref texture_views, sampler, }) => { entries = entries - .extend_with_indices(((17, texture_views.as_slice()), (18, sampler))); + .extend_with_indices(((19, texture_views.as_slice()), (20, sampler))); } None => {} } let lut_bindings = get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); - entries = entries.extend_with_indices(((19, lut_bindings.0), (20, lut_bindings.1))); + entries = entries.extend_with_indices(((21, lut_bindings.0), (22, lut_bindings.1))); // When using WebGL, we can't have a depth texture with multisampling let prepass_bindings; @@ -600,7 +606,7 @@ pub fn prepare_mesh_view_bind_groups( for (binding, index) in prepass_bindings .iter() .map(Option::as_ref) - .zip([21, 22, 23, 24]) + .zip([23, 24, 25, 26]) .flat_map(|(b, i)| b.map(|b| (b, i))) { entries = entries.extend_with_indices(((index, binding),)); @@ -616,7 +622,7 @@ pub fn prepare_mesh_view_bind_groups( .unwrap_or(&fallback_image_zero.sampler); entries = - entries.extend_with_indices(((25, transmission_view), (26, transmission_sampler))); + entries.extend_with_indices(((27, transmission_view), (28, transmission_sampler))); commands.entity(entity).insert(MeshViewBindGroup { value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries), diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index b8e74c60b8b437..b6fabee889714d 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -13,87 +13,89 @@ #else @group(0) @binding(2) var point_shadow_textures: texture_depth_cube_array; #endif -@group(0) @binding(3) var point_shadow_textures_sampler: sampler_comparison; +@group(0) @binding(3) var point_shadow_textures_comparison_sampler: sampler_comparison; +@group(0) @binding(4) var point_shadow_textures_linear_sampler: sampler; #ifdef NO_ARRAY_TEXTURES_SUPPORT -@group(0) @binding(4) var directional_shadow_textures: texture_depth_2d; +@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d; #else -@group(0) @binding(4) var directional_shadow_textures: texture_depth_2d_array; +@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d_array; #endif -@group(0) @binding(5) var directional_shadow_textures_sampler: sampler_comparison; +@group(0) @binding(6) var directional_shadow_textures_comparison_sampler: sampler_comparison; +@group(0) @binding(7) var directional_shadow_textures_linear_sampler: sampler; #if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 -@group(0) @binding(6) var point_lights: types::PointLights; -@group(0) @binding(7) var cluster_light_index_lists: types::ClusterLightIndexLists; -@group(0) @binding(8) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; +@group(0) @binding(8) var point_lights: types::PointLights; +@group(0) @binding(9) var cluster_light_index_lists: types::ClusterLightIndexLists; +@group(0) @binding(10) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; #else -@group(0) @binding(6) var point_lights: types::PointLights; -@group(0) @binding(7) var cluster_light_index_lists: types::ClusterLightIndexLists; -@group(0) @binding(8) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; +@group(0) @binding(8) var point_lights: types::PointLights; +@group(0) @binding(9) var cluster_light_index_lists: types::ClusterLightIndexLists; +@group(0) @binding(10) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; #endif -@group(0) @binding(9) var globals: Globals; -@group(0) @binding(10) var fog: types::Fog; -@group(0) @binding(11) var light_probes: types::LightProbes; +@group(0) @binding(11) var globals: Globals; +@group(0) @binding(12) var fog: types::Fog; +@group(0) @binding(13) var light_probes: types::LightProbes; const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u; #if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 -@group(0) @binding(12) var visibility_ranges: array>; +@group(0) @binding(14) var visibility_ranges: array>; #else -@group(0) @binding(12) var visibility_ranges: array, VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE>; +@group(0) @binding(14) var visibility_ranges: array, VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE>; #endif -@group(0) @binding(13) var screen_space_ambient_occlusion_texture: texture_2d; +@group(0) @binding(15) var screen_space_ambient_occlusion_texture: texture_2d; #ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY -@group(0) @binding(14) var diffuse_environment_maps: binding_array, 8u>; -@group(0) @binding(15) var specular_environment_maps: binding_array, 8u>; +@group(0) @binding(16) var diffuse_environment_maps: binding_array, 8u>; +@group(0) @binding(17) var specular_environment_maps: binding_array, 8u>; #else -@group(0) @binding(14) var diffuse_environment_map: texture_cube; -@group(0) @binding(15) var specular_environment_map: texture_cube; +@group(0) @binding(16) var diffuse_environment_map: texture_cube; +@group(0) @binding(17) var specular_environment_map: texture_cube; #endif -@group(0) @binding(16) var environment_map_sampler: sampler; +@group(0) @binding(18) var environment_map_sampler: sampler; #ifdef IRRADIANCE_VOLUMES_ARE_USABLE #ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY -@group(0) @binding(17) var irradiance_volumes: binding_array, 8u>; +@group(0) @binding(19) var irradiance_volumes: binding_array, 8u>; #else -@group(0) @binding(17) var irradiance_volume: texture_3d; +@group(0) @binding(19) var irradiance_volume: texture_3d; #endif -@group(0) @binding(18) var irradiance_volume_sampler: sampler; +@group(0) @binding(20) var irradiance_volume_sampler: sampler; #endif // NB: If you change these, make sure to update `tonemapping_shared.wgsl` too. -@group(0) @binding(19) var dt_lut_texture: texture_3d; -@group(0) @binding(20) var dt_lut_sampler: sampler; +@group(0) @binding(21) var dt_lut_texture: texture_3d; +@group(0) @binding(22) var dt_lut_sampler: sampler; #ifdef MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(21) var depth_prepass_texture: texture_depth_multisampled_2d; +@group(0) @binding(23) var depth_prepass_texture: texture_depth_multisampled_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(22) var normal_prepass_texture: texture_multisampled_2d; +@group(0) @binding(24) var normal_prepass_texture: texture_multisampled_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(23) var motion_vector_prepass_texture: texture_multisampled_2d; +@group(0) @binding(25) var motion_vector_prepass_texture: texture_multisampled_2d; #endif // MOTION_VECTOR_PREPASS #else // MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(21) var depth_prepass_texture: texture_depth_2d; +@group(0) @binding(23) var depth_prepass_texture: texture_depth_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(22) var normal_prepass_texture: texture_2d; +@group(0) @binding(24) var normal_prepass_texture: texture_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(23) var motion_vector_prepass_texture: texture_2d; +@group(0) @binding(25) var motion_vector_prepass_texture: texture_2d; #endif // MOTION_VECTOR_PREPASS #endif // MULTISAMPLED #ifdef DEFERRED_PREPASS -@group(0) @binding(24) var deferred_prepass_texture: texture_2d; +@group(0) @binding(26) var deferred_prepass_texture: texture_2d; #endif // DEFERRED_PREPASS -@group(0) @binding(25) var view_transmission_texture: texture_2d; -@group(0) @binding(26) var view_transmission_sampler: sampler; +@group(0) @binding(27) var view_transmission_texture: texture_2d; +@group(0) @binding(28) var view_transmission_sampler: sampler; diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index f517daec4d6b47..6d4f587bf4b7ba 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -28,6 +28,7 @@ struct DirectionalLight { direction_to_light: vec3, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, + soft_shadow_size: f32, shadow_depth_bias: f32, shadow_normal_bias: f32, num_cascades: u32, diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index ec155cf3fcb77a..20ad25422a30ab 100644 --- a/crates/bevy_pbr/src/render/shadow_sampling.wgsl +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -12,14 +12,14 @@ fn sample_shadow_map_hardware(light_local: vec2, depth: f32, array_index: i #ifdef NO_ARRAY_TEXTURES_SUPPORT return textureSampleCompare( view_bindings::directional_shadow_textures, - view_bindings::directional_shadow_textures_sampler, + view_bindings::directional_shadow_textures_comparison_sampler, light_local, depth, ); #else return textureSampleCompareLevel( view_bindings::directional_shadow_textures, - view_bindings::directional_shadow_textures_sampler, + view_bindings::directional_shadow_textures_comparison_sampler, light_local, array_index, depth, @@ -27,6 +27,28 @@ fn sample_shadow_map_hardware(light_local: vec2, depth: f32, array_index: i #endif } +fn search_for_blockers_hardware(light_local: vec2, depth: f32, array_index: i32) -> vec2 { +#ifdef NO_ARRAY_TEXTURES_SUPPORT + let sampled_depth = textureSample( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_linear_sampler, + light_local, + ); +#else + let sampled_depth = textureSample( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_linear_sampler, + light_local, + array_index, + ); +#endif + + if (sampled_depth >= depth) { + return vec2(sampled_depth, 1.0); + } + return vec2(0.0); +} + // 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; @@ -113,9 +135,9 @@ fn map(min1: f32, max1: f32, min2: f32, max2: f32, value: f32) -> f32 { // 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) -> mat2x2 { +fn random_rotation_matrix(scale: vec2, temporal: bool) -> mat2x2 { let random_angle = 2.0 * PI * interleaved_gradient_noise( - scale, view_bindings::globals.frame_count); + scale, select(1u, view_bindings::globals.frame_count, temporal)); let m = vec2(sin(random_angle), cos(random_angle)); return mat2x2( m.y, -m.x, @@ -123,13 +145,25 @@ fn random_rotation_matrix(scale: vec2) -> mat2x2 { ); } -fn sample_shadow_map_jimenez_fourteen(light_local: vec2, depth: f32, array_index: i32, texel_size: f32) -> f32 { +fn calculate_uv_offset_scale_jimenez_fourteen(texel_size: f32, blur_size: f32) -> vec2 { let shadow_map_size = vec2(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); + return f * blur_size / (texel_size * shadow_map_size); +} + +fn sample_shadow_map_jimenez_fourteen( + light_local: vec2, + depth: f32, + array_index: i32, + texel_size: f32, + blur_size: f32, + temporal: bool, +) -> f32 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + let rotation_matrix = random_rotation_matrix(light_local * shadow_map_size, temporal); + let uv_offset_scale = calculate_uv_offset_scale_jimenez_fourteen(texel_size, blur_size); // https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) let sample_offset0 = (rotation_matrix * utils::SPIRAL_OFFSET_0_) * uv_offset_scale; @@ -153,11 +187,47 @@ fn sample_shadow_map_jimenez_fourteen(light_local: vec2, depth: f32, array_ return sum / 8.0; } +fn search_for_blockers( + 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 sample_offset0 = D3D_SAMPLE_POINT_POSITIONS[0] * uv_offset_scale; + let sample_offset1 = D3D_SAMPLE_POINT_POSITIONS[1] * uv_offset_scale; + let sample_offset2 = D3D_SAMPLE_POINT_POSITIONS[2] * uv_offset_scale; + let sample_offset3 = D3D_SAMPLE_POINT_POSITIONS[3] * uv_offset_scale; + let sample_offset4 = D3D_SAMPLE_POINT_POSITIONS[4] * uv_offset_scale; + let sample_offset5 = D3D_SAMPLE_POINT_POSITIONS[5] * uv_offset_scale; + let sample_offset6 = D3D_SAMPLE_POINT_POSITIONS[6] * uv_offset_scale; + let sample_offset7 = D3D_SAMPLE_POINT_POSITIONS[7] * uv_offset_scale; + + var sum = vec2(0.0); + sum += search_for_blockers_hardware(light_local + sample_offset0, depth, array_index); + sum += search_for_blockers_hardware(light_local + sample_offset1, depth, array_index); + sum += search_for_blockers_hardware(light_local + sample_offset2, depth, array_index); + sum += search_for_blockers_hardware(light_local + sample_offset3, depth, array_index); + sum += search_for_blockers_hardware(light_local + sample_offset4, depth, array_index); + sum += search_for_blockers_hardware(light_local + sample_offset5, depth, array_index); + sum += search_for_blockers_hardware(light_local + sample_offset6, depth, array_index); + sum += search_for_blockers_hardware(light_local + sample_offset7, depth, array_index); + + if (sum.y == 0.0) { + return 0.0; + } + return sum.x / sum.y; +} + fn sample_shadow_map(light_local: vec2, depth: f32, array_index: i32, texel_size: f32) -> f32 { #ifdef SHADOW_FILTER_METHOD_GAUSSIAN return sample_shadow_map_castano_thirteen(light_local, depth, array_index); #else ifdef SHADOW_FILTER_METHOD_TEMPORAL - return sample_shadow_map_jimenez_fourteen(light_local, depth, array_index, texel_size); + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, 1.0, true); #else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2 return sample_shadow_map_hardware(light_local, depth, array_index); #else @@ -169,6 +239,29 @@ fn sample_shadow_map(light_local: vec2, depth: f32, array_index: i32, texel #endif } +// Samples the shadow map when percentage-closer soft shadows are being used. +// +// A good overview of the technique: +// +fn sample_shadow_map_pcss( + light_local: vec2, + depth: f32, + array_index: i32, + texel_size: f32, + light_size: f32, +) -> f32 { + let z_blocker = search_for_blockers(light_local, depth, array_index, texel_size, light_size); + // 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); +#ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, blur_size, true); +#else // SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, blur_size, false); +#endif // SHADOW_FILTER_METHOD_TEMPORAL +} + // 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 @@ -176,9 +269,20 @@ fn sample_shadow_map(light_local: vec2, depth: f32, array_index: i32, texel // The shadow maps have no mipmaps so Level just samples from LOD 0. fn sample_shadow_cubemap_hardware(light_local: vec3, 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); + return textureSampleCompare( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_comparison_sampler, + light_local, + depth + ); #else - return textureSampleCompareLevel(view_bindings::point_shadow_textures, view_bindings::point_shadow_textures_sampler, light_local, i32(light_id), depth); + return textureSampleCompareLevel( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_comparison_sampler, + light_local, + i32(light_id), + depth + ); #endif } @@ -264,7 +368,7 @@ fn sample_shadow_cubemap_temporal( } let basis = orthonormalize(light_local, up) * scale * distance_to_light; - let rotation_matrix = random_rotation_matrix(vec2(1.0)); + let rotation_matrix = random_rotation_matrix(vec2(1.0), true); let sample_offset0 = rotation_matrix * utils::SPIRAL_OFFSET_0_ * POINT_SHADOW_TEMPORAL_OFFSET_SCALE; diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index 21b25f7f3aebf5..4d667436a92ac5 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -3,7 +3,9 @@ #import bevy_pbr::{ mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, mesh_view_bindings as view_bindings, - shadow_sampling::{SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_map} + shadow_sampling::{ + SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_map, sample_shadow_map_pcss, + } } #import bevy_render::{ @@ -146,7 +148,12 @@ fn world_to_directional_light_local( return vec4(light_local, depth, 1.0); } -fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position: vec4, surface_normal: vec3) -> f32 { +fn sample_directional_cascade( + light_id: u32, + cascade_index: u32, + frag_position: vec4, + surface_normal: vec3, +) -> f32 { let light = &view_bindings::lights.directional_lights[light_id]; let cascade = &(*light).cascades[cascade_index]; @@ -161,7 +168,15 @@ fn sample_directional_cascade(light_id: u32, cascade_index: u32, frag_position: } let array_index = i32((*light).depth_texture_base_index + cascade_index); - return sample_shadow_map(light_local.xy, light_local.z, array_index, (*cascade).texel_size); + let texel_size = (*cascade).texel_size; + + // If soft shadows are enabled, use the PCSS path. + if ((*light).soft_shadow_size > 0.0) { + return sample_shadow_map_pcss( + light_local.xy, light_local.z, array_index, texel_size, (*light).soft_shadow_size); + } + + return sample_shadow_map(light_local.xy, light_local.z, array_index, texel_size); } fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3, view_z: f32) -> f32 { diff --git a/examples/3d/pcss.rs b/examples/3d/pcss.rs new file mode 100644 index 00000000000000..c583e283442297 --- /dev/null +++ b/examples/3d/pcss.rs @@ -0,0 +1,262 @@ +//! Demonstrates percentage-closer soft shadows (PCSS). + +use bevy::{math::vec3, pbr::ShadowFilteringMethod, prelude::*}; + +/// The current application settings (light type, shadow filter, and the status +/// of PCSS). +#[derive(Resource)] +struct AppStatus { + /// The type of light presently in the scene: either directional or point. + light_type: LightType, + /// The type of shadow filter: Gaussian or temporal. + shadow_filter: ShadowFilter, + /// Whether soft shadows are enabled. + pcss_enabled: bool, +} + +/// The type of light presently in the scene: either directional or point. +#[derive(Clone, Copy, Default)] +enum LightType { + /// A directional light, with a cascaded shadow map. + #[default] + Directional, + /// A point light, with a cube shadow map. + Point, +} + +/// The type of shadow filter. +/// +/// Generally, `Gaussian` is preferred when temporal antialiasing isn't in use, +/// while `Temporal` is preferred when TAA is in use. +#[derive(Clone, Copy, Default)] +enum ShadowFilter { + /// The non-temporal Gaussian filter (Castano '13). + #[default] + Gaussian, + /// The temporal Gaussian filter (Jimenez '14). + Temporal, +} + +/// The application entry point. +fn main() { + App::new() + .init_resource::() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, handle_light_type_change_input) + .add_systems(Update, handle_shadow_filter_change_input) + .add_systems(Update, handle_pcss_toggle_input) + .add_systems( + Update, + update_help_text + .after(handle_light_type_change_input) + .after(handle_shadow_filter_change_input) + .after(handle_pcss_toggle_input), + ) + .run(); +} + +/// Creates all the objects in the scene. +fn setup(mut commands: Commands, asset_server: Res, app_status: Res) { + spawn_camera(&mut commands); + spawn_light(&mut commands, &app_status); + spawn_gltf_scene(&mut commands, &asset_server); + spawn_help_text(&mut commands, &asset_server, &app_status); +} + +/// Spawns the camera, with the initial shadow filtering method. +fn spawn_camera(commands: &mut Commands) { + commands + .spawn(Camera3dBundle { + transform: Transform::from_xyz(7.0, 4.0, 7.0).looking_at(vec3(0.0, 1.25, 0.0), Vec3::Y), + ..default() + }) + .insert(ShadowFilteringMethod::Gaussian); +} + +/// Spawns the initial light. +fn spawn_light(commands: &mut Commands, app_status: &AppStatus) { + commands.spawn(DirectionalLightBundle { + directional_light: create_directional_light(&app_status), + transform: Transform::from_xyz(1.0, 5.0, -4.0).looking_at(vec3(0.0, 2.5, 0.0), Vec3::Y), + ..default() + }); +} + +/// Loads and spawns the glTF palm tree scene. +fn spawn_gltf_scene(commands: &mut Commands, asset_server: &AssetServer) { + commands.spawn(SceneBundle { + scene: asset_server.load("models/PalmTree/PalmTree.gltf#Scene0"), + ..default() + }); +} + +/// Spawns the help text at the top of the screen. +fn spawn_help_text(commands: &mut Commands, asset_server: &AssetServer, app_status: &AppStatus) { + commands.spawn( + TextBundle { + text: app_status.create_help_text(asset_server), + ..default() + } + .with_style(Style { + position_type: PositionType::Absolute, + top: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }), + ); +} + +/// Updates the help text at the top of the screen. +fn update_help_text( + mut text_query: Query<&mut Text>, + app_status: Res, + asset_server: Res, +) { + for mut text in text_query.iter_mut() { + *text = app_status.create_help_text(&asset_server); + } +} + +/// Handles requests from the user to change the type of light. +fn handle_light_type_change_input( + mut commands: Commands, + mut lights: Query, With)>>, + keyboard: Res>, + mut app_status: ResMut, +) { + if !keyboard.just_pressed(KeyCode::Space) { + return; + } + + for light in lights.iter_mut() { + match app_status.light_type { + LightType::Directional => { + app_status.light_type = LightType::Point; + commands + .entity(light) + .remove::() + .insert(create_point_light(&app_status)); + } + LightType::Point => { + app_status.light_type = LightType::Directional; + commands + .entity(light) + .remove::() + .insert(create_directional_light(&app_status)); + } + } + } +} + +/// Handles requests from the user to change the shadow filter method. +fn handle_shadow_filter_change_input( + mut cameras: Query<&mut ShadowFilteringMethod>, + keyboard: Res>, + mut app_status: ResMut, +) { + if !keyboard.just_pressed(KeyCode::Tab) { + return; + } + + app_status.shadow_filter = match app_status.shadow_filter { + ShadowFilter::Gaussian => ShadowFilter::Temporal, + ShadowFilter::Temporal => ShadowFilter::Gaussian, + }; + + for mut shadow_filtering_method in cameras.iter_mut() { + *shadow_filtering_method = match app_status.shadow_filter { + ShadowFilter::Gaussian => ShadowFilteringMethod::Gaussian, + ShadowFilter::Temporal => ShadowFilteringMethod::Temporal, + }; + } +} + +/// Handles requests from the user to toggle soft shadows on and off. +fn handle_pcss_toggle_input( + mut lights: Query>, + keyboard: Res>, + mut app_status: ResMut, +) { + if !keyboard.just_pressed(KeyCode::Enter) { + return; + } + + app_status.pcss_enabled = !app_status.pcss_enabled; + + for (directional_light, point_light) in lights.iter_mut() { + if let Some(mut directional_light) = directional_light { + *directional_light = create_directional_light(&app_status); + } + if let Some(mut point_light) = point_light { + *point_light = create_point_light(&app_status); + } + } +} + +/// Creates the [`DirectionalLight`] component with the appropriate settings. +fn create_directional_light(app_status: &AppStatus) -> DirectionalLight { + DirectionalLight { + shadows_enabled: true, + soft_shadow_size: if app_status.pcss_enabled { + Some(10.0) + } else { + None + }, + shadow_depth_bias: 0.25, + ..default() + } +} + +/// Creates the [`PointLight`] component with the appropriate settings. +fn create_point_light(app_status: &AppStatus) -> PointLight { + PointLight { + shadows_enabled: true, + // TODO: soft_shadows: true, + ..default() + } +} + +impl Default for AppStatus { + fn default() -> Self { + Self { + light_type: default(), + shadow_filter: default(), + pcss_enabled: true, + } + } +} + +impl AppStatus { + /// Builds the help text at the top of the screen, reflecting the current + /// status of the app. + fn create_help_text(&self, asset_server: &AssetServer) -> Text { + let light_type_help_text = match self.light_type { + LightType::Directional => "Press Space to switch to a point light", + LightType::Point => "Press Space to switch to a directional light", + }; + + let shadow_filter_help_text = match self.shadow_filter { + ShadowFilter::Gaussian => "Press Tab to switch to temporal shadow filtering", + ShadowFilter::Temporal => "Press Tab to switch to Gaussian shadow filtering", + }; + + let pcss_help_text = if self.pcss_enabled { + "Press Enter to disable soft shadows" + } else { + "Press Enter to enable soft shadows" + }; + + Text::from_section( + format!( + "{}\n{}\n{}", + light_type_help_text, shadow_filter_help_text, pcss_help_text + ), + TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 24.0, + ..default() + }, + ) + } +}