bevy_light/
cascade.rs

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