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