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], lights: Vec::new(),
25 }
26 }
27}
28
29pub const MAX_LIGHTS: usize = 8;
30
31#[repr(C)]
33#[derive(Copy, Clone, Pod, Zeroable)]
34pub struct LightData {
35 pub pos_radius: [f32; 4], pub color_intensity: [f32; 4], }
38
39#[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); }
122
123 #[test]
124 fn test_gpu_alignment() {
125 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 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}