use glam::Vec3;
use crate::color::Color;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum LightKind {
#[default]
Directional,
Point { radius: f32 },
Spot {
radius: f32,
inner_angle: f32,
outer_angle: f32,
},
}
#[derive(Debug, Clone, Copy)]
pub struct Light3D {
pub kind: LightKind,
pub position: Vec3,
pub direction: Vec3,
pub color: Color,
pub intensity: f32,
pub enabled: bool,
}
impl Default for Light3D {
fn default() -> Self {
Self {
kind: LightKind::Directional,
position: Vec3::ZERO,
direction: Vec3::new(0.0, -1.0, -1.0).normalize(),
color: Color::rgb(1.0, 1.0, 1.0),
intensity: 1.0,
enabled: true,
}
}
}
impl Light3D {
pub fn directional(direction: Vec3, color: Color) -> Self {
Self {
kind: LightKind::Directional,
direction: direction.normalize(),
color,
..Default::default()
}
}
pub fn point(position: Vec3, radius: f32, color: Color) -> Self {
Self {
kind: LightKind::Point { radius },
position,
color,
..Default::default()
}
}
pub fn spot(
position: Vec3,
direction: Vec3,
radius: f32,
inner_angle: f32,
outer_angle: f32,
color: Color,
) -> Self {
Self {
kind: LightKind::Spot {
radius,
inner_angle,
outer_angle,
},
position,
direction: direction.normalize(),
color,
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Material3D {
pub diffuse: Color,
pub specular: Color,
pub shininess: f32,
pub emissive: Color,
}
impl Default for Material3D {
fn default() -> Self {
Self {
diffuse: Color::rgb(1.0, 1.0, 1.0),
specular: Color::rgb(0.5, 0.5, 0.5),
shininess: 32.0,
emissive: Color::rgb(0.0, 0.0, 0.0),
}
}
}
impl Material3D {
pub fn color(diffuse: Color) -> Self {
Self {
diffuse,
..Default::default()
}
}
pub fn metallic(color: Color, shininess: f32) -> Self {
Self {
diffuse: color,
specular: color,
shininess,
emissive: Color::rgb(0.0, 0.0, 0.0),
}
}
pub fn emissive(color: Color) -> Self {
Self {
diffuse: color,
specular: Color::rgb(0.0, 0.0, 0.0),
shininess: 1.0,
emissive: color,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PbrMaterial {
pub albedo: Color,
pub metallic: f32,
pub roughness: f32,
pub ao: f32,
pub emissive: Color,
}
impl Default for PbrMaterial {
fn default() -> Self {
Self {
albedo: Color::rgb(1.0, 1.0, 1.0),
metallic: 0.0,
roughness: 0.5,
ao: 1.0,
emissive: Color::rgb(0.0, 0.0, 0.0),
}
}
}
impl PbrMaterial {
pub fn new(albedo: Color) -> Self {
Self {
albedo,
..Default::default()
}
}
pub fn metal(albedo: Color, roughness: f32) -> Self {
Self {
albedo,
metallic: 1.0,
roughness,
..Default::default()
}
}
pub fn dielectric(albedo: Color, roughness: f32) -> Self {
Self {
albedo,
metallic: 0.0,
roughness,
..Default::default()
}
}
pub fn smooth(albedo: Color, metallic: f32) -> Self {
Self {
albedo,
metallic,
roughness: 0.1,
..Default::default()
}
}
pub fn rough(albedo: Color, metallic: f32) -> Self {
Self {
albedo,
metallic,
roughness: 0.9,
..Default::default()
}
}
pub fn emissive(color: Color, intensity: f32) -> Self {
Self {
albedo: color,
metallic: 0.0,
roughness: 0.5,
ao: 1.0,
emissive: Color::rgb(
color.r * intensity,
color.g * intensity,
color.b * intensity,
),
}
}
pub fn with_metallic(mut self, metallic: f32) -> Self {
self.metallic = metallic.clamp(0.0, 1.0);
self
}
pub fn with_roughness(mut self, roughness: f32) -> Self {
self.roughness = roughness.clamp(0.0, 1.0);
self
}
pub fn with_ao(mut self, ao: f32) -> Self {
self.ao = ao.clamp(0.0, 1.0);
self
}
pub fn with_emissive(mut self, emissive: Color) -> Self {
self.emissive = emissive;
self
}
}
pub const MAX_LIGHTS: usize = 4;
#[derive(Debug, Clone)]
pub struct LightingState {
pub ambient: Color,
pub lights: [Option<Light3D>; MAX_LIGHTS],
}
impl Default for LightingState {
fn default() -> Self {
Self {
ambient: Color::rgb(0.1, 0.1, 0.1),
lights: [None; MAX_LIGHTS],
}
}
}
impl LightingState {
pub fn set_ambient(&mut self, color: Color) {
self.ambient = color;
}
pub fn set_light(&mut self, index: usize, light: Option<Light3D>) {
if index < MAX_LIGHTS {
self.lights[index] = light;
}
}
pub fn get_light(&self, index: usize) -> Option<&Light3D> {
if index < MAX_LIGHTS {
self.lights[index].as_ref()
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::{GOLD, GREEN, RED, WHITE};
#[test]
fn test_light_directional_normalizes_direction() {
let light = Light3D::directional(Vec3::new(10.0, 0.0, 0.0), WHITE);
assert!((light.direction.length() - 1.0).abs() < 1e-6);
assert_eq!(light.direction, Vec3::X);
}
#[test]
fn test_light_point() {
let light = Light3D::point(Vec3::new(1.0, 2.0, 3.0), 10.0, RED);
assert_eq!(light.position, Vec3::new(1.0, 2.0, 3.0));
assert!(matches!(light.kind, LightKind::Point { radius } if radius == 10.0));
assert_eq!(light.color.r, 1.0);
}
#[test]
fn test_light_spot() {
let light = Light3D::spot(
Vec3::new(0.0, 5.0, 0.0),
Vec3::new(0.0, -1.0, 0.0),
20.0,
0.5,
1.0,
WHITE,
);
assert_eq!(light.position, Vec3::new(0.0, 5.0, 0.0));
assert!((light.direction - Vec3::NEG_Y).length() < 1e-6);
assert!(matches!(
light.kind,
LightKind::Spot { radius, inner_angle, outer_angle }
if radius == 20.0 && inner_angle == 0.5 && outer_angle == 1.0
));
}
#[test]
fn test_light_default() {
let light = Light3D::default();
assert!(matches!(light.kind, LightKind::Directional));
assert!(light.enabled);
assert_eq!(light.intensity, 1.0);
}
#[test]
fn test_material3d_default() {
let mat = Material3D::default();
assert_eq!(mat.diffuse.r, 1.0);
assert_eq!(mat.shininess, 32.0);
}
#[test]
fn test_material3d_color() {
let mat = Material3D::color(RED);
assert_eq!(mat.diffuse.r, 1.0);
assert_eq!(mat.diffuse.g, 0.0);
}
#[test]
fn test_material3d_metallic() {
let mat = Material3D::metallic(GOLD, 64.0);
assert_eq!(mat.diffuse, mat.specular); assert_eq!(mat.shininess, 64.0);
}
#[test]
fn test_material3d_emissive() {
let mat = Material3D::emissive(GREEN);
assert_eq!(mat.emissive.g, mat.diffuse.g);
}
#[test]
fn test_pbr_material_default() {
let mat = PbrMaterial::default();
assert_eq!(mat.metallic, 0.0);
assert_eq!(mat.roughness, 0.5);
assert_eq!(mat.ao, 1.0);
}
#[test]
fn test_pbr_material_metal() {
let mat = PbrMaterial::metal(GOLD, 0.3);
assert_eq!(mat.metallic, 1.0);
assert_eq!(mat.roughness, 0.3);
}
#[test]
fn test_pbr_material_dielectric() {
let mat = PbrMaterial::dielectric(WHITE, 0.7);
assert_eq!(mat.metallic, 0.0);
assert_eq!(mat.roughness, 0.7);
}
#[test]
fn test_pbr_material_smooth() {
let mat = PbrMaterial::smooth(WHITE, 0.5);
assert_eq!(mat.roughness, 0.1);
}
#[test]
fn test_pbr_material_rough() {
let mat = PbrMaterial::rough(WHITE, 0.5);
assert_eq!(mat.roughness, 0.9);
}
#[test]
fn test_pbr_material_with_metallic_clamp() {
let mat = PbrMaterial::default()
.with_metallic(1.5) .with_metallic(-0.5); assert_eq!(mat.metallic, 0.0);
let mat2 = PbrMaterial::default().with_metallic(1.5);
assert_eq!(mat2.metallic, 1.0);
}
#[test]
fn test_pbr_material_with_roughness_clamp() {
let mat = PbrMaterial::default().with_roughness(2.0);
assert_eq!(mat.roughness, 1.0);
let mat2 = PbrMaterial::default().with_roughness(-1.0);
assert_eq!(mat2.roughness, 0.0);
}
#[test]
fn test_pbr_material_with_ao_clamp() {
let mat = PbrMaterial::default().with_ao(1.5);
assert_eq!(mat.ao, 1.0);
let mat2 = PbrMaterial::default().with_ao(-0.5);
assert_eq!(mat2.ao, 0.0);
}
#[test]
fn test_pbr_material_emissive() {
let mat = PbrMaterial::emissive(Color::rgb(1.0, 0.5, 0.0), 2.0);
assert_eq!(mat.emissive.r, 2.0);
assert_eq!(mat.emissive.g, 1.0);
assert_eq!(mat.emissive.b, 0.0);
}
#[test]
fn test_lighting_state_default() {
let state = LightingState::default();
assert!((state.ambient.r - 0.1).abs() < 0.01);
assert!(state.lights.iter().all(|l| l.is_none()));
}
#[test]
fn test_lighting_state_set_ambient() {
let mut state = LightingState::default();
state.set_ambient(Color::rgb(0.5, 0.5, 0.5));
assert_eq!(state.ambient.r, 0.5);
}
#[test]
fn test_lighting_state_set_get_light() {
let mut state = LightingState::default();
let light = Light3D::directional(Vec3::NEG_Y, WHITE);
state.set_light(0, Some(light));
assert!(state.get_light(0).is_some());
assert!(state.get_light(1).is_none());
state.set_light(0, None);
assert!(state.get_light(0).is_none());
}
#[test]
fn test_lighting_state_bounds_check() {
let mut state = LightingState::default();
let light = Light3D::default();
state.set_light(MAX_LIGHTS, Some(light));
assert!(state.get_light(MAX_LIGHTS).is_none());
for i in 0..MAX_LIGHTS {
state.set_light(i, Some(Light3D::default()));
assert!(state.get_light(i).is_some());
}
}
}