1use crate::components::{SpotLight, SpotLightGeometry};
15use crate::gfx::projection::{look_at, perspective_rh, up_for};
16use crate::gfx::render_types::{MAX_SHADOWED_SPOTS, SpotShadowData};
17use crate::gfx::transform::mat4_mul;
18use crate::math::vec3::{add, scale};
19use alloc::vec;
20use alloc::vec::Vec;
21
22const SHADOW_NEAR: f32 = 0.05;
26
27const DEPTH_BIAS: f32 = 0.0015;
31const NORMAL_BIAS: f32 = 0.035;
32
33const MIN_SHADOW_RANGE: f32 = 0.1;
36
37pub(crate) fn assign_spot_shadow_slices(spot_lights: &[SpotLight]) -> Vec<i32> {
41 let mut next = 0_i32;
42 let mut wanted = 0_usize;
43 let slices: Vec<i32> = spot_lights
44 .iter()
45 .map(|l| {
46 if !l.cast_shadows {
47 return -1;
48 }
49 wanted += 1;
50 if (next as usize) < MAX_SHADOWED_SPOTS {
51 let slice = next;
52 next += 1;
53 slice
54 } else {
55 -1
56 }
57 })
58 .collect();
59 slices
60}
61
62pub(crate) fn build_spot_shadow_data(
66 spot_lights: &[SpotLight],
67 slices: &[i32],
68) -> Vec<SpotShadowData> {
69 let mut out = vec![SpotShadowData::ZERO; count_shadowed(slices)];
70 for (light, &slice) in spot_lights.iter().zip(slices) {
71 if slice >= 0 {
72 out[slice as usize] = spot_shadow_data(light);
73 }
74 }
75 out
76}
77
78pub(crate) fn count_shadowed(slices: &[i32]) -> usize {
80 slices.iter().filter(|s| **s >= 0).count()
81}
82
83fn spot_shadow_data(light: &SpotLight) -> SpotShadowData {
87 let dir = light.unit_direction();
88 let far = light.range.max(MIN_SHADOW_RANGE);
89 let view = look_at(
90 light.position,
91 add(light.position, scale(dir, far)),
92 up_for(dir),
93 );
94 let fov = (2.0 * light.outer_angle).to_radians();
95 let proj = perspective_rh(fov, 1.0, SHADOW_NEAR, far);
96 SpotShadowData {
97 light_vp: mat4_mul(proj, view),
98 depth_bias: DEPTH_BIAS,
99 normal_bias: NORMAL_BIAS,
100 _pad: [0.0; 2],
101 }
102}
103
104#[derive(Debug, Default)]
109pub struct SpotShadowScheduler {
110 clock: u32,
111 primed: u32,
112}
113
114impl SpotShadowScheduler {
115 pub fn next_mask(&mut self, every_frame: bool, shadowed: usize) -> u32 {
117 let (mask, primed) = select_slice_mask(every_frame, self.clock, self.primed, shadowed);
118 self.clock = self.clock.wrapping_add(1);
119 self.primed = primed;
120 mask
121 }
122}
123
124fn select_slice_mask(every_frame: bool, clock: u32, primed: u32, shadowed: usize) -> (u32, u32) {
128 let shadowed = shadowed.min(MAX_SHADOWED_SPOTS);
129 if shadowed == 0 {
130 return (0, primed);
131 }
132 let all = if shadowed >= 32 {
133 u32::MAX
134 } else {
135 (1_u32 << shadowed) - 1
136 };
137 let scheduled = if every_frame {
138 all
139 } else {
140 1_u32 << (clock as usize % shadowed)
141 };
142 let unprimed = all & !primed;
143 let mask = (scheduled | unprimed) & all;
144 (mask, primed | mask)
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 fn spot(cast: bool) -> SpotLight {
152 SpotLight {
153 cast_shadows: cast,
154 ..SpotLight::default()
155 }
156 }
157
158 fn transform(m: [[f32; 4]; 4], p: [f32; 3]) -> [f32; 4] {
159 let mut out = [0.0_f32; 4];
160 for row in 0..4 {
161 out[row] = m[0][row] * p[0] + m[1][row] * p[1] + m[2][row] * p[2] + m[3][row];
162 }
163 out
164 }
165
166 #[test]
167 fn slices_are_handed_out_in_declaration_order() {
168 let lights = vec![spot(true), spot(true), spot(true)];
169 assert_eq!(assign_spot_shadow_slices(&lights), vec![0, 1, 2]);
170 }
171
172 #[test]
174 fn non_casting_spots_are_skipped_without_consuming_a_slice() {
175 let lights = vec![spot(true), spot(false), spot(true)];
176 assert_eq!(assign_spot_shadow_slices(&lights), vec![0, -1, 1]);
177 }
178
179 #[test]
180 fn slices_past_the_cap_get_no_shadow() {
181 let lights: Vec<SpotLight> = (0..MAX_SHADOWED_SPOTS + 3).map(|_| spot(true)).collect();
182 let slices = assign_spot_shadow_slices(&lights);
183 assert_eq!(count_shadowed(&slices), MAX_SHADOWED_SPOTS);
184 assert_eq!(
185 slices[MAX_SHADOWED_SPOTS - 1],
186 MAX_SHADOWED_SPOTS as i32 - 1
187 );
188 assert!(slices[MAX_SHADOWED_SPOTS..].iter().all(|s| *s == -1));
189 }
190
191 #[test]
192 fn shadow_data_is_indexed_by_slice_not_by_light() {
193 let mut a = spot(false);
194 a.range = 5.0;
195 let mut b = spot(true);
196 b.range = 33.0;
197 let lights = vec![a, b];
198 let slices = assign_spot_shadow_slices(&lights);
199 assert_eq!(slices, vec![-1, 0]);
200 let data = build_spot_shadow_data(&lights, &slices);
201 assert_eq!(data.len(), 1);
203 let clip = transform(data[0].light_vp, [0.0, 4.0 - 32.0, 0.0]);
205 assert!(clip[3] > 0.0);
206 assert!((clip[2] / clip[3]) < 1.0);
207 }
208
209 #[test]
212 fn the_cone_inscribes_the_shadow_frustum() {
213 let mut l = spot(true);
214 l.position = [0.0, 10.0, 0.0];
215 l.direction = [0.0, -1.0, 0.0];
216 l.outer_angle = 30.0;
217 l.range = 20.0;
218 let d = spot_shadow_data(&l);
219
220 let centre = transform(d.light_vp, [0.0, 0.0, 0.0]);
222 assert!((centre[0] / centre[3]).abs() < 1e-4);
223 assert!((centre[1] / centre[3]).abs() < 1e-4);
224
225 let edge_x = 30.0_f32.to_radians().tan() * 10.0;
227 let edge = transform(d.light_vp, [edge_x, 0.0, 0.0]);
228 assert!(((edge[0] / edge[3]).abs() - 1.0).abs() < 1e-3);
229 }
230
231 #[test]
234 fn a_straight_down_cone_produces_a_finite_matrix() {
235 let mut l = spot(true);
236 l.direction = [0.0, -1.0, 0.0];
237 let d = spot_shadow_data(&l);
238 assert!(d.light_vp.iter().flatten().all(|v| v.is_finite()));
239 }
240
241 #[test]
243 fn a_degenerate_range_still_produces_a_finite_matrix() {
244 let mut l = spot(true);
245 l.range = 0.0;
246 let d = spot_shadow_data(&l);
247 assert!(d.light_vp.iter().flatten().all(|v| v.is_finite()));
248 }
249
250 #[test]
251 fn no_shadowed_spots_renders_nothing() {
252 let mut s = SpotShadowScheduler::default();
253 assert_eq!(s.next_mask(false, 0), 0);
254 }
255
256 #[test]
259 fn all_slices_prime_on_the_first_frame() {
260 let mut s = SpotShadowScheduler::default();
261 assert_eq!(s.next_mask(false, 4), 0b1111);
262 }
263
264 #[test]
265 fn hybrid_settles_into_one_slice_per_frame() {
266 let mut s = SpotShadowScheduler::default();
267 s.next_mask(false, 4);
268 assert_eq!(s.next_mask(false, 4), 0b0010);
269 assert_eq!(s.next_mask(false, 4), 0b0100);
270 assert_eq!(s.next_mask(false, 4), 0b1000);
271 assert_eq!(s.next_mask(false, 4), 0b0001);
272 }
273
274 #[test]
275 fn every_frame_refreshes_all_slices() {
276 let mut s = SpotShadowScheduler::default();
277 s.next_mask(true, 3);
278 assert_eq!(s.next_mask(true, 3), 0b111);
279 }
280
281 #[test]
284 fn a_newly_appearing_slice_is_primed_immediately() {
285 let mut s = SpotShadowScheduler::default();
286 s.next_mask(false, 2);
287 let mask = s.next_mask(false, 4);
288 assert!(mask & 0b1100 == 0b1100, "the two new slices prime at once");
289 }
290
291 #[test]
292 fn the_mask_never_exceeds_the_shadowed_count() {
293 let mut s = SpotShadowScheduler::default();
294 for _ in 0..40 {
295 assert_eq!(s.next_mask(false, 3) & !0b111, 0);
296 }
297 }
298}