Skip to content

Commit

Permalink
Better cascades config defaults + builder, tweak example configs (#7456)
Browse files Browse the repository at this point in the history
# Objective

- Improve ergonomics / documentation of cascaded shadow maps
- Allow for the customization of the nearest shadowing distance.
- Fixes #7393 
- Fixes #7362 

## Solution

- Introduce `CascadeShadowConfigBuilder`
- Tweak various example cascade settings for better quality.

---

## Changelog

- Made examples look nicer under cascaded shadow maps.
- Introduce `CascadeShadowConfigBuilder` to help with creating `CascadeShadowConfig`

## Migration Guide

- Configure settings for cascaded shadow maps for directional lights using the newly introduced `CascadeShadowConfigBuilder`.

Co-authored-by: Robert Swain <robert.swain@gmail.com>
  • Loading branch information
danchia and superdump committed Feb 5, 2023
1 parent 5ee57ff commit 52f0617
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 44 deletions.
143 changes: 115 additions & 28 deletions crates/bevy_pbr/src/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,31 +226,37 @@ 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 {
/// The (positive) distance to the far boundary of each cascade.
pub bounds: Vec<f32>,
/// 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()
}
}

Expand All @@ -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<CascadeShadowConfigBuilder> for CascadeShadowConfig {
fn from(builder: CascadeShadowConfigBuilder) -> Self {
builder.build()
}
}

#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct Cascades {
Expand Down Expand Up @@ -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,
)
Expand Down
5 changes: 4 additions & 1 deletion crates/bevy_pbr/src/render/pbr_functions.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
19 changes: 16 additions & 3 deletions crates/bevy_pbr/src/render/shadows.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -144,22 +144,22 @@ fn sample_cascade(light_id: u32, cascade_index: u32, frag_position: vec4<f32>, s
directional_shadow_textures_sampler,
light_local,
depth
);
);
#else
return textureSampleCompareLevel(
directional_shadow_textures,
directional_shadow_textures_sampler,
light_local,
i32((*light).depth_texture_base_index + cascade_index),
depth
);
);
#endif
}

fn fetch_directional_shadow(light_id: u32, frag_position: vec4<f32>, surface_normal: vec3<f32>, 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;
}
Expand All @@ -178,3 +178,16 @@ fn fetch_directional_shadow(light_id: u32, frag_position: vec4<f32>, surface_nor
}
return shadow;
}

fn cascade_debug_visualization(
output_color: vec3<f32>,
light_id: u32,
view_z: f32,
) -> vec3<f32> {
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<f32>(
(1.0 - overlay_alpha) * output_color.rgb + overlay_alpha * cascade_color
);
}
11 changes: 7 additions & 4 deletions examples/3d/atmospheric_fog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! | `S` | Toggle Directional Light Fog Influence |

use bevy::{
pbr::{CascadeShadowConfig, NotShadowCaster},
pbr::{CascadeShadowConfigBuilder, NotShadowCaster},
prelude::*,
};

Expand Down Expand Up @@ -49,9 +49,12 @@ fn setup_terrain_scene(
asset_server: Res<AssetServer>,
) {
// 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 {
Expand Down
7 changes: 7 additions & 0 deletions examples/3d/fxaa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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()
});

Expand Down
11 changes: 8 additions & 3 deletions examples/3d/lighting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use std::f32::consts::PI;

use bevy::{pbr::CascadeShadowConfig, prelude::*};
use bevy::{pbr::CascadeShadowConfigBuilder, prelude::*};

fn main() {
App::new()
Expand Down Expand Up @@ -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()
});

Expand Down
15 changes: 13 additions & 2 deletions examples/3d/load_gltf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

use std::f32::consts::*;

use bevy::{pbr::CascadeShadowConfig, prelude::*};
use bevy::{
pbr::{CascadeShadowConfigBuilder, DirectionalLightShadowMap},
prelude::*,
};

fn main() {
App::new()
.insert_resource(AmbientLight {
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)
Expand All @@ -28,7 +32,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
// 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 {
Expand Down
8 changes: 7 additions & 1 deletion examples/3d/shadow_caster_receiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use std::f32::consts::PI;

use bevy::{
pbr::{NotShadowCaster, NotShadowReceiver},
pbr::{CascadeShadowConfigBuilder, NotShadowCaster, NotShadowReceiver},
prelude::*,
};

Expand Down Expand Up @@ -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()
});

Expand Down
11 changes: 9 additions & 2 deletions examples/3d/split_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
});

Expand Down
Loading

0 comments on commit 52f0617

Please sign in to comment.