Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_light/src/cascade.rs
6601 views
1
use bevy_camera::{Camera, Projection};
2
use bevy_ecs::{entity::EntityHashMap, prelude::*};
3
use bevy_math::{ops, Mat4, Vec3A, Vec4};
4
use bevy_reflect::prelude::*;
5
use bevy_transform::components::GlobalTransform;
6
7
use 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)]
24
pub 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
33
impl Default for CascadeShadowConfig {
34
fn default() -> Self {
35
CascadeShadowConfigBuilder::default().into()
36
}
37
}
38
39
fn 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`].
57
pub 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
93
impl 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
133
impl 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
158
impl 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)]
166
pub 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)]
173
pub 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
186
pub 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
195
pub 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.
261
fn 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
}
334
335