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