use bytemuck::{Pod, Zeroable};
use crate::Color;
use crate::math::Vec3;
pub const MAX_LIGHTS: usize = 64;
pub const MAX_SHADOWS: usize = 4;
pub(crate) const NO_SHADOW: i32 = -1;
const NARROWEST_CONE: f32 = 0.01;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Light {
pub(crate) position: Vec3,
pub(crate) kind: Kind,
pub(crate) direction: Vec3,
pub(crate) range: f32,
pub(crate) color: Vec3,
pub(crate) cone: f32,
pub(crate) casts: bool,
}
impl Light {
pub fn directional(direction: Vec3, color: Color) -> Self {
Self {
position: Vec3::ZERO,
kind: Kind::Directional,
direction: direction.normalize_or_zero(),
range: 0.0,
color: rgb(color),
cone: 0.0,
casts: false,
}
}
pub fn point(position: Vec3, color: Color, range: f32) -> Self {
Self {
position,
kind: Kind::Point,
direction: Vec3::ZERO,
range: range.max(f32::EPSILON),
color: rgb(color),
cone: 0.0,
casts: false,
}
}
pub fn spot(spot: Spot) -> Self {
Self {
direction: spot.direction.normalize_or_zero(),
kind: Kind::Spot,
cone: spot.angle.max(NARROWEST_CONE).cos(),
..Self::point(spot.position, spot.color, spot.range)
}
}
#[must_use]
pub fn shadow(mut self) -> Self {
self.casts = true;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Spot {
pub position: Vec3,
pub direction: Vec3,
pub color: Color,
pub range: f32,
pub angle: f32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u32)]
pub(crate) enum Kind {
Directional = 0,
Point = 1,
Spot = 2,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub(crate) struct GpuLight {
position: Vec3,
kind: u32,
direction: Vec3,
range: f32,
color: Vec3,
cone: f32,
pub(crate) shadow: i32,
_padding: [u32; 3],
}
impl GpuLight {
pub(crate) fn new(light: &Light, shadow: i32) -> Self {
Self {
position: light.position,
kind: light.kind as u32,
direction: light.direction,
range: light.range,
color: light.color,
cone: light.cone,
shadow,
_padding: [0; 3],
}
}
}
pub(crate) fn default_lights() -> [Light; 1] {
[Light::directional(
Vec3::new(-0.4, -1.0, -0.6),
Color::WHITE,
)]
}
pub(crate) fn rgb(color: Color) -> Vec3 {
Vec3::new(color.red, color.green, color.blue)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_light_stays_the_size_the_shader_steps_by() {
assert_eq!(size_of::<GpuLight>(), 64);
}
#[test]
fn a_light_casts_nothing_until_it_is_asked_to() {
let sun = Light::directional(Vec3::NEG_Y, Color::WHITE);
assert!(!sun.casts);
assert!(sun.shadow().casts);
assert_eq!(
GpuLight::new(&sun, NO_SHADOW).shadow,
NO_SHADOW,
"and reads no map until a frame gives it one"
);
assert!(
default_lights().iter().all(|light| !light.casts),
"no default light casts, so a frame that submits no lights \
still builds no caster batches"
);
}
}