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