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::math::vec3::{length, scale};
17use crate::render::area_light;
18use crate::render::spot_shadow;
19use alloc::vec::Vec;
20
21pub struct LightData {
27 pub lights: Vec<GpuLight>,
29 pub spot_shadows: Vec<SpotShadowData>,
31 pub area_lights: Vec<AreaLightData>,
33}
34
35pub fn build_light_data(
41 pt_lights: &[PointLight],
42 spot_lights: &[SpotLight],
43 rect_lights: &[RectAreaLight],
44) -> LightData {
45 let slices = spot_shadow::assign_spot_shadow_slices(spot_lights);
46 let spot_shadows = spot_shadow::build_spot_shadow_data(spot_lights, &slices);
47 let slots = area_light::assign_area_light_slots(rect_lights);
48 let area_lights = area_light::build_area_light_data(rect_lights, &slots);
49
50 let points = pt_lights.iter().map(|l| GpuLight {
51 position: l.position,
52 range: l.range,
53 color: l.color,
54 intensity: l.intensity,
55 kind: LIGHT_KIND_POINT,
56 ..GpuLight::ZERO
57 });
58 let spots = spot_lights
59 .iter()
60 .zip(&slices)
61 .map(|(l, &shadow_index)| GpuLight {
62 position: l.position,
63 range: l.range,
64 color: l.color,
65 intensity: l.intensity,
66 direction: l.unit_direction(),
67 kind: LIGHT_KIND_SPOT,
68 cos_inner: l.cos_inner(),
69 cos_outer: l.cos_outer(),
70 shadow_index,
71 ..GpuLight::ZERO
72 });
73 let areas = rect_lights
74 .iter()
75 .zip(&slots)
76 .map(|(l, &data_index)| GpuLight {
77 position: l.centre,
78 range: l.range,
79 color: l.color,
80 intensity: l.intensity,
81 direction: l.normal,
82 kind: LIGHT_KIND_AREA,
83 data_index,
84 ..GpuLight::ZERO
85 });
86 let lights: Vec<GpuLight> = points
87 .chain(spots)
88 .chain(areas)
89 .take(MAX_LOCAL_LIGHTS)
90 .collect();
91
92 let kept = lights.iter().filter(|l| l.shadow_index >= 0).count();
95 let spot_shadows = if kept < spot_shadows.len() {
96 spot_shadows[..kept].to_vec()
97 } else {
98 spot_shadows
99 };
100
101 let kept_areas = lights.iter().filter(|l| l.data_index >= 0).count();
104 let area_lights = if kept_areas < area_lights.len() {
105 area_lights[..kept_areas].to_vec()
106 } else {
107 area_lights
108 };
109
110 LightData {
111 lights,
112 spot_shadows,
113 area_lights,
114 }
115}
116
117const ZERO_DIR: DirectionalLightData = DirectionalLightData {
118 direction: [0.0; 3],
119 intensity: 0.0,
120 color: [0.0; 3],
121 _pad: 0.0,
122};
123const ZERO_PT: PointLightData = PointLightData {
124 position: [0.0; 3],
125 range: 0.0,
126 color: [0.0; 3],
127 intensity: 0.0,
128};
129
130#[derive(Clone, Copy, Debug, PartialEq)]
143pub struct DirectionalLightSet {
144 lights: [DirectionalLight; MAX_DIRECTIONAL_LIGHTS],
145 count: usize,
146}
147
148impl DirectionalLightSet {
149 pub fn collect(lights: impl Iterator<Item = DirectionalLight>) -> Self {
151 let mut set = Self {
152 lights: [DirectionalLight::ZERO; MAX_DIRECTIONAL_LIGHTS],
153 count: 0,
154 };
155 for light in lights.take(MAX_DIRECTIONAL_LIGHTS) {
156 set.lights[set.count] = light;
157 set.count += 1;
158 }
159 set
160 }
161
162 pub fn as_slice(&self) -> &[DirectionalLight] {
164 &self.lights[..self.count]
165 }
166}
167
168pub fn directional_light_data(
173 lights: &[DirectionalLight],
174) -> ([DirectionalLightData; MAX_DIRECTIONAL_LIGHTS], i32) {
175 let mut directional = [ZERO_DIR; MAX_DIRECTIONAL_LIGHTS];
176 for (slot, l) in directional.iter_mut().zip(lights) {
177 *slot = DirectionalLightData {
178 direction: l.direction,
179 intensity: l.intensity,
180 color: l.color,
181 _pad: 0.0,
182 };
183 }
184 (directional, lights.len().min(MAX_DIRECTIONAL_LIGHTS) as i32)
185}
186
187pub fn sun_direction(uniforms: &LightUniforms) -> [f32; 3] {
192 if uniforms.num_directional > 0 {
193 uniforms.directional[0].direction
194 } else {
195 LightUniforms::DEFAULT.directional[0].direction
196 }
197}
198
199pub fn sun_color(uniforms: &LightUniforms) -> [f32; 3] {
202 if uniforms.num_directional > 0 {
203 let l = &uniforms.directional[0];
204 [
205 l.color[0] * l.intensity,
206 l.color[1] * l.intensity,
207 l.color[2] * l.intensity,
208 ]
209 } else {
210 [1.0, 1.0, 1.0]
211 }
212}
213
214pub fn glint_sun(uniforms: &LightUniforms) -> ([f32; 4], [f32; 4]) {
221 if uniforms.num_directional <= 0 {
222 return ([0.0; 4], [0.0; 4]);
223 }
224 let light = &uniforms.directional[0];
225 let len = length(light.direction);
226 if len < 1e-6 {
227 return ([0.0; 4], [0.0; 4]);
228 }
229 let dir = scale(light.direction, 1.0 / len);
230 (
231 [dir[0], dir[1], dir[2], 0.0],
232 [
233 light.color[0] * light.intensity,
234 light.color[1] * light.intensity,
235 light.color[2] * light.intensity,
236 0.0,
237 ],
238 )
239}
240
241pub fn build_light_uniforms(
244 dir_lights: Vec<DirectionalLight>,
245 pt_lights: Vec<PointLight>,
246 local_lights: &[GpuLight],
247 ambient_intensity: f32,
248) -> LightUniforms {
249 if dir_lights.is_empty() && pt_lights.is_empty() && local_lights.is_empty() {
250 return LightUniforms {
251 ambient_intensity,
252 ..LightUniforms::DEFAULT
253 };
254 }
255
256 let (directional, num_directional) = directional_light_data(&dir_lights);
257 let mut point = [ZERO_PT; MAX_POINT_LIGHTS];
261 let num_point = pt_lights.len().min(MAX_POINT_LIGHTS);
262 for (i, l) in pt_lights.into_iter().take(MAX_POINT_LIGHTS).enumerate() {
263 point[i] = PointLightData {
264 position: l.position,
265 range: l.range,
266 color: l.color,
267 intensity: l.intensity,
268 };
269 }
270
271 LightUniforms {
272 directional,
273 point,
274 num_directional,
275 num_point: num_point as i32,
276 ambient_intensity,
277 num_local_lights: local_lights.len() as i32,
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 use alloc::vec;
286 fn dir(direction: [f32; 3], color: [f32; 3], intensity: f32) -> DirectionalLight {
287 DirectionalLight {
288 direction,
289 color,
290 intensity,
291 }
292 }
293
294 fn pt(position: [f32; 3], color: [f32; 3], intensity: f32, range: f32) -> PointLight {
295 PointLight {
296 position,
297 color,
298 intensity,
299 range,
300 }
301 }
302
303 fn spot(position: [f32; 3], direction: [f32; 3], inner: f32, outer: f32) -> SpotLight {
304 SpotLight {
305 position,
306 direction,
307 inner_angle: inner,
308 outer_angle: outer,
309 ..SpotLight::default()
310 }
311 }
312
313 fn uniforms(dir_lights: Vec<DirectionalLight>, pt_lights: Vec<PointLight>) -> LightUniforms {
315 let local = build_light_data(&pt_lights, &[], &[]).lights;
316 build_light_uniforms(dir_lights, pt_lights, &local, 1.0)
317 }
318
319 #[test]
322 fn directional_packing_matches_the_init_build() {
323 let lights = vec![
324 dir([-0.3, 0.85, 0.4], [1.0, 0.95, 0.8], 1.5),
325 dir([0.1, -1.0, 0.0], [0.2, 0.3, 0.4], 0.5),
326 ];
327 let (packed, count) = directional_light_data(&lights);
328 let built = uniforms(lights, vec![]);
329 assert_eq!(count, built.num_directional);
330 assert_eq!(packed, built.directional);
331 }
332
333 #[test]
336 fn directional_packing_zeroes_the_unused_slots() {
337 let (packed, count) = directional_light_data(&[dir([0.0, 1.0, 0.0], [1.0; 3], 2.0)]);
338 assert_eq!(count, 1);
339 assert!(packed[1..].iter().all(|d| *d == ZERO_DIR));
340 let (empty, none) = directional_light_data(&[]);
341 assert_eq!(none, 0);
342 assert!(empty.iter().all(|d| *d == ZERO_DIR));
343 }
344
345 #[test]
347 fn directional_packing_clamps_to_the_array_capacity() {
348 let many: Vec<DirectionalLight> = (0..MAX_DIRECTIONAL_LIGHTS + 2)
349 .map(|i| dir([0.0, 1.0, 0.0], [1.0; 3], i as f32))
350 .collect();
351 let (packed, count) = directional_light_data(&many);
352 assert_eq!(count, MAX_DIRECTIONAL_LIGHTS as i32);
353 assert_eq!(packed.len(), MAX_DIRECTIONAL_LIGHTS);
354 }
355
356 #[test]
357 fn empty_inputs_return_default() {
358 let u = uniforms(vec![], vec![]);
359 assert_eq!(u.num_directional, LightUniforms::DEFAULT.num_directional);
360 assert_eq!(u.num_point, LightUniforms::DEFAULT.num_point);
361 }
362
363 #[test]
364 fn ambient_intensity_carried_in_both_branches() {
365 let empty = build_light_uniforms(vec![], vec![], &[], 2.5);
368 assert!((empty.ambient_intensity - 2.5).abs() < 1e-6);
369 let populated = build_light_uniforms(
370 vec![dir([-0.3, 0.85, 0.4], [1.0; 3], 1.0)],
371 vec![],
372 &[],
373 3.0,
374 );
375 assert!((populated.ambient_intensity - 3.0).abs() < 1e-6);
376 }
377
378 #[test]
379 fn single_directional_light_fields_mapped() {
380 let u = uniforms(vec![dir([-0.3, 0.85, 0.4], [1.0, 0.95, 0.8], 1.5)], vec![]);
381 assert_eq!(u.num_directional, 1);
382 assert_eq!(u.num_point, 0);
383 assert_eq!(u.directional[0].direction, [-0.3, 0.85, 0.4]);
384 assert_eq!(u.directional[0].color, [1.0, 0.95, 0.8]);
385 assert!((u.directional[0].intensity - 1.5).abs() < 1e-6);
386 }
387
388 #[test]
389 fn single_point_light_fields_mapped() {
390 let u = uniforms(vec![], vec![pt([2.0, 3.0, 4.0], [1.0, 0.8, 0.5], 8.0, 6.0)]);
391 assert_eq!(u.num_directional, 0);
392 assert_eq!(u.num_point, 1);
393 assert_eq!(u.point[0].position, [2.0, 3.0, 4.0]);
394 assert_eq!(u.point[0].color, [1.0, 0.8, 0.5]);
395 assert!((u.point[0].intensity - 8.0).abs() < 1e-6);
396 assert!((u.point[0].range - 6.0).abs() < 1e-6);
397 }
398
399 #[test]
400 fn excess_directional_lights_clamped_to_max() {
401 let lights: Vec<DirectionalLight> = (0..MAX_DIRECTIONAL_LIGHTS + 2)
402 .map(|i| dir([i as f32, 0.0, 0.0], [1.0; 3], 1.0))
403 .collect();
404 let u = uniforms(lights, vec![]);
405 assert_eq!(u.num_directional, MAX_DIRECTIONAL_LIGHTS as i32);
406 }
407
408 #[test]
409 fn excess_point_lights_clamped_to_max() {
410 let lights: Vec<PointLight> = (0..MAX_POINT_LIGHTS + 2)
413 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
414 .collect();
415 let u = uniforms(vec![], lights);
416 assert_eq!(u.num_point, MAX_POINT_LIGHTS as i32);
417 assert_eq!(u.num_local_lights, (MAX_POINT_LIGHTS + 2) as i32);
418 }
419
420 #[test]
423 fn num_local_lights_counts_spot_lights() {
424 let local =
425 build_light_data(&[], &[spot([0.0; 3], [0.0, -1.0, 0.0], 10.0, 20.0)], &[]).lights;
426 let u = build_light_uniforms(vec![], vec![], &local, 1.0);
427 assert_eq!(u.num_point, 0);
428 assert_eq!(u.num_local_lights, 1);
429 }
430
431 #[test]
432 fn light_buffer_maps_point_light_fields() {
433 let buf =
434 build_light_data(&[pt([2.0, 3.0, 4.0], [1.0, 0.8, 0.5], 8.0, 6.0)], &[], &[]).lights;
435 assert_eq!(buf.len(), 1);
436 assert_eq!(buf[0].position, [2.0, 3.0, 4.0]);
437 assert_eq!(buf[0].color, [1.0, 0.8, 0.5]);
438 assert!((buf[0].intensity - 8.0).abs() < 1e-6);
439 assert!((buf[0].range - 6.0).abs() < 1e-6);
440 assert_eq!(buf[0].kind, LIGHT_KIND_POINT);
441 assert_eq!(buf[0].shadow_index, -1);
443 assert_eq!(buf[0].direction, [0.0; 3]);
444 assert_eq!(buf[0].cos_inner, 0.0);
445 assert_eq!(buf[0].cos_outer, 0.0);
446 }
447
448 #[test]
449 fn light_buffer_maps_spot_light_fields() {
450 let buf = build_light_data(
451 &[],
452 &[spot([1.0, 5.0, 2.0], [0.0, -2.0, 0.0], 15.0, 30.0)],
453 &[],
454 )
455 .lights;
456 assert_eq!(buf.len(), 1);
457 assert_eq!(buf[0].position, [1.0, 5.0, 2.0]);
458 assert_eq!(buf[0].kind, LIGHT_KIND_SPOT);
459 assert_eq!(buf[0].direction, [0.0, -1.0, 0.0]);
461 assert!((buf[0].cos_inner - 15.0f32.to_radians().cos()).abs() < 1e-6);
462 assert!((buf[0].cos_outer - 30.0f32.to_radians().cos()).abs() < 1e-6);
463 assert!(buf[0].cos_inner >= buf[0].cos_outer);
465 }
466
467 #[test]
468 fn spot_inner_cone_clamped_to_the_outer_cone() {
469 let buf =
470 build_light_data(&[], &[spot([0.0; 3], [0.0, -1.0, 0.0], 60.0, 20.0)], &[]).lights;
471 assert!((buf[0].cos_inner - buf[0].cos_outer).abs() < 1e-6);
472 }
473
474 #[test]
475 fn spot_lights_follow_the_point_lights_in_the_buffer() {
476 let buf = build_light_data(
477 &[
478 pt([0.0; 3], [1.0; 3], 1.0, 5.0),
479 pt([1.0; 3], [1.0; 3], 1.0, 5.0),
480 ],
481 &[spot([2.0; 3], [0.0, -1.0, 0.0], 10.0, 20.0)],
482 &[],
483 )
484 .lights;
485 assert_eq!(buf.len(), 3);
486 assert_eq!(buf[0].kind, LIGHT_KIND_POINT);
487 assert_eq!(buf[1].kind, LIGHT_KIND_POINT);
488 assert_eq!(buf[2].kind, LIGHT_KIND_SPOT);
489 }
490
491 #[test]
493 fn shadow_indices_point_at_real_spot_shadow_entries() {
494 let mut casting = spot([0.0, 5.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0);
495 casting.cast_shadows = true;
496 let mut dark = spot([3.0, 5.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0);
497 dark.cast_shadows = false;
498 let data = build_light_data(
499 &[pt([0.0; 3], [1.0; 3], 1.0, 5.0)],
500 &[
501 casting,
502 dark,
503 spot([6.0, 5.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0),
504 ],
505 &[],
506 );
507 assert_eq!(data.lights[0].shadow_index, -1);
509 assert_eq!(data.lights[1].shadow_index, 0);
510 assert_eq!(data.lights[2].shadow_index, -1);
511 assert_eq!(data.lights[3].shadow_index, 1);
512 assert_eq!(data.spot_shadows.len(), 2);
513 for l in &data.lights {
514 assert!(
515 l.shadow_index < data.spot_shadows.len() as i32,
516 "shadow_index stays in bounds of spot_shadows"
517 );
518 }
519 }
520
521 #[test]
524 fn clamped_spots_do_not_strand_shadow_slices() {
525 let points: Vec<PointLight> = (0..MAX_LOCAL_LIGHTS - 1)
526 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
527 .collect();
528 let spots: Vec<SpotLight> = (0..4)
529 .map(|i| {
530 let mut s = spot([i as f32, 5.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0);
531 s.cast_shadows = true;
532 s
533 })
534 .collect();
535 let data = build_light_data(&points, &spots, &[]);
536 assert_eq!(data.lights.len(), MAX_LOCAL_LIGHTS);
537 assert_eq!(data.spot_shadows.len(), 1);
539 let max_index = data.lights.iter().map(|l| l.shadow_index).max().unwrap();
540 assert_eq!(max_index, 0);
541 }
542
543 fn area(centre: [f32; 3], half_size: [f32; 2]) -> RectAreaLight {
544 RectAreaLight {
545 centre,
546 half_size,
547 ..RectAreaLight::default()
548 }
549 }
550
551 #[test]
554 fn area_lights_follow_the_other_kinds_and_index_their_table() {
555 let mut casting = spot([0.0, 5.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0);
556 casting.cast_shadows = true;
557 let data = build_light_data(
558 &[pt([0.0; 3], [1.0; 3], 1.0, 5.0)],
559 &[casting],
560 &[
561 area([2.0, 3.0, 0.0], [1.0, 2.0]),
562 area([5.0; 3], [1.0, 1.0]),
563 ],
564 );
565 assert_eq!(data.lights.len(), 4);
566 assert_eq!(data.lights[2].kind, LIGHT_KIND_AREA);
567 assert_eq!(data.lights[3].kind, LIGHT_KIND_AREA);
568 assert_eq!(data.lights[2].data_index, 0);
569 assert_eq!(data.lights[3].data_index, 1);
570 assert_eq!(data.area_lights.len(), 2);
571 assert_eq!(data.lights[0].data_index, -1);
573 assert_eq!(data.lights[1].data_index, -1);
574 assert_eq!(data.lights[1].shadow_index, 0);
575 for l in &data.lights {
576 assert!(l.data_index < data.area_lights.len() as i32);
577 }
578 }
579
580 #[test]
582 fn area_light_centre_and_normal_map_onto_the_gpu_light() {
583 let mut l = area([1.0, 2.0, 3.0], [1.0, 1.0]);
584 l.normal = [0.0, 0.0, 1.0];
585 let data = build_light_data(&[], &[], &[l]);
586 assert_eq!(data.lights[0].position, [1.0, 2.0, 3.0]);
587 assert_eq!(data.lights[0].direction, [0.0, 0.0, 1.0]);
588 }
589
590 #[test]
591 fn clamped_area_lights_do_not_strand_table_entries() {
592 let points: Vec<PointLight> = (0..MAX_LOCAL_LIGHTS - 1)
593 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
594 .collect();
595 let areas: Vec<RectAreaLight> = (0..4).map(|_| area([0.0; 3], [1.0, 1.0])).collect();
596 let data = build_light_data(&points, &[], &areas);
597 assert_eq!(data.lights.len(), MAX_LOCAL_LIGHTS);
598 assert_eq!(data.area_lights.len(), 1);
599 }
600
601 #[test]
602 fn light_buffer_carries_more_than_the_legacy_cap() {
603 let lights: Vec<PointLight> = (0..MAX_POINT_LIGHTS + 50)
604 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
605 .collect();
606 let buf = build_light_data(&lights, &[], &[]).lights;
607 assert_eq!(buf.len(), MAX_POINT_LIGHTS + 50);
608 }
609
610 #[test]
611 fn light_buffer_clamped_to_capacity() {
612 let lights: Vec<PointLight> = (0..MAX_LOCAL_LIGHTS + 10)
613 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
614 .collect();
615 let buf = build_light_data(&lights, &[], &[]).lights;
616 assert_eq!(buf.len(), MAX_LOCAL_LIGHTS);
617 }
618
619 #[test]
621 fn point_and_spot_lights_share_the_capacity() {
622 let points: Vec<PointLight> = (0..MAX_LOCAL_LIGHTS - 1)
623 .map(|i| pt([i as f32, 0.0, 0.0], [1.0; 3], 1.0, 5.0))
624 .collect();
625 let spots: Vec<SpotLight> = (0..4)
626 .map(|i| spot([i as f32, 0.0, 0.0], [0.0, -1.0, 0.0], 10.0, 20.0))
627 .collect();
628 let buf = build_light_data(&points, &spots, &[]).lights;
629 assert_eq!(buf.len(), MAX_LOCAL_LIGHTS);
630 assert_eq!(buf[MAX_LOCAL_LIGHTS - 1].kind, LIGHT_KIND_SPOT);
631 }
632
633 #[test]
637 fn the_sun_is_the_first_declared_directional_light() {
638 let u = uniforms(
639 vec![
640 dir([-0.3, 0.85, 0.4], [1.0, 0.5, 0.25], 2.0),
641 dir([0.1, -1.0, 0.0], [0.0, 1.0, 0.0], 9.0),
642 ],
643 vec![],
644 );
645 assert_eq!(sun_direction(&u), u.directional[0].direction);
646 assert_eq!(sun_color(&u), [2.0, 1.0, 0.5]);
647 }
648
649 #[test]
653 fn a_world_with_no_directional_light_falls_back_to_a_neutral_sun() {
654 let u = uniforms(vec![], vec![pt([0.0, 2.0, 0.0], [1.0; 3], 1.0, 10.0)]);
655 assert_eq!(u.num_directional, 0);
656 assert_eq!(
657 sun_direction(&u),
658 LightUniforms::DEFAULT.directional[0].direction
659 );
660 assert_eq!(sun_color(&u), [1.0, 1.0, 1.0]);
661 }
662
663 #[test]
667 fn the_glint_lanes_carry_a_unit_direction_and_a_weighted_colour() {
668 let u = uniforms(vec![dir([0.0, 3.0, 4.0], [0.5, 1.0, 0.75], 8.0)], vec![]);
669 let (sun_dir, sun_color) = glint_sun(&u);
670 assert_eq!(sun_dir, [0.0, 0.6, 0.8, 0.0]);
671 assert_eq!(sun_color, [4.0, 8.0, 6.0, 0.0]);
672 }
673
674 #[test]
677 fn a_world_with_no_directional_light_glints_not_at_all() {
678 let u = uniforms(vec![], vec![pt([0.0, 2.0, 0.0], [1.0; 3], 1.0, 10.0)]);
679 assert_eq!(glint_sun(&u), ([0.0; 4], [0.0; 4]));
680 }
681
682 #[test]
685 fn a_directionless_light_glints_not_at_all() {
686 let u = uniforms(vec![dir([0.0; 3], [1.0; 3], 5.0)], vec![]);
687 assert_eq!(u.num_directional, 1);
688 assert_eq!(glint_sun(&u), ([0.0; 4], [0.0; 4]));
689 }
690}