//! Provides shadow cascade configuration and construction helpers.12use bevy_camera::{Camera, Projection};3use bevy_ecs::{entity::EntityHashMap, prelude::*};4use bevy_math::{ops, Mat4, Vec3A, Vec4};5use bevy_reflect::prelude::*;6use bevy_transform::components::GlobalTransform;78use crate::{DirectionalLight, DirectionalLightShadowMap};910/// Controls how cascaded shadow mapping works.11/// Prefer using [`CascadeShadowConfigBuilder`] to construct an instance.12///13/// ```14/// # use bevy_light::CascadeShadowConfig;15/// # use bevy_light::CascadeShadowConfigBuilder;16/// # use bevy_utils::default;17/// #18/// let config: CascadeShadowConfig = CascadeShadowConfigBuilder {19/// maximum_distance: 100.0,20/// ..default()21/// }.into();22/// ```23#[derive(Component, Clone, Debug, Reflect)]24#[reflect(Component, Default, Debug, Clone)]25pub struct CascadeShadowConfig {26/// The (positive) distance to the far boundary of each cascade.27pub bounds: Vec<f32>,28/// The proportion of overlap each cascade has with the previous cascade.29pub overlap_proportion: f32,30/// The (positive) distance to the near boundary of the first cascade.31pub minimum_distance: f32,32}3334impl Default for CascadeShadowConfig {35fn default() -> Self {36CascadeShadowConfigBuilder::default().into()37}38}3940fn calculate_cascade_bounds(41num_cascades: usize,42nearest_bound: f32,43shadow_maximum_distance: f32,44) -> Vec<f32> {45if num_cascades == 1 {46return vec![shadow_maximum_distance];47}48let base = ops::powf(49shadow_maximum_distance / nearest_bound,501.0 / (num_cascades - 1) as f32,51);52(0..num_cascades)53.map(|i| nearest_bound * ops::powf(base, i as f32))54.collect()55}5657/// Builder for [`CascadeShadowConfig`].58pub struct CascadeShadowConfigBuilder {59/// The number of shadow cascades.60/// More cascades increases shadow quality by mitigating perspective aliasing - a phenomenon where areas61/// nearer the camera are covered by fewer shadow map texels than areas further from the camera, causing62/// blocky looking shadows.63///64/// This does come at the cost increased rendering overhead, however this overhead is still less65/// than if you were to use fewer cascades and much larger shadow map textures to achieve the66/// same quality level.67///68/// In case rendered geometry covers a relatively narrow and static depth relative to camera, it may69/// make more sense to use fewer cascades and a higher resolution shadow map texture as perspective aliasing70/// is not as much an issue. Be sure to adjust `minimum_distance` and `maximum_distance` appropriately.71pub num_cascades: usize,72/// The minimum shadow distance, which can help improve the texel resolution of the first cascade.73/// Areas nearer to the camera than this will likely receive no shadows.74///75/// NOTE: Due to implementation details, this usually does not impact shadow quality as much as76/// `first_cascade_far_bound` and `maximum_distance`. At many view frustum field-of-views, the77/// texel resolution of the first cascade is dominated by the width / height of the view frustum plane78/// at `first_cascade_far_bound` rather than the depth of the frustum from `minimum_distance` to79/// `first_cascade_far_bound`.80pub minimum_distance: f32,81/// The maximum shadow distance.82/// Areas further from the camera than this will likely receive no shadows.83pub maximum_distance: f32,84/// Sets the far bound of the first cascade, relative to the view origin.85/// In-between cascades will be exponentially spaced relative to the maximum shadow distance.86/// NOTE: This is ignored if there is only one cascade, the maximum distance takes precedence.87pub first_cascade_far_bound: f32,88/// Sets the overlap proportion between cascades.89/// The overlap is used to make the transition from one cascade's shadow map to the next90/// less abrupt by blending between both shadow maps.91pub overlap_proportion: f32,92}9394impl CascadeShadowConfigBuilder {95/// Returns the cascade config as specified by this builder.96pub fn build(&self) -> CascadeShadowConfig {97assert!(98self.num_cascades > 0,99"num_cascades must be positive, but was {}",100self.num_cascades101);102assert!(103self.minimum_distance >= 0.0,104"maximum_distance must be non-negative, but was {}",105self.minimum_distance106);107assert!(108self.num_cascades == 1 || self.minimum_distance < self.first_cascade_far_bound,109"minimum_distance must be less than first_cascade_far_bound, but was {}",110self.minimum_distance111);112assert!(113self.maximum_distance > self.minimum_distance,114"maximum_distance must be greater than minimum_distance, but was {}",115self.maximum_distance116);117assert!(118(0.0..1.0).contains(&self.overlap_proportion),119"overlap_proportion must be in [0.0, 1.0) but was {}",120self.overlap_proportion121);122CascadeShadowConfig {123bounds: calculate_cascade_bounds(124self.num_cascades,125self.first_cascade_far_bound,126self.maximum_distance,127),128overlap_proportion: self.overlap_proportion,129minimum_distance: self.minimum_distance,130}131}132}133134impl Default for CascadeShadowConfigBuilder {135fn default() -> Self {136// The defaults are chosen to be similar to be Unity, Unreal, and Godot.137// Unity: first cascade far bound = 10.05, maximum distance = 150.0138// Unreal Engine 5: maximum distance = 200.0139// Godot: first cascade far bound = 10.0, maximum distance = 100.0140Self {141// Currently only support one cascade in WebGL 2.142num_cascades: if cfg!(all(143feature = "webgl",144target_arch = "wasm32",145not(feature = "webgpu")146)) {1471148} else {1494150},151minimum_distance: 0.1,152maximum_distance: 150.0,153first_cascade_far_bound: 10.0,154overlap_proportion: 0.2,155}156}157}158159impl From<CascadeShadowConfigBuilder> for CascadeShadowConfig {160fn from(builder: CascadeShadowConfigBuilder) -> Self {161builder.build()162}163}164165/// A [`DirectionalLight`]'s per-view list of [`Cascade`]s.166#[derive(Component, Clone, Debug, Default, Reflect)]167#[reflect(Component, Debug, Default, Clone)]168pub struct Cascades {169/// Map from a view to the configuration of each of its [`Cascade`]s.170pub cascades: EntityHashMap<Vec<Cascade>>,171}172173/// A single cascade of a view's shadow map cascade. Several of these are174/// used to cover most of the view to ensure most geometry gets shadows, with175/// some overlap for blending at cascade transitions. Farther away cascades176/// are larger and have a lower effective shadowmap texel per world unit177/// resolution. All cascades have the same pixel dimensions however.178#[derive(Clone, Debug, Default, Reflect)]179#[reflect(Clone, Default)]180pub struct Cascade {181/// The transform of the light, i.e. the view to world matrix.182pub world_from_cascade: Mat4,183/// The orthographic projection for this cascade.184pub clip_from_cascade: Mat4,185/// The view-projection matrix for this cascade, converting world space into light clip space.186/// Importantly, this is derived and stored separately from `view_transform` and `projection` to187/// ensure shadow stability.188pub clip_from_world: Mat4,189/// Size of each shadow map texel in world units.190pub texel_size: f32,191}192193/// Sets up [`Cascades`] for all shadow mapped [`DirectionalLight`]s.194pub fn build_directional_light_cascades(195directional_light_shadow_map: Res<DirectionalLightShadowMap>,196views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>,197mut lights: Query<(198&GlobalTransform,199&DirectionalLight,200&CascadeShadowConfig,201&mut Cascades,202)>,203) {204let views = views205.iter()206.filter_map(|(entity, transform, projection, camera)| {207if camera.is_active {208Some((entity, projection, transform.to_matrix()))209} else {210None211}212})213.collect::<Vec<_>>();214215for (transform, directional_light, cascades_config, mut cascades) in &mut lights {216if !directional_light.shadow_maps_enabled {217continue;218}219cascades.cascades.clear();220221// It is very important to the numerical and thus visual stability of shadows that222// `world_from_light` has orthogonal upper-left 3x3 and zero translation.223// Even though only the direction (i.e. rotation) of the light matters, we don't constrain224// users to not change any other aspects of the transform - there's no guarantee225// `transform.to_matrix()` will give us a matrix with our desired properties.226// Instead, we directly create a good matrix from just the rotation.227let world_from_light = Mat4::from_quat(transform.rotation());228// The transpose is the inverse for orthogonal matrices.229let light_from_world = world_from_light.transpose();230231for (view_entity, projection, world_from_view) in views.iter().copied() {232let light_view_from_camera = light_from_world * world_from_view;233let overlap_factor = 1.0 - cascades_config.overlap_proportion;234let far_bounds = cascades_config.bounds.iter();235let near_bounds = [cascades_config.minimum_distance]236.into_iter()237.chain(far_bounds.clone().map(|bound| overlap_factor * bound));238let view_cascades = near_bounds239.zip(far_bounds)240.map(|(near_bound, far_bound)| {241// Negate bounds as -z is camera forward direction.242let corners = projection.get_frustum_corners(-near_bound, -far_bound);243calculate_cascade(244corners,245directional_light_shadow_map.size as f32,246world_from_light,247light_view_from_camera,248)249})250.collect();251cascades.cascades.insert(view_entity, view_cascades);252}253}254}255256/// Returns a [`Cascade`] for the frustum defined by `frustum_corners`.257///258/// The corner vertices should be specified in the following order:259/// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane.260///261/// See this [reference](https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf) for more details.262fn calculate_cascade(263frustum_corners: [Vec3A; 8],264cascade_texture_size: f32,265world_from_light: Mat4,266light_from_camera: Mat4,267) -> Cascade {268let mut min = Vec3A::splat(f32::MAX);269let mut max = Vec3A::splat(f32::MIN);270for corner_camera_view in frustum_corners {271let corner_light_view = light_from_camera.transform_point3a(corner_camera_view);272min = min.min(corner_light_view);273max = max.max(corner_light_view);274}275276// NOTE: Use the larger of the frustum slice far plane diagonal and body diagonal lengths as this277// will be the maximum possible projection size. Use the ceiling to get an integer which is278// very important for floating point stability later. It is also important that these are279// calculated using the original camera space corner positions for floating point precision280// as even though the lengths using corner_light_view above should be the same, precision can281// introduce small but significant differences.282// NOTE: The size remains the same unless the view frustum or cascade configuration is modified.283let body_diagonal = (frustum_corners[0] - frustum_corners[6]).length_squared();284let far_plane_diagonal = (frustum_corners[4] - frustum_corners[6]).length_squared();285let cascade_diameter = body_diagonal.max(far_plane_diagonal).sqrt().ceil();286287// NOTE: If we ensure that cascade_texture_size is a power of 2, then as we made cascade_diameter an288// integer, cascade_texel_size is then an integer multiple of a power of 2 and can be289// exactly represented in a floating point value.290let cascade_texel_size = cascade_diameter / cascade_texture_size;291// NOTE: For shadow stability it is very important that the near_plane_center is at integer292// multiples of the texel size to be exactly representable in a floating point value.293let near_plane_center = Vec3A::new(294(0.5 * (min.x + max.x) / cascade_texel_size).floor() * cascade_texel_size,295(0.5 * (min.y + max.y) / cascade_texel_size).floor() * cascade_texel_size,296// NOTE: max.z is the near plane for right-handed y-up297max.z,298);299300// It is critical for `cascade_from_world` to be stable. So rather than forming `world_from_cascade`301// and inverting it, which risks instability due to numerical precision, we directly form302// `cascade_from_world` as the reference material suggests.303let world_from_light_transpose = world_from_light.transpose();304let cascade_from_world = Mat4::from_cols(305world_from_light_transpose.x_axis,306world_from_light_transpose.y_axis,307world_from_light_transpose.z_axis,308(-near_plane_center).extend(1.0),309);310let world_from_cascade = Mat4::from_cols(311world_from_light.x_axis,312world_from_light.y_axis,313world_from_light.z_axis,314world_from_light * near_plane_center.extend(1.0),315);316317// Right-handed orthographic projection, centered at `near_plane_center`.318// NOTE: This is different from the reference material, as we use reverse Z.319let r = (max.z - min.z).recip();320let clip_from_cascade = Mat4::from_cols(321Vec4::new(2.0 / cascade_diameter, 0.0, 0.0, 0.0),322Vec4::new(0.0, 2.0 / cascade_diameter, 0.0, 0.0),323Vec4::new(0.0, 0.0, r, 0.0),324Vec4::new(0.0, 0.0, 1.0, 1.0),325);326327let clip_from_world = clip_from_cascade * cascade_from_world;328Cascade {329world_from_cascade,330clip_from_cascade,331clip_from_world,332texel_size: cascade_texel_size,333}334}335336337