1use crate::area_light;
9use crate::components::{
10 DirectionalLight, PointLight, RectAreaLight, SpotLight, SpotLightGeometry,
11};
12use crate::render_types::{
13 AreaLightData, DirectionalLightData, GpuLight, LIGHT_KIND_AREA, LIGHT_KIND_POINT,
14 LIGHT_KIND_SPOT, LightUniforms, MAX_DIRECTIONAL_LIGHTS, MAX_LOCAL_LIGHTS, MAX_POINT_LIGHTS,
15 PointLightData, SpotShadowData,
16};
17use crate::spot_shadow;
18use alloc::vec::Vec;
19
20pub struct LightData {
26 pub lights: Vec<GpuLight>,
28 pub spot_shadows: Vec<SpotShadowData>,
30 pub area_lights: Vec<AreaLightData>,
32}
33
34pub fn build_light_data(
40 pt_lights: &[PointLight],
41 spot_lights: &[SpotLight],
42 rect_lights: &[RectAreaLight],
43) -> LightData {
44 let total = pt_lights.len() + spot_lights.len() + rect_lights.len();
45 if total > MAX_LOCAL_LIGHTS {
46 tracing::warn!(
47 "GraphicsSystem: {} local lights declared; only {} are supported -- extras ignored",
48 total,
49 MAX_LOCAL_LIGHTS
50 );
51 }
52 let slices = spot_shadow::assign_spot_shadow_slices(spot_lights);
53 let spot_shadows = spot_shadow::build_spot_shadow_data(spot_lights, &slices);
54 let slots = area_light::assign_area_light_slots(rect_lights);
55 let area_lights = area_light::build_area_light_data(rect_lights, &slots);
56
57 let points = pt_lights.iter().map(|l| GpuLight {
58 position: l.position,
59 range: l.range,
60 color: l.color,
61 intensity: l.intensity,
62 kind: LIGHT_KIND_POINT,
63 ..GpuLight::ZERO
64 });
65 let spots = spot_lights
66 .iter()
67 .zip(&slices)
68 .map(|(l, &shadow_index)| GpuLight {
69 position: l.position,
70 range: l.range,
71 color: l.color,
72 intensity: l.intensity,
73 direction: l.unit_direction(),
74 kind: LIGHT_KIND_SPOT,
75 cos_inner: l.cos_inner(),
76 cos_outer: l.cos_outer(),
77 shadow_index,
78 ..GpuLight::ZERO
79 });
80 let areas = rect_lights
81 .iter()
82 .zip(&slots)
83 .map(|(l, &data_index)| GpuLight {
84 position: l.centre,
85 range: l.range,
86 color: l.color,
87 intensity: l.intensity,
88 direction: l.normal,
89 kind: LIGHT_KIND_AREA,
90 data_index,
91 ..GpuLight::ZERO
92 });
93 let lights: Vec<GpuLight> = points
94 .chain(spots)
95 .chain(areas)
96 .take(MAX_LOCAL_LIGHTS)
97 .collect();
98
99 let kept = lights.iter().filter(|l| l.shadow_index >= 0).count();
102 let spot_shadows = if kept < spot_shadows.len() {
103 spot_shadows[..kept].to_vec()
104 } else {
105 spot_shadows
106 };
107
108 let kept_areas = lights.iter().filter(|l| l.data_index >= 0).count();
111 let area_lights = if kept_areas < area_lights.len() {
112 area_lights[..kept_areas].to_vec()
113 } else {
114 area_lights
115 };
116
117 LightData {
118 lights,
119 spot_shadows,
120 area_lights,
121 }
122}
123
124const ZERO_DIR: DirectionalLightData = DirectionalLightData {
125 direction: [0.0; 3],
126 intensity: 0.0,
127 color: [0.0; 3],
128 _pad: 0.0,
129};
130const ZERO_PT: PointLightData = PointLightData {
131 position: [0.0; 3],
132 range: 0.0,
133 color: [0.0; 3],
134 intensity: 0.0,
135};
136
137pub fn directional_light_data(
142 lights: &[DirectionalLight],
143) -> ([DirectionalLightData; MAX_DIRECTIONAL_LIGHTS], i32) {
144 if lights.len() > MAX_DIRECTIONAL_LIGHTS {
145 tracing::warn!(
146 "GraphicsSystem: {} directional lights declared; only {} are supported -- extras ignored",
147 lights.len(),
148 MAX_DIRECTIONAL_LIGHTS
149 );
150 }
151 let mut directional = [ZERO_DIR; MAX_DIRECTIONAL_LIGHTS];
152 for (slot, l) in directional.iter_mut().zip(lights) {
153 *slot = DirectionalLightData {
154 direction: l.direction,
155 intensity: l.intensity,
156 color: l.color,
157 _pad: 0.0,
158 };
159 }
160 (directional, lights.len().min(MAX_DIRECTIONAL_LIGHTS) as i32)
161}
162
163pub fn sun_direction(uniforms: &LightUniforms) -> [f32; 3] {
168 if uniforms.num_directional > 0 {
169 uniforms.directional[0].direction
170 } else {
171 LightUniforms::DEFAULT.directional[0].direction
172 }
173}
174
175pub fn sun_color(uniforms: &LightUniforms) -> [f32; 3] {
178 if uniforms.num_directional > 0 {
179 let l = &uniforms.directional[0];
180 [
181 l.color[0] * l.intensity,
182 l.color[1] * l.intensity,
183 l.color[2] * l.intensity,
184 ]
185 } else {
186 [1.0, 1.0, 1.0]
187 }
188}
189
190pub fn build_light_uniforms(
193 dir_lights: Vec<DirectionalLight>,
194 pt_lights: Vec<PointLight>,
195 local_lights: &[GpuLight],
196 ambient_intensity: f32,
197) -> LightUniforms {
198 if dir_lights.is_empty() && pt_lights.is_empty() && local_lights.is_empty() {
199 return LightUniforms {
200 ambient_intensity,
201 ..LightUniforms::DEFAULT
202 };
203 }
204
205 let (directional, num_directional) = directional_light_data(&dir_lights);
206 let mut point = [ZERO_PT; MAX_POINT_LIGHTS];
210 let num_point = pt_lights.len().min(MAX_POINT_LIGHTS);
211 for (i, l) in pt_lights.into_iter().take(MAX_POINT_LIGHTS).enumerate() {
212 point[i] = PointLightData {
213 position: l.position,
214 range: l.range,
215 color: l.color,
216 intensity: l.intensity,
217 };
218 }
219
220 LightUniforms {
221 directional,
222 point,
223 num_directional,
224 num_point: num_point as i32,
225 ambient_intensity,
226 num_local_lights: local_lights.len() as i32,
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 use alloc::vec;
235 fn dir(direction: [f32; 3], color: [f32; 3], intensity: f32) -> DirectionalLight {
236 DirectionalLight {
237 direction,
238 color,
239 intensity,
240 }
241 }
242
243 fn pt(position: [f32; 3], color: [f32; 3], intensity: f32, range: f32) -> PointLight {
244 PointLight {
245 position,
246 color,
247 intensity,
248 range,
249 }
250 }
251
252 fn spot(position: [f32; 3], direction: [f32; 3], inner: f32, outer: f32) -> SpotLight {
253 SpotLight {
254 position,
255 direction,
256 inner_angle: inner,
257 outer_angle: outer,
258 ..SpotLight::default()
259 }
260 }
261
262 fn uniforms(dir_lights: Vec<DirectionalLight>, pt_lights: Vec<PointLight>) -> LightUniforms {
264 let local = build_light_data(&pt_lights, &[], &[]).lights;
265 build_light_uniforms(dir_lights, pt_lights, &local, 1.0)
266 }
267
268 #[test]
271 fn directional_packing_matches_the_init_build() {
272 let lights = vec![
273 dir([-0.3, 0.85, 0.4], [1.0, 0.95, 0.8], 1.5),
274 dir([0.1, -1.0, 0.0], [0.2, 0.3, 0.4], 0.5),
275 ];
276 let (packed, count) = directional_light_data(&lights);
277 let built = uniforms(lights, vec![]);
278 assert_eq!(count, built.num_directional);
279 assert_eq!(packed, built.directional);
280 }
281
282 #[test]
285 fn directional_packing_zeroes_the_unused_slots() {
286 let (packed, count) = directional_light_data(&[dir([0.0, 1.0, 0.0], [1.0; 3], 2.0)]);
287 assert_eq!(count, 1);
288 assert!(packed[1..].iter().all(|d| *d == ZERO_DIR));
289 let (empty, none) = directional_light_data(&[]);
290 assert_eq!(none, 0);
291 assert!(empty.iter().all(|d| *d == ZERO_DIR));
292 }
293
294 #[test]
296 fn directional_packing_clamps_to_the_array_capacity() {
297 let many: Vec<DirectionalLight> = (0..MAX_DIRECTIONAL_LIGHTS + 2)
298 .map(|i| dir([0.0, 1.0, 0.0], [1.0; 3], i as f32))
299 .collect();
300 let (packed, count) = directional_light_data(&many);
301 assert_eq!(count, MAX_DIRECTIONAL_LIGHTS as i32);
302 assert_eq!(packed.len(), MAX_DIRECTIONAL_LIGHTS);
303 }
304
305 #[test]
306 fn empty_inputs_return_default() {
307 let u = uniforms(vec![], vec![]);
308 assert_eq!(u.num_directional, LightUniforms::DEFAULT.num_directional);
309 assert_eq!(u.num_point, LightUniforms::DEFAULT.num_point);
310 }
311
312 #[test]
313 fn ambient_intensity_carried_in_both_branches() {
314 let empty = build_light_uniforms(vec![], vec![], &[], 2.5);
317 assert!((empty.ambient_intensity - 2.5).abs() < 1e-6);
318 let populated = build_light_uniforms(
319 vec![dir([-0.3, 0.85, 0.4], [1.0; 3], 1.0)],
320 vec![],
321 &[],
322 3.0,
323 );
324 assert!((populated.ambient_intensity - 3.0).abs() < 1e-6);
325 }
326
327 #[test]
328 fn single_directional_light_fields_mapped() {
329 let u = uniforms(vec![dir([-0.3, 0.85, 0.4], [1.0, 0.95, 0.8], 1.5)], vec![]);
330 assert_eq!(u.num_directional, 1);
331 assert_eq!(u.num_point, 0);
332 assert_eq!(u.directional[0].direction, [-0.3, 0.85, 0.4]);
333 assert_eq!(u.directional[0].color, [1.0, 0.95, 0.8]);
334 assert!((u.directional[0].intensity - 1.5).abs() < 1e-6);
335 }
336
337 #[test]
338 fn single_point_light_fields_mapped() {
339 let u = uniforms(vec![], vec![pt([2.0, 3.0, 4.0], [1.0, 0.8, 0.5], 8.0, 6.0)]);
340 assert_eq!(u.num_directional, 0);
341 assert_eq!(u.num_point, 1);
342 assert_eq!(u.point[0].position, [2.0, 3.0, 4.0]);
343 assert_eq!(u.point[0].color, [1.0, 0.8, 0.5]);
344 assert!((u.point[0].intensity - 8.0).abs() < 1e-6);
345 assert!((u.point[0].range - 6.0).abs() < 1e-6);
346 }
347
348 #[test]
349 fn excess_directional_lights_clamped_to_max() {
350 let lights: Vec<DirectionalLight> = (0..MAX_DIRECTIONAL_LIGHTS + 2)
351 .map(|i| dir([i as f32, 0.0, 0.0], [1.0; 3], 1.0))
352 .collect();
353 let u = uniforms(lights, vec![]);
354 assert_eq!(u.num_directional, MAX_DIRECTIONAL_LIGHTS as i32);
355 }
356
357 #[test]
358 fn excess_point_lights_clamped_to_max() {
359 let lights: Vec<PointLight> = (0..MAX_POINT_LIGHTS + 2)
362 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
363 .collect();
364 let u = uniforms(vec![], lights);
365 assert_eq!(u.num_point, MAX_POINT_LIGHTS as i32);
366 assert_eq!(u.num_local_lights, (MAX_POINT_LIGHTS + 2) as i32);
367 }
368
369 #[test]
372 fn num_local_lights_counts_spot_lights() {
373 let local =
374 build_light_data(&[], &[spot([0.0; 3], [0.0, -1.0, 0.0], 10.0, 20.0)], &[]).lights;
375 let u = build_light_uniforms(vec![], vec![], &local, 1.0);
376 assert_eq!(u.num_point, 0);
377 assert_eq!(u.num_local_lights, 1);
378 }
379
380 #[test]
381 fn light_buffer_maps_point_light_fields() {
382 let buf =
383 build_light_data(&[pt([2.0, 3.0, 4.0], [1.0, 0.8, 0.5], 8.0, 6.0)], &[], &[]).lights;
384 assert_eq!(buf.len(), 1);
385 assert_eq!(buf[0].position, [2.0, 3.0, 4.0]);
386 assert_eq!(buf[0].color, [1.0, 0.8, 0.5]);
387 assert!((buf[0].intensity - 8.0).abs() < 1e-6);
388 assert!((buf[0].range - 6.0).abs() < 1e-6);
389 assert_eq!(buf[0].kind, LIGHT_KIND_POINT);
390 assert_eq!(buf[0].shadow_index, -1);
392 assert_eq!(buf[0].direction, [0.0; 3]);
393 assert_eq!(buf[0].cos_inner, 0.0);
394 assert_eq!(buf[0].cos_outer, 0.0);
395 }
396
397 #[test]
398 fn light_buffer_maps_spot_light_fields() {
399 let buf = build_light_data(
400 &[],
401 &[spot([1.0, 5.0, 2.0], [0.0, -2.0, 0.0], 15.0, 30.0)],
402 &[],
403 )
404 .lights;
405 assert_eq!(buf.len(), 1);
406 assert_eq!(buf[0].position, [1.0, 5.0, 2.0]);
407 assert_eq!(buf[0].kind, LIGHT_KIND_SPOT);
408 assert_eq!(buf[0].direction, [0.0, -1.0, 0.0]);
410 assert!((buf[0].cos_inner - 15.0f32.to_radians().cos()).abs() < 1e-6);
411 assert!((buf[0].cos_outer - 30.0f32.to_radians().cos()).abs() < 1e-6);
412 assert!(buf[0].cos_inner >= buf[0].cos_outer);
414 }
415
416 #[test]
417 fn spot_inner_cone_clamped_to_the_outer_cone() {
418 let buf =
419 build_light_data(&[], &[spot([0.0; 3], [0.0, -1.0, 0.0], 60.0, 20.0)], &[]).lights;
420 assert!((buf[0].cos_inner - buf[0].cos_outer).abs() < 1e-6);
421 }
422
423 #[test]
424 fn spot_lights_follow_the_point_lights_in_the_buffer() {
425 let buf = build_light_data(
426 &[
427 pt([0.0; 3], [1.0; 3], 1.0, 5.0),
428 pt([1.0; 3], [1.0; 3], 1.0, 5.0),
429 ],
430 &[spot([2.0; 3], [0.0, -1.0, 0.0], 10.0, 20.0)],
431 &[],
432 )
433 .lights;
434 assert_eq!(buf.len(), 3);
435 assert_eq!(buf[0].kind, LIGHT_KIND_POINT);
436 assert_eq!(buf[1].kind, LIGHT_KIND_POINT);
437 assert_eq!(buf[2].kind, LIGHT_KIND_SPOT);
438 }
439
440 #[test]
442 fn shadow_indices_point_at_real_spot_shadow_entries() {
443 let mut casting = spot([0.0, 5.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0);
444 casting.cast_shadows = true;
445 let mut dark = spot([3.0, 5.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0);
446 dark.cast_shadows = false;
447 let data = build_light_data(
448 &[pt([0.0; 3], [1.0; 3], 1.0, 5.0)],
449 &[
450 casting,
451 dark,
452 spot([6.0, 5.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0),
453 ],
454 &[],
455 );
456 assert_eq!(data.lights[0].shadow_index, -1);
458 assert_eq!(data.lights[1].shadow_index, 0);
459 assert_eq!(data.lights[2].shadow_index, -1);
460 assert_eq!(data.lights[3].shadow_index, 1);
461 assert_eq!(data.spot_shadows.len(), 2);
462 for l in &data.lights {
463 assert!(
464 l.shadow_index < data.spot_shadows.len() as i32,
465 "shadow_index stays in bounds of spot_shadows"
466 );
467 }
468 }
469
470 #[test]
473 fn clamped_spots_do_not_strand_shadow_slices() {
474 let points: Vec<PointLight> = (0..MAX_LOCAL_LIGHTS - 1)
475 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
476 .collect();
477 let spots: Vec<SpotLight> = (0..4)
478 .map(|i| {
479 let mut s = spot([i as f32, 5.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0);
480 s.cast_shadows = true;
481 s
482 })
483 .collect();
484 let data = build_light_data(&points, &spots, &[]);
485 assert_eq!(data.lights.len(), MAX_LOCAL_LIGHTS);
486 assert_eq!(data.spot_shadows.len(), 1);
488 let max_index = data.lights.iter().map(|l| l.shadow_index).max().unwrap();
489 assert_eq!(max_index, 0);
490 }
491
492 fn area(centre: [f32; 3], half_size: [f32; 2]) -> RectAreaLight {
493 RectAreaLight {
494 centre,
495 half_size,
496 ..RectAreaLight::default()
497 }
498 }
499
500 #[test]
503 fn area_lights_follow_the_other_kinds_and_index_their_table() {
504 let mut casting = spot([0.0, 5.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0);
505 casting.cast_shadows = true;
506 let data = build_light_data(
507 &[pt([0.0; 3], [1.0; 3], 1.0, 5.0)],
508 &[casting],
509 &[
510 area([2.0, 3.0, 0.0], [1.0, 2.0]),
511 area([5.0; 3], [1.0, 1.0]),
512 ],
513 );
514 assert_eq!(data.lights.len(), 4);
515 assert_eq!(data.lights[2].kind, LIGHT_KIND_AREA);
516 assert_eq!(data.lights[3].kind, LIGHT_KIND_AREA);
517 assert_eq!(data.lights[2].data_index, 0);
518 assert_eq!(data.lights[3].data_index, 1);
519 assert_eq!(data.area_lights.len(), 2);
520 assert_eq!(data.lights[0].data_index, -1);
522 assert_eq!(data.lights[1].data_index, -1);
523 assert_eq!(data.lights[1].shadow_index, 0);
524 for l in &data.lights {
525 assert!(l.data_index < data.area_lights.len() as i32);
526 }
527 }
528
529 #[test]
531 fn area_light_centre_and_normal_map_onto_the_gpu_light() {
532 let mut l = area([1.0, 2.0, 3.0], [1.0, 1.0]);
533 l.normal = [0.0, 0.0, 1.0];
534 let data = build_light_data(&[], &[], &[l]);
535 assert_eq!(data.lights[0].position, [1.0, 2.0, 3.0]);
536 assert_eq!(data.lights[0].direction, [0.0, 0.0, 1.0]);
537 }
538
539 #[test]
540 fn clamped_area_lights_do_not_strand_table_entries() {
541 let points: Vec<PointLight> = (0..MAX_LOCAL_LIGHTS - 1)
542 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
543 .collect();
544 let areas: Vec<RectAreaLight> = (0..4).map(|_| area([0.0; 3], [1.0, 1.0])).collect();
545 let data = build_light_data(&points, &[], &areas);
546 assert_eq!(data.lights.len(), MAX_LOCAL_LIGHTS);
547 assert_eq!(data.area_lights.len(), 1);
548 }
549
550 #[test]
551 fn light_buffer_carries_more_than_the_legacy_cap() {
552 let lights: Vec<PointLight> = (0..MAX_POINT_LIGHTS + 50)
553 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
554 .collect();
555 let buf = build_light_data(&lights, &[], &[]).lights;
556 assert_eq!(buf.len(), MAX_POINT_LIGHTS + 50);
557 }
558
559 #[test]
560 fn light_buffer_clamped_to_capacity() {
561 let lights: Vec<PointLight> = (0..MAX_LOCAL_LIGHTS + 10)
562 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
563 .collect();
564 let buf = build_light_data(&lights, &[], &[]).lights;
565 assert_eq!(buf.len(), MAX_LOCAL_LIGHTS);
566 }
567
568 #[test]
570 fn point_and_spot_lights_share_the_capacity() {
571 let points: Vec<PointLight> = (0..MAX_LOCAL_LIGHTS - 1)
572 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
573 .collect();
574 let spots: Vec<SpotLight> = (0..4)
575 .map(|i| spot([i as f32, 0.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0))
576 .collect();
577 let buf = build_light_data(&points, &spots, &[]).lights;
578 assert_eq!(buf.len(), MAX_LOCAL_LIGHTS);
579 assert_eq!(buf[MAX_LOCAL_LIGHTS - 1].kind, LIGHT_KIND_SPOT);
580 }
581}