use bevy_camera::{Camera, Projection};1use bevy_ecs::{entity::EntityHashMap, prelude::*};2use bevy_math::{ops, Mat4, Vec3A, Vec4};3use bevy_reflect::prelude::*;4use bevy_transform::components::GlobalTransform;56use crate::{DirectionalLight, DirectionalLightShadowMap};78/// Controls how cascaded shadow mapping works.9/// Prefer using [`CascadeShadowConfigBuilder`] to construct an instance.10///11/// ```12/// # use bevy_light::CascadeShadowConfig;13/// # use bevy_light::CascadeShadowConfigBuilder;14/// # use bevy_utils::default;15/// #16/// let config: CascadeShadowConfig = CascadeShadowConfigBuilder {17/// maximum_distance: 100.0,18/// ..default()19/// }.into();20/// ```21#[derive(Component, Clone, Debug, Reflect)]22#[reflect(Component, Default, Debug, Clone)]23pub struct CascadeShadowConfig {24/// The (positive) distance to the far boundary of each cascade.25pub bounds: Vec<f32>,26/// The proportion of overlap each cascade has with the previous cascade.27pub overlap_proportion: f32,28/// The (positive) distance to the near boundary of the first cascade.29pub minimum_distance: f32,30}3132impl Default for CascadeShadowConfig {33fn default() -> Self {34CascadeShadowConfigBuilder::default().into()35}36}3738fn calculate_cascade_bounds(39num_cascades: usize,40nearest_bound: f32,41shadow_maximum_distance: f32,42) -> Vec<f32> {43if num_cascades == 1 {44return vec![shadow_maximum_distance];45}46let base = ops::powf(47shadow_maximum_distance / nearest_bound,481.0 / (num_cascades - 1) as f32,49);50(0..num_cascades)51.map(|i| nearest_bound * ops::powf(base, i as f32))52.collect()53}5455/// Builder for [`CascadeShadowConfig`].56pub struct CascadeShadowConfigBuilder {57/// The number of shadow cascades.58/// More cascades increases shadow quality by mitigating perspective aliasing - a phenomenon where areas59/// nearer the camera are covered by fewer shadow map texels than areas further from the camera, causing60/// blocky looking shadows.61///62/// This does come at the cost increased rendering overhead, however this overhead is still less63/// than if you were to use fewer cascades and much larger shadow map textures to achieve the64/// same quality level.65///66/// In case rendered geometry covers a relatively narrow and static depth relative to camera, it may67/// make more sense to use fewer cascades and a higher resolution shadow map texture as perspective aliasing68/// is not as much an issue. Be sure to adjust `minimum_distance` and `maximum_distance` appropriately.69pub num_cascades: usize,70/// The minimum shadow distance, which can help improve the texel resolution of the first cascade.71/// Areas nearer to the camera than this will likely receive no shadows.72///73/// NOTE: Due to implementation details, this usually does not impact shadow quality as much as74/// `first_cascade_far_bound` and `maximum_distance`. At many view frustum field-of-views, the75/// texel resolution of the first cascade is dominated by the width / height of the view frustum plane76/// at `first_cascade_far_bound` rather than the depth of the frustum from `minimum_distance` to77/// `first_cascade_far_bound`.78pub minimum_distance: f32,79/// The maximum shadow distance.80/// Areas further from the camera than this will likely receive no shadows.81pub maximum_distance: f32,82/// Sets the far bound of the first cascade, relative to the view origin.83/// In-between cascades will be exponentially spaced relative to the maximum shadow distance.84/// NOTE: This is ignored if there is only one cascade, the maximum distance takes precedence.85pub first_cascade_far_bound: f32,86/// Sets the overlap proportion between cascades.87/// The overlap is used to make the transition from one cascade's shadow map to the next88/// less abrupt by blending between both shadow maps.89pub overlap_proportion: f32,90}9192impl CascadeShadowConfigBuilder {93/// Returns the cascade config as specified by this builder.94pub fn build(&self) -> CascadeShadowConfig {95assert!(96self.num_cascades > 0,97"num_cascades must be positive, but was {}",98self.num_cascades99);100assert!(101self.minimum_distance >= 0.0,102"maximum_distance must be non-negative, but was {}",103self.minimum_distance104);105assert!(106self.num_cascades == 1 || self.minimum_distance < self.first_cascade_far_bound,107"minimum_distance must be less than first_cascade_far_bound, but was {}",108self.minimum_distance109);110assert!(111self.maximum_distance > self.minimum_distance,112"maximum_distance must be greater than minimum_distance, but was {}",113self.maximum_distance114);115assert!(116(0.0..1.0).contains(&self.overlap_proportion),117"overlap_proportion must be in [0.0, 1.0) but was {}",118self.overlap_proportion119);120CascadeShadowConfig {121bounds: calculate_cascade_bounds(122self.num_cascades,123self.first_cascade_far_bound,124self.maximum_distance,125),126overlap_proportion: self.overlap_proportion,127minimum_distance: self.minimum_distance,128}129}130}131132impl Default for CascadeShadowConfigBuilder {133fn default() -> Self {134// The defaults are chosen to be similar to be Unity, Unreal, and Godot.135// Unity: first cascade far bound = 10.05, maximum distance = 150.0136// Unreal Engine 5: maximum distance = 200.0137// Godot: first cascade far bound = 10.0, maximum distance = 100.0138Self {139// Currently only support one cascade in WebGL 2.140num_cascades: if cfg!(all(141feature = "webgl",142target_arch = "wasm32",143not(feature = "webgpu")144)) {1451146} else {1474148},149minimum_distance: 0.1,150maximum_distance: 150.0,151first_cascade_far_bound: 10.0,152overlap_proportion: 0.2,153}154}155}156157impl From<CascadeShadowConfigBuilder> for CascadeShadowConfig {158fn from(builder: CascadeShadowConfigBuilder) -> Self {159builder.build()160}161}162163#[derive(Component, Clone, Debug, Default, Reflect)]164#[reflect(Component, Debug, Default, Clone)]165pub struct Cascades {166/// Map from a view to the configuration of each of its [`Cascade`]s.167pub cascades: EntityHashMap<Vec<Cascade>>,168}169170#[derive(Clone, Debug, Default, Reflect)]171#[reflect(Clone, Default)]172pub struct Cascade {173/// The transform of the light, i.e. the view to world matrix.174pub world_from_cascade: Mat4,175/// The orthographic projection for this cascade.176pub clip_from_cascade: Mat4,177/// The view-projection matrix for this cascade, converting world space into light clip space.178/// Importantly, this is derived and stored separately from `view_transform` and `projection` to179/// ensure shadow stability.180pub clip_from_world: Mat4,181/// Size of each shadow map texel in world units.182pub texel_size: f32,183}184185pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) {186for (directional_light, mut cascades) in lights.iter_mut() {187if !directional_light.shadows_enabled {188continue;189}190cascades.cascades.clear();191}192}193194pub 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.shadows_enabled {217continue;218}219220// It is very important to the numerical and thus visual stability of shadows that221// light_to_world has orthogonal upper-left 3x3 and zero translation.222// Even though only the direction (i.e. rotation) of the light matters, we don't constrain223// users to not change any other aspects of the transform - there's no guarantee224// `transform.to_matrix()` will give us a matrix with our desired properties.225// Instead, we directly create a good matrix from just the rotation.226let world_from_light = Mat4::from_quat(transform.rotation());227let light_to_world_inverse = world_from_light.transpose();228229for (view_entity, projection, view_to_world) in views.iter().copied() {230let camera_to_light_view = light_to_world_inverse * view_to_world;231let overlap_factor = 1.0 - cascades_config.overlap_proportion;232let far_bounds = cascades_config.bounds.iter();233let near_bounds = [cascades_config.minimum_distance]234.into_iter()235.chain(far_bounds.clone().map(|bound| overlap_factor * bound));236let view_cascades = near_bounds237.zip(far_bounds)238.map(|(near_bound, far_bound)| {239// Negate bounds as -z is camera forward direction.240let corners = projection.get_frustum_corners(-near_bound, -far_bound);241calculate_cascade(242corners,243directional_light_shadow_map.size as f32,244world_from_light,245camera_to_light_view,246)247})248.collect();249cascades.cascades.insert(view_entity, view_cascades);250}251}252}253254/// Returns a [`Cascade`] for the frustum defined by `frustum_corners`.255///256/// The corner vertices should be specified in the following order:257/// first the bottom right, top right, top left, bottom left for the near plane, then similar for the far plane.258///259/// See this [reference](https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf) for more details.260fn calculate_cascade(261frustum_corners: [Vec3A; 8],262cascade_texture_size: f32,263world_from_light: Mat4,264light_from_camera: Mat4,265) -> Cascade {266let mut min = Vec3A::splat(f32::MAX);267let mut max = Vec3A::splat(f32::MIN);268for corner_camera_view in frustum_corners {269let corner_light_view = light_from_camera.transform_point3a(corner_camera_view);270min = min.min(corner_light_view);271max = max.max(corner_light_view);272}273274// NOTE: Use the larger of the frustum slice far plane diagonal and body diagonal lengths as this275// will be the maximum possible projection size. Use the ceiling to get an integer which is276// very important for floating point stability later. It is also important that these are277// calculated using the original camera space corner positions for floating point precision278// as even though the lengths using corner_light_view above should be the same, precision can279// introduce small but significant differences.280// NOTE: The size remains the same unless the view frustum or cascade configuration is modified.281let body_diagonal = (frustum_corners[0] - frustum_corners[6]).length_squared();282let far_plane_diagonal = (frustum_corners[4] - frustum_corners[6]).length_squared();283let cascade_diameter = body_diagonal.max(far_plane_diagonal).sqrt().ceil();284285// NOTE: If we ensure that cascade_texture_size is a power of 2, then as we made cascade_diameter an286// integer, cascade_texel_size is then an integer multiple of a power of 2 and can be287// exactly represented in a floating point value.288let cascade_texel_size = cascade_diameter / cascade_texture_size;289// NOTE: For shadow stability it is very important that the near_plane_center is at integer290// multiples of the texel size to be exactly representable in a floating point value.291let near_plane_center = Vec3A::new(292(0.5 * (min.x + max.x) / cascade_texel_size).floor() * cascade_texel_size,293(0.5 * (min.y + max.y) / cascade_texel_size).floor() * cascade_texel_size,294// NOTE: max.z is the near plane for right-handed y-up295max.z,296);297298// It is critical for `cascade_from_world` to be stable. So rather than forming `world_from_cascade`299// and inverting it, which risks instability due to numerical precision, we directly form300// `cascade_from_world` as the reference material suggests.301let world_from_light_transpose = world_from_light.transpose();302let cascade_from_world = Mat4::from_cols(303world_from_light_transpose.x_axis,304world_from_light_transpose.y_axis,305world_from_light_transpose.z_axis,306(-near_plane_center).extend(1.0),307);308let world_from_cascade = Mat4::from_cols(309world_from_light.x_axis,310world_from_light.y_axis,311world_from_light.z_axis,312world_from_light * near_plane_center.extend(1.0),313);314315// Right-handed orthographic projection, centered at `near_plane_center`.316// NOTE: This is different from the reference material, as we use reverse Z.317let r = (max.z - min.z).recip();318let clip_from_cascade = Mat4::from_cols(319Vec4::new(2.0 / cascade_diameter, 0.0, 0.0, 0.0),320Vec4::new(0.0, 2.0 / cascade_diameter, 0.0, 0.0),321Vec4::new(0.0, 0.0, r, 0.0),322Vec4::new(0.0, 0.0, 1.0, 1.0),323);324325let clip_from_world = clip_from_cascade * cascade_from_world;326Cascade {327world_from_cascade,328clip_from_cascade,329clip_from_world,330texel_size: cascade_texel_size,331}332}333334335