1use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
19use nalgebra::Point3;
20
21#[derive(Debug, Clone, Copy)]
28pub struct PointLight {
29 pub position: Point3<f32>,
31 pub color: Rgb565,
33 pub radius: f32,
36 pub intensity: f32,
38}
39
40impl PointLight {
41 pub fn new(position: Point3<f32>, color: Rgb565, radius: f32) -> Self {
43 Self {
44 position,
45 color,
46 radius,
47 intensity: 1.0,
48 }
49 }
50
51 pub fn with_intensity(mut self, intensity: f32) -> Self {
53 self.intensity = intensity;
54 self
55 }
56
57 #[inline]
62 pub fn contribution_at(&self, world_pos: Point3<f32>) -> Rgb565 {
63 let diff = world_pos - self.position;
64 let dist_sq = diff.dot(&diff);
65 let r_sq = self.radius * self.radius;
66 if dist_sq >= r_sq {
67 return Rgb565::new(0, 0, 0);
68 }
69 let t = 1.0 - dist_sq / r_sq;
70 let factor = t * self.intensity;
71 let r = ((self.color.r() as f32) * factor).min(31.0) as u8;
72 let g = ((self.color.g() as f32) * factor).min(63.0) as u8;
73 let b = ((self.color.b() as f32) * factor).min(31.0) as u8;
74 Rgb565::new(r, g, b)
75 }
76}
77
78pub struct PointLightSet<const N: usize> {
83 pub lights: heapless::Vec<PointLight, N>,
84}
85
86impl<const N: usize> PointLightSet<N> {
87 pub const fn new() -> Self {
89 Self {
90 lights: heapless::Vec::new(),
91 }
92 }
93
94 pub fn add(&mut self, light: PointLight) -> bool {
96 self.lights.push(light).is_ok()
97 }
98
99 pub fn clear(&mut self) {
101 self.lights.clear();
102 }
103
104 pub fn len(&self) -> usize {
106 self.lights.len()
107 }
108
109 pub fn is_empty(&self) -> bool {
111 self.lights.is_empty()
112 }
113
114 pub fn accumulate(&self, world_pos: Point3<f32>) -> Rgb565 {
118 let mut r = 0u32;
119 let mut g = 0u32;
120 let mut b = 0u32;
121 for light in &self.lights {
122 let c = light.contribution_at(world_pos);
123 r += c.r() as u32;
124 g += c.g() as u32;
125 b += c.b() as u32;
126 }
127 Rgb565::new(r.min(31) as u8, g.min(63) as u8, b.min(31) as u8)
128 }
129}
130
131impl<const N: usize> Default for PointLightSet<N> {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 extern crate std;
140 use super::*;
141 use embedded_graphics_core::pixelcolor::WebColors;
142
143 #[test]
144 fn test_point_light_full_at_center() {
145 let light = PointLight::new(Point3::new(0.0, 0.0, 0.0), Rgb565::CSS_WHITE, 5.0);
146 let tint = light.contribution_at(Point3::new(0.0, 0.0, 0.0));
147 assert_eq!(tint.r(), 31);
148 assert_eq!(tint.g(), 63);
149 assert_eq!(tint.b(), 31);
150 }
151
152 #[test]
153 fn test_point_light_zero_outside_radius() {
154 let light = PointLight::new(Point3::new(0.0, 0.0, 0.0), Rgb565::CSS_WHITE, 1.0);
155 let tint = light.contribution_at(Point3::new(2.0, 0.0, 0.0));
156 assert_eq!(tint.r(), 0);
157 assert_eq!(tint.g(), 0);
158 assert_eq!(tint.b(), 0);
159 }
160
161 #[test]
162 fn test_point_light_falloff() {
163 let light = PointLight::new(Point3::new(0.0, 0.0, 0.0), Rgb565::CSS_WHITE, 10.0);
164 let near = light.contribution_at(Point3::new(1.0, 0.0, 0.0));
165 let far = light.contribution_at(Point3::new(5.0, 0.0, 0.0));
166 assert!(near.r() > far.r());
167 }
168
169 #[test]
170 fn test_point_light_set_accumulates() {
171 let mut set: PointLightSet<4> = PointLightSet::new();
172 set.add(PointLight::new(
173 Point3::new(0.0, 0.0, 0.0),
174 Rgb565::new(10, 20, 10),
175 5.0,
176 ));
177 set.add(PointLight::new(
178 Point3::new(0.0, 0.0, 0.0),
179 Rgb565::new(5, 10, 5),
180 5.0,
181 ));
182 let tint = set.accumulate(Point3::new(0.0, 0.0, 0.0));
183 assert!(tint.r() >= 10);
184 }
185
186 #[test]
187 fn test_point_light_set_empty() {
188 let set: PointLightSet<4> = PointLightSet::new();
189 let tint = set.accumulate(Point3::new(0.0, 0.0, 0.0));
190 assert_eq!(tint.r(), 0);
191 assert_eq!(tint.g(), 0);
192 assert_eq!(tint.b(), 0);
193 }
194
195 #[test]
196 fn test_point_light_set_saturation() {
197 let mut set: PointLightSet<4> = PointLightSet::new();
198 set.add(PointLight::new(
200 Point3::new(0.0, 0.0, 0.0),
201 Rgb565::CSS_WHITE,
202 5.0,
203 ));
204 set.add(PointLight::new(
205 Point3::new(0.0, 0.0, 0.0),
206 Rgb565::CSS_WHITE,
207 5.0,
208 ));
209 let tint = set.accumulate(Point3::new(0.0, 0.0, 0.0));
210 assert_eq!(tint.r(), 31);
211 assert_eq!(tint.g(), 63);
212 assert_eq!(tint.b(), 31);
213 }
214
215 #[test]
216 fn test_point_light_analytical_falloff_math() {
217 let radius = 10.0f32;
218 let light = PointLight::new(Point3::new(0.0, 0.0, 0.0), Rgb565::new(31, 63, 31), radius)
219 .with_intensity(1.0);
220
221 let pos_half = Point3::new(5.0, 0.0, 0.0);
223 let tint = light.contribution_at(pos_half);
224
225 let expected_r = (31.0 * 0.75) as u8; let expected_g = (63.0 * 0.75) as u8; let expected_b = (31.0 * 0.75) as u8; assert_eq!(tint.r(), expected_r);
230 assert_eq!(tint.g(), expected_g);
231 assert_eq!(tint.b(), expected_b);
232 }
233}