diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index d1d9f134814a8..c71445b0ee9e4 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -226,14 +226,23 @@ pub struct DirectionalLightShadowMap { impl Default for DirectionalLightShadowMap { fn default() -> Self { - #[cfg(feature = "webgl")] - return Self { size: 1024 }; - #[cfg(not(feature = "webgl"))] - return Self { size: 2048 }; + Self { size: 2048 } } } /// Controls how cascaded shadow mapping works. +/// Prefer using [`CascadeShadowConfigBuilder`] to construct an instance. +/// +/// ``` +/// # use bevy_pbr::CascadeShadowConfig; +/// # use bevy_pbr::CascadeShadowConfigBuilder; +/// # use bevy_utils::default; +/// # +/// let config: CascadeShadowConfig = CascadeShadowConfigBuilder { +/// maximum_distance: 100.0, +/// ..default() +/// }.into(); +/// ``` #[derive(Component, Clone, Debug, Reflect)] #[reflect(Component)] pub struct CascadeShadowConfig { @@ -241,16 +250,13 @@ pub struct CascadeShadowConfig { pub bounds: Vec, /// The proportion of overlap each cascade has with the previous cascade. pub overlap_proportion: f32, + /// The (positive) distance to the near boundary of the first cascade. + pub minimum_distance: f32, } impl Default for CascadeShadowConfig { fn default() -> Self { - if cfg!(feature = "webgl") { - // Currently only support one cascade in webgl. - Self::new(1, 5.0, 100.0, 0.2) - } else { - Self::new(4, 5.0, 1000.0, 0.2) - } + CascadeShadowConfigBuilder::default().into() } } @@ -268,31 +274,112 @@ fn calculate_cascade_bounds( .collect() } -impl CascadeShadowConfig { - /// Returns a cascade config for `num_cascades` cascades, with the first cascade - /// having far bound `nearest_bound` and the last cascade having far bound `shadow_maximum_distance`. - /// In-between cascades will be exponentially spaced. - pub fn new( - num_cascades: usize, - nearest_bound: f32, - shadow_maximum_distance: f32, - overlap_proportion: f32, - ) -> Self { +/// Builder for [`CascadeShadowConfig`]. +pub struct CascadeShadowConfigBuilder { + /// The number of shadow cascades. + /// More cascades increases shadow quality by mitigating perspective aliasing - a phenomenom where areas + /// nearer the camera are covered by fewer shadow map texels than areas further from the camera, causing + /// blocky looking shadows. + /// + /// This does come at the cost increased rendering overhead, however this overhead is still less + /// than if you were to use fewer cascades and much larger shadow map textures to achieve the + /// same quality level. + /// + /// In case rendered geometry covers a relatively narrow and static depth relative to camera, it may + /// make more sense to use fewer cascades and a higher resolution shadow map texture as perspective aliasing + /// is not as much an issue. Be sure to adjust `minimum_distance` and `maximum_distance` appropriately. + pub num_cascades: usize, + /// The minimum shadow distance, which can help improve the texel resolution of the first cascade. + /// Areas nearer to the camera than this will likely receive no shadows. + /// + /// NOTE: Due to implementation details, this usually does not impact shadow quality as much as + /// `first_cascade_far_bound` and `maximum_distance`. At many view frustum field-of-views, the + /// texel resolution of the first cascade is dominated by the width / height of the view frustum plane + /// at `first_cascade_far_bound` rather than the depth of the frustum from `minimum_distance` to + /// `first_cascade_far_bound`. + pub minimum_distance: f32, + /// The maximum shadow distance. + /// Areas further from the camera than this will likely receive no shadows. + pub maximum_distance: f32, + /// Sets the far bound of the first cascade, relative to the view origin. + /// In-between cascades will be exponentially spaced relative to the maximum shadow distance. + /// NOTE: This is ignored if there is only one cascade, the maximum distance takes precedence. + pub first_cascade_far_bound: f32, + /// Sets the overlap proportion between cascades. + /// The overlap is used to make the transition from one cascade's shadow map to the next + /// less abrupt by blending between both shadow maps. + pub overlap_proportion: f32, +} + +impl CascadeShadowConfigBuilder { + /// Returns the cascade config as specified by this builder. + pub fn build(&self) -> CascadeShadowConfig { assert!( - num_cascades > 0, - "num_cascades must be positive, but was {num_cascades}", + self.num_cascades > 0, + "num_cascades must be positive, but was {}", + self.num_cascades ); assert!( - (0.0..1.0).contains(&overlap_proportion), - "overlap_proportion must be in [0.0, 1.0) but was {overlap_proportion}", + self.minimum_distance >= 0.0, + "maximum_distance must be non-negative, but was {}", + self.minimum_distance ); - Self { - bounds: calculate_cascade_bounds(num_cascades, nearest_bound, shadow_maximum_distance), - overlap_proportion, + assert!( + self.num_cascades == 1 || self.minimum_distance < self.first_cascade_far_bound, + "minimum_distance must be less than first_cascade_far_bound, but was {}", + self.minimum_distance + ); + assert!( + self.maximum_distance > self.minimum_distance, + "maximum_distance must be greater than minimum_distance, but was {}", + self.maximum_distance + ); + assert!( + (0.0..1.0).contains(&self.overlap_proportion), + "overlap_proportion must be in [0.0, 1.0) but was {}", + self.overlap_proportion + ); + CascadeShadowConfig { + bounds: calculate_cascade_bounds( + self.num_cascades, + self.first_cascade_far_bound, + self.maximum_distance, + ), + overlap_proportion: self.overlap_proportion, + minimum_distance: self.minimum_distance, } } } +impl Default for CascadeShadowConfigBuilder { + fn default() -> Self { + if cfg!(feature = "webgl") { + // Currently only support one cascade in webgl. + Self { + num_cascades: 1, + minimum_distance: 0.1, + maximum_distance: 100.0, + first_cascade_far_bound: 5.0, + overlap_proportion: 0.2, + } + } else { + Self { + num_cascades: 4, + minimum_distance: 0.1, + maximum_distance: 1000.0, + first_cascade_far_bound: 5.0, + overlap_proportion: 0.2, + } + } + } +} + +impl From for CascadeShadowConfig { + fn from(builder: CascadeShadowConfigBuilder) -> Self { + builder.build() + } +} + #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component)] pub struct Cascades { @@ -375,7 +462,7 @@ pub fn update_directional_light_cascades( (1.0 - cascades_config.overlap_proportion) * -cascades_config.bounds[idx - 1] } else { - 0.0 + -cascades_config.minimum_distance }, -far_bound, ) diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 57b5162028b47..c33cea15be2fa 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -226,7 +226,10 @@ fn pbr( && (lights.directional_lights[i].flags & DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { shadow = fetch_directional_shadow(i, in.world_position, in.world_normal, view_z); } - let light_contrib = directional_light(i, roughness, NdotV, in.N, in.V, R, F0, diffuse_color); + var light_contrib = directional_light(i, roughness, NdotV, in.N, in.V, R, F0, diffuse_color); +#ifdef DIRECTIONAL_LIGHT_SHADOW_MAP_DEBUG_CASCADES + light_contrib = cascade_debug_visualization(light_contrib, i, view_z); +#endif light_accum = light_accum + light_contrib * shadow; } diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index a3f1f79a97f2d..f74733c90051e 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -144,7 +144,7 @@ fn sample_cascade(light_id: u32, cascade_index: u32, frag_position: vec4, s directional_shadow_textures_sampler, light_local, depth - ); + ); #else return textureSampleCompareLevel( directional_shadow_textures, @@ -152,14 +152,14 @@ fn sample_cascade(light_id: u32, cascade_index: u32, frag_position: vec4, s light_local, i32((*light).depth_texture_base_index + cascade_index), depth - ); + ); #endif } fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3, view_z: f32) -> f32 { let light = &lights.directional_lights[light_id]; let cascade_index = get_cascade_index(light_id, view_z); - + if (cascade_index >= (*light).num_cascades) { return 1.0; } @@ -178,3 +178,16 @@ fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_nor } return shadow; } + +fn cascade_debug_visualization( + output_color: vec3, + light_id: u32, + view_z: f32, +) -> vec3 { + let overlay_alpha = 0.95; + let cascade_index = get_cascade_index(light_id, view_z); + let cascade_color = hsv2rgb(f32(cascade_index) / f32(#{MAX_CASCADES_PER_LIGHT}u + 1u), 1.0, 0.5); + return vec3( + (1.0 - overlay_alpha) * output_color.rgb + overlay_alpha * cascade_color + ); +} diff --git a/examples/3d/atmospheric_fog.rs b/examples/3d/atmospheric_fog.rs index d1024e83724c3..5766bc7201e3b 100644 --- a/examples/3d/atmospheric_fog.rs +++ b/examples/3d/atmospheric_fog.rs @@ -8,7 +8,7 @@ //! | `S` | Toggle Directional Light Fog Influence | use bevy::{ - pbr::{CascadeShadowConfig, NotShadowCaster}, + pbr::{CascadeShadowConfigBuilder, NotShadowCaster}, prelude::*, }; @@ -49,9 +49,12 @@ fn setup_terrain_scene( asset_server: Res, ) { // Configure a properly scaled cascade shadow map for this scene (defaults are too large, mesh units are in km) - // For WebGL we only support 1 cascade level for now - let cascade_shadow_config = - CascadeShadowConfig::new(if cfg!(feature = "webgl") { 1 } else { 4 }, 0.5, 2.5, 0.2); + let cascade_shadow_config = CascadeShadowConfigBuilder { + first_cascade_far_bound: 0.3, + maximum_distance: 3.0, + ..default() + } + .build(); // Sun commands.spawn(DirectionalLightBundle { diff --git a/examples/3d/fxaa.rs b/examples/3d/fxaa.rs index cc09d08ab41ef..279f4c18a583b 100644 --- a/examples/3d/fxaa.rs +++ b/examples/3d/fxaa.rs @@ -4,6 +4,7 @@ use std::f32::consts::PI; use bevy::{ core_pipeline::fxaa::{Fxaa, Sensitivity}, + pbr::CascadeShadowConfigBuilder, prelude::*, render::{ render_resource::{Extent3d, SamplerDescriptor, TextureDimension, TextureFormat}, @@ -81,6 +82,12 @@ fn setup( PI * -0.15, PI * -0.15, )), + cascade_shadow_config: CascadeShadowConfigBuilder { + maximum_distance: 3.0, + first_cascade_far_bound: 0.9, + ..default() + } + .into(), ..default() }); diff --git a/examples/3d/lighting.rs b/examples/3d/lighting.rs index eef200b79cb36..ef382ddb32b58 100644 --- a/examples/3d/lighting.rs +++ b/examples/3d/lighting.rs @@ -3,7 +3,7 @@ use std::f32::consts::PI; -use bevy::{pbr::CascadeShadowConfig, prelude::*}; +use bevy::{pbr::CascadeShadowConfigBuilder, prelude::*}; fn main() { App::new() @@ -198,8 +198,13 @@ fn setup( }, // The default cascade config is designed to handle large scenes. // As this example has a much smaller world, we can tighten the shadow - // far bound for better visual quality. - cascade_shadow_config: CascadeShadowConfig::new(4, 5.0, 30.0, 0.2), + // bounds for better visual quality. + cascade_shadow_config: CascadeShadowConfigBuilder { + first_cascade_far_bound: 4.0, + maximum_distance: 10.0, + ..default() + } + .into(), ..default() }); diff --git a/examples/3d/load_gltf.rs b/examples/3d/load_gltf.rs index 0e59304e173d6..3123599d9cb3c 100644 --- a/examples/3d/load_gltf.rs +++ b/examples/3d/load_gltf.rs @@ -2,7 +2,10 @@ use std::f32::consts::*; -use bevy::{pbr::CascadeShadowConfig, prelude::*}; +use bevy::{ + pbr::{CascadeShadowConfigBuilder, DirectionalLightShadowMap}, + prelude::*, +}; fn main() { App::new() @@ -10,6 +13,7 @@ fn main() { color: Color::WHITE, brightness: 1.0 / 5.0f32, }) + .insert_resource(DirectionalLightShadowMap { size: 4096 }) .add_plugins(DefaultPlugins) .add_startup_system(setup) .add_system(animate_light_direction) @@ -28,7 +32,14 @@ fn setup(mut commands: Commands, asset_server: Res) { }, // This is a relatively small scene, so use tighter shadow // cascade bounds than the default for better quality. - cascade_shadow_config: CascadeShadowConfig::new(1, 1.1, 1.5, 0.3), + // We also adjusted the shadow map to be larger since we're + // only using a single cascade. + cascade_shadow_config: CascadeShadowConfigBuilder { + num_cascades: 1, + maximum_distance: 1.6, + ..default() + } + .into(), ..default() }); commands.spawn(SceneBundle { diff --git a/examples/3d/shadow_caster_receiver.rs b/examples/3d/shadow_caster_receiver.rs index 3488ed48b383e..0216acb01638a 100644 --- a/examples/3d/shadow_caster_receiver.rs +++ b/examples/3d/shadow_caster_receiver.rs @@ -3,7 +3,7 @@ use std::f32::consts::PI; use bevy::{ - pbr::{NotShadowCaster, NotShadowReceiver}, + pbr::{CascadeShadowConfigBuilder, NotShadowCaster, NotShadowReceiver}, prelude::*, }; @@ -109,6 +109,12 @@ fn setup( PI / 2., -PI / 4., )), + cascade_shadow_config: CascadeShadowConfigBuilder { + first_cascade_far_bound: 7.0, + maximum_distance: 25.0, + ..default() + } + .into(), ..default() }); diff --git a/examples/3d/split_screen.rs b/examples/3d/split_screen.rs index 2ff5b299e7cc7..9a32318ada676 100644 --- a/examples/3d/split_screen.rs +++ b/examples/3d/split_screen.rs @@ -3,8 +3,8 @@ use std::f32::consts::PI; use bevy::{ - core_pipeline::clear_color::ClearColorConfig, prelude::*, render::camera::Viewport, - window::WindowResized, + core_pipeline::clear_color::ClearColorConfig, pbr::CascadeShadowConfigBuilder, prelude::*, + render::camera::Viewport, window::WindowResized, }; fn main() { @@ -41,6 +41,13 @@ fn setup( shadows_enabled: true, ..default() }, + cascade_shadow_config: CascadeShadowConfigBuilder { + num_cascades: 2, + first_cascade_far_bound: 200.0, + maximum_distance: 280.0, + ..default() + } + .into(), ..default() }); diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 1f1232d79d03a..7b564c12e06da 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -3,6 +3,7 @@ use std::f32::consts::PI; use std::time::Duration; +use bevy::pbr::CascadeShadowConfigBuilder; use bevy::prelude::*; fn main() { @@ -55,6 +56,12 @@ fn setup( shadows_enabled: true, ..default() }, + cascade_shadow_config: CascadeShadowConfigBuilder { + first_cascade_far_bound: 200.0, + maximum_distance: 400.0, + ..default() + } + .into(), ..default() }); diff --git a/examples/stress_tests/many_foxes.rs b/examples/stress_tests/many_foxes.rs index f4d2f631e2170..bf58aa522de8c 100644 --- a/examples/stress_tests/many_foxes.rs +++ b/examples/stress_tests/many_foxes.rs @@ -6,6 +6,7 @@ use std::time::Duration; use bevy::{ diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, + pbr::CascadeShadowConfigBuilder, prelude::*, window::{PresentMode, WindowPlugin}, }; @@ -172,6 +173,12 @@ fn setup( shadows_enabled: true, ..default() }, + cascade_shadow_config: CascadeShadowConfigBuilder { + first_cascade_far_bound: 0.9 * radius, + maximum_distance: 2.8 * radius, + ..default() + } + .into(), ..default() });