Skip to main content

concinnity_core/render/
area_light.rs

1//! Packs authored `RectAreaLight`s into the per-scene `AreaLightData` table the
2//! forward pass reads alongside the `GpuLight` buffer.
3//!
4//! The GpuLight record carries the panel's centre (`position`), emitting
5//! direction (`direction`), colour, intensity, and range; only the two in-plane
6//! edge vectors and the sidedness flag need the parallel table, indexed by
7//! `GpuLight.data_index`. The edge vectors are pre-scaled by the half-extents, so
8//! the shader reconstructs the four corners as `centre +/- right +/- up` without
9//! needing the sizes separately.
10//!
11//! The tangent frame comes from `geometry::glass_quad::plane_basis`, shared with
12//! the glass-panel quad builder, so a panel and an area light with the same
13//! normal agree on which way is "across".
14
15use crate::components::RectAreaLight;
16use crate::geometry::glass_quad::plane_basis;
17use crate::gfx::render_types::{AreaLightData, MAX_AREA_LIGHTS};
18use alloc::vec;
19use alloc::vec::Vec;
20
21// Per-rect table index: `indices[i]` is the `AreaLightData` slot rect `i` owns,
22// or -1 once the table is full. The value is what `GpuLight.data_index` carries.
23pub(crate) fn assign_area_light_slots(rect_lights: &[RectAreaLight]) -> Vec<i32> {
24    (0..rect_lights.len())
25        .map(|i| if i < MAX_AREA_LIGHTS { i as i32 } else { -1 })
26        .collect()
27}
28
29// The `AreaLightData` for each assigned slot, ordered by slot index.
30pub(crate) fn build_area_light_data(
31    rect_lights: &[RectAreaLight],
32    slots: &[i32],
33) -> Vec<AreaLightData> {
34    let mut out = vec![AreaLightData::ZERO; count_area_lights(slots)];
35    for (light, &slot) in rect_lights.iter().zip(slots) {
36        if slot >= 0 {
37            out[slot as usize] = area_light_data(light);
38        }
39    }
40    out
41}
42
43// How many slots `assign_area_light_slots` handed out.
44pub(crate) fn count_area_lights(slots: &[i32]) -> usize {
45    slots.iter().filter(|s| **s >= 0).count()
46}
47
48// One rect's edge vectors, pre-scaled by its half-extents. The authored normal
49// is already unit length and the half-extents already positive (the
50// `rect_area_light` validator guarantees both), so no re-clamping here.
51fn area_light_data(light: &RectAreaLight) -> AreaLightData {
52    let (tangent, bitangent) = plane_basis(light.normal);
53    let hw = light.half_size[0];
54    let hh = light.half_size[1];
55    AreaLightData {
56        right: [tangent[0] * hw, tangent[1] * hw, tangent[2] * hw],
57        two_sided: u32::from(light.two_sided),
58        up: [bitangent[0] * hh, bitangent[1] * hh, bitangent[2] * hh],
59        _pad: 0.0,
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::math::vec3::{dot, length};
67
68    fn rect(normal: [f32; 3], half_size: [f32; 2]) -> RectAreaLight {
69        RectAreaLight {
70            normal,
71            half_size,
72            ..RectAreaLight::default()
73        }
74    }
75
76    #[test]
77    fn slots_are_handed_out_in_declaration_order() {
78        let lights = vec![rect([0.0, 0.0, 1.0], [1.0, 1.0]); 3];
79        assert_eq!(assign_area_light_slots(&lights), vec![0, 1, 2]);
80    }
81
82    #[test]
83    fn slots_past_the_cap_are_dropped() {
84        let lights = vec![rect([0.0, 0.0, 1.0], [1.0, 1.0]); MAX_AREA_LIGHTS + 2];
85        let slots = assign_area_light_slots(&lights);
86        assert_eq!(count_area_lights(&slots), MAX_AREA_LIGHTS);
87        assert!(slots[MAX_AREA_LIGHTS..].iter().all(|s| *s == -1));
88    }
89
90    // The edge vectors carry the half-extents, so the shader can rebuild the
91    // corners without the sizes.
92    #[test]
93    fn edge_vectors_are_scaled_by_the_half_extents() {
94        let d = area_light_data(&rect([0.0, 0.0, 1.0], [3.0, 0.5]));
95        assert!((length(d.right) - 3.0).abs() < 1e-5);
96        assert!((length(d.up) - 0.5).abs() < 1e-5);
97    }
98
99    // The two edges and the normal must stay mutually perpendicular, or the
100    // reconstructed quad is skewed.
101    #[test]
102    fn the_edge_frame_stays_orthogonal_for_any_normal() {
103        for n in [
104            [0.0, 0.0, 1.0],
105            [0.0, -1.0, 0.0],
106            [0.0, 1.0, 0.0],
107            [0.577, 0.577, 0.577],
108            [-0.3, 0.9, 0.31],
109        ] {
110            let len = length(n);
111            let unit = [n[0] / len, n[1] / len, n[2] / len];
112            let d = area_light_data(&rect(unit, [2.0, 2.0]));
113            assert!(
114                dot(d.right, d.up).abs() < 1e-4,
115                "edges perpendicular: {n:?}"
116            );
117            assert!(dot(d.right, unit).abs() < 1e-4, "right in plane: {n:?}");
118            assert!(dot(d.up, unit).abs() < 1e-4, "up in plane: {n:?}");
119            assert!(d.right.iter().chain(&d.up).all(|v| v.is_finite()));
120        }
121    }
122
123    #[test]
124    fn two_sided_flag_is_carried() {
125        let mut l = rect([0.0, 0.0, 1.0], [1.0, 1.0]);
126        assert_eq!(area_light_data(&l).two_sided, 0);
127        l.two_sided = true;
128        assert_eq!(area_light_data(&l).two_sided, 1);
129    }
130
131    #[test]
132    fn data_is_indexed_by_slot() {
133        let lights = vec![
134            rect([0.0, 0.0, 1.0], [5.0, 1.0]),
135            rect([0.0, 0.0, 1.0], [1.0, 7.0]),
136        ];
137        let slots = assign_area_light_slots(&lights);
138        let data = build_area_light_data(&lights, &slots);
139        assert_eq!(data.len(), 2);
140        assert!((length(data[0].right) - 5.0).abs() < 1e-5);
141        assert!((length(data[1].up) - 7.0).abs() < 1e-5);
142    }
143}