1use crate::gfx::projection::{look_at, normalize3, ortho_rh};
26use crate::gfx::render_types::{NUM_SHADOW_CASCADES, ShadowUniforms};
27use crate::gfx::transform::IDENTITY;
28use crate::gfx::transform::mat4_mul;
29use crate::math::vec3::{add, cross, dot, scale, sub};
30use crate::math::{powf, round, sqrt, tan};
31
32const SPLIT_LAMBDA: f32 = 0.5;
33
34pub fn empty_shadow_uniforms() -> ShadowUniforms {
38 ShadowUniforms {
39 light_vps: [IDENTITY; NUM_SHADOW_CASCADES],
40 cascade_splits: [f32::INFINITY; NUM_SHADOW_CASCADES],
41 active_cascades: NUM_SHADOW_CASCADES as u32,
42 _pad: [0; 3],
43 }
44}
45
46#[derive(Clone, Copy)]
49pub struct ShadowUniformInputs {
50 pub view: [[f32; 4]; 4],
52 pub cam_pos: [f32; 3],
54 pub fov_y_rad: f32,
56 pub aspect: f32,
58 pub near: f32,
60 pub shadow_distance: f32,
62 pub light_dir_to_source: [f32; 3],
65 pub shadow_map_size: u32,
67 pub active_cascades: u32,
71}
72
73pub fn compute_shadow_uniforms(inputs: ShadowUniformInputs) -> ShadowUniforms {
75 let ShadowUniformInputs {
76 view,
77 cam_pos,
78 fov_y_rad,
79 aspect,
80 near,
81 shadow_distance,
82 light_dir_to_source,
83 shadow_map_size,
84 active_cascades,
85 } = inputs;
86 let shadow_far = shadow_distance.max(near + 1.0);
87 let active = active_cascades.clamp(1, NUM_SHADOW_CASCADES as u32) as usize;
88
89 let cascade_count = active as f32;
93 let mut splits = [-1.0_f32; NUM_SHADOW_CASCADES];
94 for (i, split) in splits.iter_mut().take(active).enumerate() {
95 let p = (i + 1) as f32 / cascade_count;
96 let log = near * powf(shadow_far / near, p);
97 let lin = near + (shadow_far - near) * p;
98 *split = SPLIT_LAMBDA * log + (1.0 - SPLIT_LAMBDA) * lin;
99 }
100
101 let l_to = normalize3(light_dir_to_source);
102
103 let right = [view[0][0], view[1][0], view[2][0]];
106 let up = [view[0][1], view[1][1], view[2][1]];
107 let forward = [-view[0][2], -view[1][2], -view[2][2]];
108
109 let tan_half_v = tan(fov_y_rad * 0.5);
110 let tan_half_h = tan_half_v * aspect;
111
112 let mut light_vps = [IDENTITY; NUM_SHADOW_CASCADES];
113 let mut prev_split = near;
114 for i in 0..active {
115 let near_d = prev_split;
116 let far_d = splits[i];
117 prev_split = far_d;
118
119 let h_near = near_d * tan_half_v;
121 let w_near = near_d * tan_half_h;
122 let h_far = far_d * tan_half_v;
123 let w_far = far_d * tan_half_h;
124 let cn = add(cam_pos, scale(forward, near_d));
125 let cf = add(cam_pos, scale(forward, far_d));
126 let corners: [[f32; 3]; 8] = [
127 add(add(cn, scale(right, -w_near)), scale(up, -h_near)),
128 add(add(cn, scale(right, w_near)), scale(up, -h_near)),
129 add(add(cn, scale(right, w_near)), scale(up, h_near)),
130 add(add(cn, scale(right, -w_near)), scale(up, h_near)),
131 add(add(cf, scale(right, -w_far)), scale(up, -h_far)),
132 add(add(cf, scale(right, w_far)), scale(up, -h_far)),
133 add(add(cf, scale(right, w_far)), scale(up, h_far)),
134 add(add(cf, scale(right, -w_far)), scale(up, h_far)),
135 ];
136
137 let mut centre = [0.0_f32; 3];
139 for c in &corners {
140 centre[0] += c[0];
141 centre[1] += c[1];
142 centre[2] += c[2];
143 }
144 centre = scale(centre, 1.0 / 8.0);
145 let mut r2 = 0.0_f32;
146 for c in &corners {
147 let d = sub(*c, centre);
148 let dd = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
149 if dd > r2 {
150 r2 = dd;
151 }
152 }
153 let radius = sqrt(r2).max(1e-3);
154
155 let up_l = if l_to[1].abs() > 0.95 {
157 [1.0_f32, 0.0, 0.0]
158 } else {
159 [0.0_f32, 1.0, 0.0]
160 };
161
162 let f = scale(l_to, -1.0);
165 let r = normalize3(cross(f, up_l));
166 let u = cross(r, f);
167
168 let texel_size = 2.0 * radius / shadow_map_size.max(1) as f32;
176 let cx = dot(r, centre);
177 let cy = dot(u, centre);
178 let snap_dx = round(cx / texel_size) * texel_size - cx;
179 let snap_dy = round(cy / texel_size) * texel_size - cy;
180 let centre = add(add(centre, scale(r, snap_dx)), scale(u, snap_dy));
181
182 let caster_extent = shadow_far;
196 let light_eye = add(centre, scale(l_to, radius));
197 let light_view = look_at(light_eye, centre, up_l);
198 let proj = ortho_rh(
199 -radius,
200 radius,
201 -radius,
202 radius,
203 -caster_extent,
204 2.0 * radius,
205 );
206 light_vps[i] = mat4_mul(proj, light_view);
207 }
208
209 ShadowUniforms {
210 light_vps,
211 cascade_splits: splits,
212 active_cascades: active as u32,
213 _pad: [0; 3],
214 }
215}
216
217#[cfg(test)]
220mod tests {
221 use super::*;
222
223 fn ident_view() -> [[f32; 4]; 4] {
224 look_at([0.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0])
226 }
227
228 #[test]
229 fn empty_uniforms_have_infinite_splits() {
230 let u = empty_shadow_uniforms();
231 for s in &u.cascade_splits {
232 assert!(s.is_infinite());
233 }
234 }
235
236 #[test]
237 fn splits_are_strictly_increasing_within_range() {
238 let u = compute_shadow_uniforms(ShadowUniformInputs {
239 view: ident_view(),
240 cam_pos: [0.0, 0.0, 0.0],
241 fov_y_rad: core::f32::consts::FRAC_PI_2,
242 aspect: 1.0,
243 near: 0.1,
244 shadow_distance: 80.0,
245 light_dir_to_source: [0.0, 1.0, 0.0],
246 shadow_map_size: 2048,
247 active_cascades: 4,
248 });
249 for i in 1..NUM_SHADOW_CASCADES {
250 assert!(
251 u.cascade_splits[i] > u.cascade_splits[i - 1],
252 "splits must increase: {:?}",
253 u.cascade_splits
254 );
255 }
256 assert!(u.cascade_splits[0] > 0.1);
257 assert!((u.cascade_splits[NUM_SHADOW_CASCADES - 1] - 80.0).abs() < 1e-3);
258 }
259
260 #[test]
261 fn fewer_active_cascades_fill_only_the_live_slots() {
262 let u = compute_shadow_uniforms(ShadowUniformInputs {
266 view: ident_view(),
267 cam_pos: [0.0, 0.0, 0.0],
268 fov_y_rad: core::f32::consts::FRAC_PI_2,
269 aspect: 1.0,
270 near: 0.1,
271 shadow_distance: 80.0,
272 light_dir_to_source: [0.0, 1.0, 0.0],
273 shadow_map_size: 2048,
274 active_cascades: 2,
275 });
276 assert_eq!(u.active_cascades, 2);
277 assert!(u.cascade_splits[0] > 0.1);
278 assert!(u.cascade_splits[1] > u.cascade_splits[0]);
279 assert!((u.cascade_splits[1] - 80.0).abs() < 1e-3);
280 assert!(u.cascade_splits[2] < 0.0);
282 assert!(u.cascade_splits[3] < 0.0);
283 assert_eq!(u.light_vps[2], IDENTITY);
284 assert_eq!(u.light_vps[3], IDENTITY);
285 let one = compute_shadow_uniforms(ShadowUniformInputs {
287 view: ident_view(),
288 cam_pos: [0.0, 0.0, 0.0],
289 fov_y_rad: core::f32::consts::FRAC_PI_2,
290 aspect: 1.0,
291 near: 0.1,
292 shadow_distance: 80.0,
293 light_dir_to_source: [0.0, 1.0, 0.0],
294 shadow_map_size: 2048,
295 active_cascades: 0,
296 });
297 assert_eq!(one.active_cascades, 1);
298 assert!((one.cascade_splits[0] - 80.0).abs() < 1e-3);
299 }
300
301 #[test]
302 fn near_clamped_to_avoid_degenerate_log() {
303 let u = compute_shadow_uniforms(ShadowUniformInputs {
306 view: ident_view(),
307 cam_pos: [0.0, 0.0, 0.0],
308 fov_y_rad: core::f32::consts::FRAC_PI_2,
309 aspect: 1.0,
310 near: 5.0,
311 shadow_distance: 1.0,
312 light_dir_to_source: [0.0, 1.0, 0.0],
313 shadow_map_size: 2048,
314 active_cascades: 4,
315 });
316 for s in &u.cascade_splits {
317 assert!(s.is_finite() && *s > 0.0);
318 }
319 }
320
321 #[test]
322 fn cascade_vps_finite_for_typical_inputs() {
323 let u = compute_shadow_uniforms(ShadowUniformInputs {
324 view: ident_view(),
325 cam_pos: [10.0, 5.0, -3.0],
326 fov_y_rad: core::f32::consts::FRAC_PI_4,
327 aspect: 16.0 / 9.0,
328 near: 0.1,
329 shadow_distance: 80.0,
330 light_dir_to_source: [-0.4, 0.7, 0.3],
331 shadow_map_size: 2048,
332 active_cascades: 4,
333 });
334 for vp in &u.light_vps {
335 for col in vp {
336 for v in col {
337 assert!(v.is_finite(), "non-finite element in light_vp");
338 }
339 }
340 }
341 }
342
343 #[test]
344 fn point_inside_first_cascade_projects_into_unit_box() {
345 let u = compute_shadow_uniforms(ShadowUniformInputs {
348 view: ident_view(),
349 cam_pos: [0.0, 0.0, 0.0],
350 fov_y_rad: core::f32::consts::FRAC_PI_4,
351 aspect: 16.0 / 9.0,
352 near: 0.1,
353 shadow_distance: 80.0,
354 light_dir_to_source: [0.0, 1.0, 0.0],
355 shadow_map_size: 2048,
356 active_cascades: 4,
357 });
358 let p = [0.0_f32, 0.0, -2.0, 1.0];
360 let vp = u.light_vps[0];
361 let mut clip = [0.0_f32; 4];
362 for row in 0..4 {
363 clip[row] =
364 vp[0][row] * p[0] + vp[1][row] * p[1] + vp[2][row] * p[2] + vp[3][row] * p[3];
365 }
366 let ndc = [clip[0] / clip[3], clip[1] / clip[3], clip[2] / clip[3]];
367 assert!(ndc[0].abs() <= 1.0, "x out of range: {}", ndc[0]);
368 assert!(ndc[1].abs() <= 1.0, "y out of range: {}", ndc[1]);
369 assert!(
370 ndc[2] >= -0.05 && ndc[2] <= 1.05,
371 "depth out of range: {}",
372 ndc[2]
373 );
374 }
375
376 fn cascade0_texel(cam: [f32; 3], world_p: [f32; 3], light: [f32; 3], size: u32) -> (f32, f32) {
379 let view = look_at(cam, [cam[0], cam[1], cam[2] - 1.0], [0.0, 1.0, 0.0]);
380 let u = compute_shadow_uniforms(ShadowUniformInputs {
381 view,
382 cam_pos: cam,
383 fov_y_rad: core::f32::consts::FRAC_PI_4,
384 aspect: 16.0 / 9.0,
385 near: 0.1,
386 shadow_distance: 80.0,
387 light_dir_to_source: light,
388 shadow_map_size: size,
389 active_cascades: 4,
390 });
391 let vp = u.light_vps[0];
392 let p = [world_p[0], world_p[1], world_p[2], 1.0];
393 let mut clip = [0.0_f32; 4];
394 for row in 0..4 {
395 clip[row] =
396 vp[0][row] * p[0] + vp[1][row] * p[1] + vp[2][row] * p[2] + vp[3][row] * p[3];
397 }
398 let uvx = (clip[0] / clip[3] * 0.5 + 0.5) * size as f32;
399 let uvy = (-clip[1] / clip[3] * 0.5 + 0.5) * size as f32;
400 (uvx, uvy)
401 }
402
403 #[test]
404 fn texels_do_not_crawl_under_camera_translation() {
405 let light = [-0.4, 0.78, 0.5];
413 let size = 2048u32;
414 let world_p = [3.0_f32, 0.0, -5.0];
415
416 let a = cascade0_texel([0.0, 2.0, 0.0], world_p, light, size);
418 let b = cascade0_texel([0.137, 2.0, 0.091], world_p, light, size);
419
420 let dx = a.0 - b.0;
423 let dy = a.1 - b.1;
424 let rx = dx - dx.round();
425 let ry = dy - dy.round();
426 assert!(
427 rx.abs() < 0.05,
428 "shadow x crawls within a texel: residual {rx}"
429 );
430 assert!(
431 ry.abs() < 0.05,
432 "shadow y crawls within a texel: residual {ry}"
433 );
434 }
435
436 fn cascade0_ndc(cam: [f32; 3], world_p: [f32; 3], light: [f32; 3]) -> [f32; 3] {
438 let view = look_at(cam, [cam[0], cam[1], cam[2] - 1.0], [0.0, 1.0, 0.0]);
439 let u = compute_shadow_uniforms(ShadowUniformInputs {
440 view,
441 cam_pos: cam,
442 fov_y_rad: core::f32::consts::FRAC_PI_4,
443 aspect: 16.0 / 9.0,
444 near: 0.1,
445 shadow_distance: 80.0,
446 light_dir_to_source: light,
447 shadow_map_size: 2048,
448 active_cascades: 4,
449 });
450 let vp = u.light_vps[0];
451 let p = [world_p[0], world_p[1], world_p[2], 1.0];
452 let mut clip = [0.0_f32; 4];
453 for row in 0..4 {
454 clip[row] =
455 vp[0][row] * p[0] + vp[1][row] * p[1] + vp[2][row] * p[2] + vp[3][row] * p[3];
456 }
457 [clip[0] / clip[3], clip[1] / clip[3], clip[2] / clip[3]]
458 }
459
460 #[test]
461 fn tall_casters_above_cascade_are_not_clipped() {
462 let cam = [0.0_f32, 0.0, 0.0];
471 let light = [0.0_f32, 0.85, 0.3]; let ndc = cascade0_ndc(cam, [0.0, 30.0, -3.0], light);
473 assert!(
474 ndc[0].abs() <= 1.0,
475 "caster x outside footprint: {}",
476 ndc[0]
477 );
478 assert!(
479 ndc[1].abs() <= 1.0,
480 "caster y outside footprint: {}",
481 ndc[1]
482 );
483 assert!(
484 (0.0..=1.0).contains(&ndc[2]),
485 "tall caster clipped from shadow map: ndc.z = {}",
486 ndc[2]
487 );
488 }
489}