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