use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
use nalgebra::Point3;
#[derive(Debug, Clone, Copy)]
pub struct PointLight {
pub position: Point3<f32>,
pub color: Rgb565,
pub radius: f32,
pub intensity: f32,
}
impl PointLight {
pub fn new(position: Point3<f32>, color: Rgb565, radius: f32) -> Self {
Self {
position,
color,
radius,
intensity: 1.0,
}
}
pub fn with_intensity(mut self, intensity: f32) -> Self {
self.intensity = intensity;
self
}
#[inline]
pub fn contribution_at(&self, world_pos: Point3<f32>) -> Rgb565 {
let diff = world_pos - self.position;
let dist_sq = diff.dot(&diff);
let r_sq = self.radius * self.radius;
if dist_sq >= r_sq {
return Rgb565::new(0, 0, 0);
}
let t = 1.0 - dist_sq / r_sq;
let factor = t * self.intensity;
let r = ((self.color.r() as f32) * factor).min(31.0) as u8;
let g = ((self.color.g() as f32) * factor).min(63.0) as u8;
let b = ((self.color.b() as f32) * factor).min(31.0) as u8;
Rgb565::new(r, g, b)
}
}
pub struct PointLightSet<const N: usize> {
pub lights: heapless::Vec<PointLight, N>,
}
impl<const N: usize> PointLightSet<N> {
pub const fn new() -> Self {
Self {
lights: heapless::Vec::new(),
}
}
pub fn add(&mut self, light: PointLight) -> bool {
self.lights.push(light).is_ok()
}
pub fn clear(&mut self) {
self.lights.clear();
}
pub fn len(&self) -> usize {
self.lights.len()
}
pub fn is_empty(&self) -> bool {
self.lights.is_empty()
}
pub fn accumulate(&self, world_pos: Point3<f32>) -> Rgb565 {
let mut r = 0u32;
let mut g = 0u32;
let mut b = 0u32;
for light in &self.lights {
let c = light.contribution_at(world_pos);
r += c.r() as u32;
g += c.g() as u32;
b += c.b() as u32;
}
Rgb565::new(r.min(31) as u8, g.min(63) as u8, b.min(31) as u8)
}
}
impl<const N: usize> Default for PointLightSet<N> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use embedded_graphics_core::pixelcolor::WebColors;
#[test]
fn test_point_light_full_at_center() {
let light = PointLight::new(Point3::new(0.0, 0.0, 0.0), Rgb565::CSS_WHITE, 5.0);
let tint = light.contribution_at(Point3::new(0.0, 0.0, 0.0));
assert_eq!(tint.r(), 31);
assert_eq!(tint.g(), 63);
assert_eq!(tint.b(), 31);
}
#[test]
fn test_point_light_zero_outside_radius() {
let light = PointLight::new(Point3::new(0.0, 0.0, 0.0), Rgb565::CSS_WHITE, 1.0);
let tint = light.contribution_at(Point3::new(2.0, 0.0, 0.0));
assert_eq!(tint.r(), 0);
assert_eq!(tint.g(), 0);
assert_eq!(tint.b(), 0);
}
#[test]
fn test_point_light_falloff() {
let light = PointLight::new(Point3::new(0.0, 0.0, 0.0), Rgb565::CSS_WHITE, 10.0);
let near = light.contribution_at(Point3::new(1.0, 0.0, 0.0));
let far = light.contribution_at(Point3::new(5.0, 0.0, 0.0));
assert!(near.r() > far.r());
}
#[test]
fn test_point_light_set_accumulates() {
let mut set: PointLightSet<4> = PointLightSet::new();
set.add(PointLight::new(
Point3::new(0.0, 0.0, 0.0),
Rgb565::new(10, 20, 10),
5.0,
));
set.add(PointLight::new(
Point3::new(0.0, 0.0, 0.0),
Rgb565::new(5, 10, 5),
5.0,
));
let tint = set.accumulate(Point3::new(0.0, 0.0, 0.0));
assert!(tint.r() >= 10);
}
#[test]
fn test_point_light_set_empty() {
let set: PointLightSet<4> = PointLightSet::new();
let tint = set.accumulate(Point3::new(0.0, 0.0, 0.0));
assert_eq!(tint.r(), 0);
assert_eq!(tint.g(), 0);
assert_eq!(tint.b(), 0);
}
#[test]
fn test_point_light_set_saturation() {
let mut set: PointLightSet<4> = PointLightSet::new();
set.add(PointLight::new(
Point3::new(0.0, 0.0, 0.0),
Rgb565::CSS_WHITE,
5.0,
));
set.add(PointLight::new(
Point3::new(0.0, 0.0, 0.0),
Rgb565::CSS_WHITE,
5.0,
));
let tint = set.accumulate(Point3::new(0.0, 0.0, 0.0));
assert_eq!(tint.r(), 31);
assert_eq!(tint.g(), 63);
assert_eq!(tint.b(), 31);
}
#[test]
fn test_point_light_analytical_falloff_math() {
let radius = 10.0f32;
let light = PointLight::new(Point3::new(0.0, 0.0, 0.0), Rgb565::new(31, 63, 31), radius)
.with_intensity(1.0);
let pos_half = Point3::new(5.0, 0.0, 0.0);
let tint = light.contribution_at(pos_half);
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);
assert_eq!(tint.g(), expected_g);
assert_eq!(tint.b(), expected_b);
}
}