Skip to main content

arcane_engine/renderer/
lighting.rs

1use bytemuck::{Pod, Zeroable};
2
3#[derive(Debug, Clone)]
4pub struct PointLight {
5    pub x: f32,
6    pub y: f32,
7    pub radius: f32,
8    pub r: f32,
9    pub g: f32,
10    pub b: f32,
11    pub intensity: f32,
12}
13
14#[derive(Debug, Clone)]
15pub struct LightingState {
16    pub ambient: [f32; 3],
17    pub lights: Vec<PointLight>,
18}
19
20impl Default for LightingState {
21    fn default() -> Self {
22        Self {
23            ambient: [1.0, 1.0, 1.0], // Full white = no darkening
24            lights: Vec::new(),
25        }
26    }
27}
28
29pub const MAX_LIGHTS: usize = 8;
30
31/// GPU-aligned light data. Each light = 32 bytes (2 x vec4).
32#[repr(C)]
33#[derive(Copy, Clone, Pod, Zeroable)]
34pub struct LightData {
35    pub pos_radius: [f32; 4],     // x, y, radius, _padding
36    pub color_intensity: [f32; 4], // r, g, b, intensity
37}
38
39/// GPU uniform for lighting. Total size = 16 + 8*32 = 272 bytes.
40/// Must be 16-byte aligned.
41#[repr(C)]
42#[derive(Copy, Clone, Pod, Zeroable)]
43pub struct LightingUniform {
44    pub ambient: [f32; 3],
45    pub light_count: u32,
46    pub lights: [LightData; MAX_LIGHTS],
47}
48
49impl LightingState {
50    pub fn to_uniform(&self) -> LightingUniform {
51        let mut uniform = LightingUniform {
52            ambient: self.ambient,
53            light_count: self.lights.len().min(MAX_LIGHTS) as u32,
54            lights: [LightData {
55                pos_radius: [0.0; 4],
56                color_intensity: [0.0; 4],
57            }; MAX_LIGHTS],
58        };
59
60        for (i, light) in self.lights.iter().take(MAX_LIGHTS).enumerate() {
61            uniform.lights[i] = LightData {
62                pos_radius: [light.x, light.y, light.radius, 0.0],
63                color_intensity: [light.r, light.g, light.b, light.intensity],
64            };
65        }
66
67        uniform
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_default_lighting_is_white_ambient() {
77        let state = LightingState::default();
78        assert_eq!(state.ambient, [1.0, 1.0, 1.0]);
79        assert!(state.lights.is_empty());
80    }
81
82    #[test]
83    fn test_uniform_construction() {
84        let state = LightingState {
85            ambient: [0.2, 0.2, 0.3],
86            lights: vec![PointLight {
87                x: 100.0,
88                y: 200.0,
89                radius: 150.0,
90                r: 1.0,
91                g: 0.8,
92                b: 0.5,
93                intensity: 1.5,
94            }],
95        };
96        let uniform = state.to_uniform();
97        assert_eq!(uniform.light_count, 1);
98        assert_eq!(uniform.ambient, [0.2, 0.2, 0.3]);
99        assert_eq!(uniform.lights[0].pos_radius, [100.0, 200.0, 150.0, 0.0]);
100        assert_eq!(uniform.lights[0].color_intensity, [1.0, 0.8, 0.5, 1.5]);
101    }
102
103    #[test]
104    fn test_max_lights_capped() {
105        let state = LightingState {
106            ambient: [1.0; 3],
107            lights: (0..12)
108                .map(|i| PointLight {
109                    x: i as f32,
110                    y: 0.0,
111                    radius: 10.0,
112                    r: 1.0,
113                    g: 1.0,
114                    b: 1.0,
115                    intensity: 1.0,
116                })
117                .collect(),
118        };
119        let uniform = state.to_uniform();
120        assert_eq!(uniform.light_count, 8); // capped at MAX_LIGHTS
121    }
122
123    #[test]
124    fn test_gpu_alignment() {
125        // LightingUniform must be properly aligned for GPU upload
126        assert_eq!(std::mem::size_of::<LightData>(), 32);
127        assert_eq!(std::mem::size_of::<LightingUniform>(), 272);
128        assert_eq!(std::mem::align_of::<LightingUniform>(), 4);
129    }
130
131    #[test]
132    fn test_empty_lights_uniform() {
133        let state = LightingState {
134            ambient: [0.5, 0.5, 0.5],
135            lights: vec![],
136        };
137        let uniform = state.to_uniform();
138        assert_eq!(uniform.light_count, 0);
139        assert_eq!(uniform.ambient, [0.5, 0.5, 0.5]);
140        // All light slots should be zeroed
141        for light in &uniform.lights {
142            assert_eq!(light.pos_radius, [0.0; 4]);
143            assert_eq!(light.color_intensity, [0.0; 4]);
144        }
145    }
146}