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 if wanted > MAX_SHADOWED_SPOTS {
61 tracing::warn!(
62 "GraphicsSystem: {} spot lights request shadows; only {} slices exist -- the rest light without casting",
63 wanted,
64 MAX_SHADOWED_SPOTS
65 );
66 }
67 slices
68}
69
70pub(crate) fn build_spot_shadow_data(
74 spot_lights: &[SpotLight],
75 slices: &[i32],
76) -> Vec<SpotShadowData> {
77 let mut out = vec![SpotShadowData::ZERO; count_shadowed(slices)];
78 for (light, &slice) in spot_lights.iter().zip(slices) {
79 if slice >= 0 {
80 out[slice as usize] = spot_shadow_data(light);
81 }
82 }
83 out
84}
85
86pub(crate) fn count_shadowed(slices: &[i32]) -> usize {
88 slices.iter().filter(|s| **s >= 0).count()
89}
90
91fn spot_shadow_data(light: &SpotLight) -> SpotShadowData {
95 let dir = light.unit_direction();
96 let far = light.range.max(MIN_SHADOW_RANGE);
97 let view = look_at(
98 light.position,
99 add(light.position, scale(dir, far)),
100 up_for(dir),
101 );
102 let fov = (2.0 * light.outer_angle).to_radians();
103 let proj = perspective_rh(fov, 1.0, SHADOW_NEAR, far);
104 SpotShadowData {
105 light_vp: mat4_mul(proj, view),
106 depth_bias: DEPTH_BIAS,
107 normal_bias: NORMAL_BIAS,
108 _pad: [0.0; 2],
109 }
110}
111
112#[derive(Debug, Default)]
117pub struct SpotShadowScheduler {
118 clock: u32,
119 primed: u32,
120}
121
122impl SpotShadowScheduler {
123 pub fn next_mask(&mut self, every_frame: bool, shadowed: usize) -> u32 {
125 let (mask, primed) = select_slice_mask(every_frame, self.clock, self.primed, shadowed);
126 self.clock = self.clock.wrapping_add(1);
127 self.primed = primed;
128 mask
129 }
130}
131
132fn select_slice_mask(every_frame: bool, clock: u32, primed: u32, shadowed: usize) -> (u32, u32) {
136 let shadowed = shadowed.min(MAX_SHADOWED_SPOTS);
137 if shadowed == 0 {
138 return (0, primed);
139 }
140 let all = if shadowed >= 32 {
141 u32::MAX
142 } else {
143 (1_u32 << shadowed) - 1
144 };
145 let scheduled = if every_frame {
146 all
147 } else {
148 1_u32 << (clock as usize % shadowed)
149 };
150 let unprimed = all & !primed;
151 let mask = (scheduled | unprimed) & all;
152 (mask, primed | mask)
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn spot(cast: bool) -> SpotLight {
160 SpotLight {
161 cast_shadows: cast,
162 ..SpotLight::default()
163 }
164 }
165
166 fn transform(m: [[f32; 4]; 4], p: [f32; 3]) -> [f32; 4] {
167 let mut out = [0.0_f32; 4];
168 for row in 0..4 {
169 out[row] = m[0][row] * p[0] + m[1][row] * p[1] + m[2][row] * p[2] + m[3][row];
170 }
171 out
172 }
173
174 #[test]
175 fn slices_are_handed_out_in_declaration_order() {
176 let lights = vec![spot(true), spot(true), spot(true)];
177 assert_eq!(assign_spot_shadow_slices(&lights), vec![0, 1, 2]);
178 }
179
180 #[test]
182 fn non_casting_spots_are_skipped_without_consuming_a_slice() {
183 let lights = vec![spot(true), spot(false), spot(true)];
184 assert_eq!(assign_spot_shadow_slices(&lights), vec![0, -1, 1]);
185 }
186
187 #[test]
188 fn slices_past_the_cap_get_no_shadow() {
189 let lights: Vec<SpotLight> = (0..MAX_SHADOWED_SPOTS + 3).map(|_| spot(true)).collect();
190 let slices = assign_spot_shadow_slices(&lights);
191 assert_eq!(count_shadowed(&slices), MAX_SHADOWED_SPOTS);
192 assert_eq!(
193 slices[MAX_SHADOWED_SPOTS - 1],
194 MAX_SHADOWED_SPOTS as i32 - 1
195 );
196 assert!(slices[MAX_SHADOWED_SPOTS..].iter().all(|s| *s == -1));
197 }
198
199 #[test]
200 fn shadow_data_is_indexed_by_slice_not_by_light() {
201 let mut a = spot(false);
202 a.range = 5.0;
203 let mut b = spot(true);
204 b.range = 33.0;
205 let lights = vec![a, b];
206 let slices = assign_spot_shadow_slices(&lights);
207 assert_eq!(slices, vec![-1, 0]);
208 let data = build_spot_shadow_data(&lights, &slices);
209 assert_eq!(data.len(), 1);
211 let clip = transform(data[0].light_vp, [0.0, 4.0 - 32.0, 0.0]);
213 assert!(clip[3] > 0.0);
214 assert!((clip[2] / clip[3]) < 1.0);
215 }
216
217 #[test]
220 fn the_cone_inscribes_the_shadow_frustum() {
221 let mut l = spot(true);
222 l.position = [0.0, 10.0, 0.0];
223 l.direction = [0.0, -1.0, 0.0];
224 l.outer_angle = 30.0;
225 l.range = 20.0;
226 let d = spot_shadow_data(&l);
227
228 let centre = transform(d.light_vp, [0.0, 0.0, 0.0]);
230 assert!((centre[0] / centre[3]).abs() < 1e-4);
231 assert!((centre[1] / centre[3]).abs() < 1e-4);
232
233 let edge_x = 30.0_f32.to_radians().tan() * 10.0;
235 let edge = transform(d.light_vp, [edge_x, 0.0, 0.0]);
236 assert!(((edge[0] / edge[3]).abs() - 1.0).abs() < 1e-3);
237 }
238
239 #[test]
242 fn a_straight_down_cone_produces_a_finite_matrix() {
243 let mut l = spot(true);
244 l.direction = [0.0, -1.0, 0.0];
245 let d = spot_shadow_data(&l);
246 assert!(d.light_vp.iter().flatten().all(|v| v.is_finite()));
247 }
248
249 #[test]
251 fn a_degenerate_range_still_produces_a_finite_matrix() {
252 let mut l = spot(true);
253 l.range = 0.0;
254 let d = spot_shadow_data(&l);
255 assert!(d.light_vp.iter().flatten().all(|v| v.is_finite()));
256 }
257
258 #[test]
259 fn no_shadowed_spots_renders_nothing() {
260 let mut s = SpotShadowScheduler::default();
261 assert_eq!(s.next_mask(false, 0), 0);
262 }
263
264 #[test]
267 fn all_slices_prime_on_the_first_frame() {
268 let mut s = SpotShadowScheduler::default();
269 assert_eq!(s.next_mask(false, 4), 0b1111);
270 }
271
272 #[test]
273 fn hybrid_settles_into_one_slice_per_frame() {
274 let mut s = SpotShadowScheduler::default();
275 s.next_mask(false, 4);
276 assert_eq!(s.next_mask(false, 4), 0b0010);
277 assert_eq!(s.next_mask(false, 4), 0b0100);
278 assert_eq!(s.next_mask(false, 4), 0b1000);
279 assert_eq!(s.next_mask(false, 4), 0b0001);
280 }
281
282 #[test]
283 fn every_frame_refreshes_all_slices() {
284 let mut s = SpotShadowScheduler::default();
285 s.next_mask(true, 3);
286 assert_eq!(s.next_mask(true, 3), 0b111);
287 }
288
289 #[test]
292 fn a_newly_appearing_slice_is_primed_immediately() {
293 let mut s = SpotShadowScheduler::default();
294 s.next_mask(false, 2);
295 let mask = s.next_mask(false, 4);
296 assert!(mask & 0b1100 == 0b1100, "the two new slices prime at once");
297 }
298
299 #[test]
300 fn the_mask_never_exceeds_the_shadowed_count() {
301 let mut s = SpotShadowScheduler::default();
302 for _ in 0..40 {
303 assert_eq!(s.next_mask(false, 3) & !0b111, 0);
304 }
305 }
306}