use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
use nalgebra::{Point3, Vector3};
#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use micromath::F32Ext;
#[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(
crate::simd_dsp::clamp_u5(r as i32),
crate::simd_dsp::clamp_u6(g as i32),
crate::simd_dsp::clamp_u5(b as i32),
)
}
pub fn accumulate_tonemapped(&self, world_pos: Point3<f32>, half_exposure: u32) -> 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;
}
tonemap_reinhard_rgb565(r, g, b, half_exposure)
}
}
impl<const N: usize> Default for PointLightSet<N> {
fn default() -> Self {
Self::new()
}
}
#[inline]
pub fn windowed_distance_attenuation(dist_sq: f32, radius: f32) -> f32 {
let r_sq = radius * radius;
if dist_sq >= r_sq || radius <= 1e-4 {
return 0.0;
}
let ratio_sq = dist_sq / r_sq;
let ratio_4 = ratio_sq * ratio_sq;
let window = (1.0 - ratio_4).max(0.0);
(window * window) / dist_sq.max(1.0)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SpotLight {
pub position: Point3<f32>,
pub direction: Vector3<f32>,
pub color: Rgb565,
pub range: f32,
pub intensity: f32,
pub inner_angle: f32,
pub outer_angle: f32,
}
impl SpotLight {
pub fn new(
position: Point3<f32>,
direction: Vector3<f32>,
color: Rgb565,
range: f32,
inner_angle: f32,
outer_angle: f32,
) -> Self {
Self {
position,
direction: direction.normalize(),
color,
range,
intensity: 1.0,
inner_angle,
outer_angle,
}
}
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 to_pos = world_pos - self.position;
let dist_sq = to_pos.dot(&to_pos);
let range_sq = self.range * self.range;
if dist_sq >= range_sq || dist_sq < 1e-6 {
return Rgb565::new(0, 0, 0);
}
let dist = dist_sq.sqrt();
let dir_to_pos = to_pos / dist;
let cos_theta = dir_to_pos.dot(&self.direction);
let cos_outer = self.outer_angle.cos();
let cos_inner = self.inner_angle.cos();
if cos_theta <= cos_outer {
return Rgb565::new(0, 0, 0);
}
let spot_factor = if cos_inner > cos_outer {
((cos_theta - cos_outer) / (cos_inner - cos_outer)).clamp(0.0, 1.0)
} else {
1.0
};
let spot_factor = spot_factor * spot_factor;
let dist_factor = (1.0 - dist_sq / range_sq).max(0.0);
let factor = spot_factor * dist_factor * 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 SpotLightSet<const N: usize> {
pub lights: heapless::Vec<SpotLight, N>,
}
impl<const N: usize> SpotLightSet<N> {
pub const fn new() -> Self {
Self {
lights: heapless::Vec::new(),
}
}
pub fn add(&mut self, light: SpotLight) -> 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(
crate::simd_dsp::clamp_u5(r as i32),
crate::simd_dsp::clamp_u6(g as i32),
crate::simd_dsp::clamp_u5(b as i32),
)
}
pub fn accumulate_tonemapped(&self, world_pos: Point3<f32>, half_exposure: u32) -> 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;
}
tonemap_reinhard_rgb565(r, g, b, half_exposure)
}
}
#[inline]
pub fn reinhard_tonemap(val: f32) -> f32 {
if val <= 0.0 { 0.0 } else { val / (1.0 + val) }
}
#[inline]
pub fn reinhard_extended_tonemap(val: f32, white_point: f32) -> f32 {
if val <= 0.0 {
return 0.0;
}
let w_sq = white_point * white_point;
if w_sq <= 1e-4 {
return val;
}
(val * (1.0 + val / w_sq)) / (1.0 + val)
}
#[inline]
pub fn rational_exposure_tonemap(val: f32, exposure: f32) -> f32 {
if val <= 0.0 || exposure <= 0.0 {
return 0.0;
}
let scaled = val * exposure;
scaled / (1.0 + scaled)
}
#[inline]
pub fn tonemap_reinhard_rgb565(raw_r: u32, raw_g: u32, raw_b: u32, half_exposure: u32) -> Rgb565 {
let half_exposure = half_exposure.max(1);
let r = ((raw_r * 31) / (raw_r + half_exposure)).min(31) as u8;
let g = ((raw_g * 63) / (raw_g + half_exposure)).min(63) as u8;
let b = ((raw_b * 31) / (raw_b + half_exposure)).min(31) as u8;
Rgb565::new(r, g, b)
}
impl<const N: usize> Default for SpotLightSet<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);
}
#[test]
fn test_windowed_distance_attenuation() {
assert_eq!(windowed_distance_attenuation(100.0, 10.0), 0.0);
assert_eq!(windowed_distance_attenuation(0.0, 10.0), 1.0);
assert!(windowed_distance_attenuation(25.0, 10.0) > 0.0);
}
#[test]
fn test_spot_light_inner_and_outer_cone() {
let spot = SpotLight::new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Rgb565::CSS_WHITE,
10.0,
core::f32::consts::FRAC_PI_6, core::f32::consts::FRAC_PI_4, );
let tint_center = spot.contribution_at(Point3::new(0.0, 0.0, 2.0));
assert!(tint_center.r() > 0);
let tint_behind = spot.contribution_at(Point3::new(0.0, 0.0, -2.0));
assert_eq!(tint_behind.r(), 0);
let tint_side = spot.contribution_at(Point3::new(2.0, 0.0, 0.0));
assert_eq!(tint_side.r(), 0);
}
#[test]
fn test_tonemapping_curves() {
assert_eq!(reinhard_tonemap(0.0), 0.0);
assert!((reinhard_tonemap(1.0) - 0.5).abs() < 1e-4);
assert!(reinhard_tonemap(100.0) < 1.0);
assert_eq!(reinhard_extended_tonemap(0.0, 2.0), 0.0);
assert!((reinhard_extended_tonemap(2.0, 2.0) - 1.0).abs() < 1e-4);
assert_eq!(rational_exposure_tonemap(0.0, 1.0), 0.0);
assert_eq!(rational_exposure_tonemap(1.0, 0.0), 0.0);
assert!((rational_exposure_tonemap(1.0, 1.0) - 0.5).abs() < 1e-4);
assert!((rational_exposure_tonemap(2.0, 0.5) - 0.5).abs() < 1e-4);
}
#[test]
fn test_tonemap_reinhard_rgb565() {
let zero = tonemap_reinhard_rgb565(0, 0, 0, 31);
assert_eq!(zero.r(), 0);
assert_eq!(zero.g(), 0);
assert_eq!(zero.b(), 0);
let half = tonemap_reinhard_rgb565(31, 31, 31, 31);
assert_eq!(half.r(), 15);
assert_eq!(half.g(), 31);
assert_eq!(half.b(), 15);
let high = tonemap_reinhard_rgb565(1000, 1000, 1000, 31);
assert_eq!(high.r(), 30);
assert_eq!(high.g(), 61);
assert_eq!(high.b(), 30);
}
}