atanor 0.1.0

Motor 3D ray-traced que vive solo y exclusivamente en la terminal.
Documentation
use crate::math::{Hit, Ray};
use glam::Vec3;

#[derive(Clone, Copy)]
pub struct Material {
    pub albedo: Vec3,
    pub specular: f32,
    pub shininess: f32,
    pub reflectivity: f32,
    /// Patrón procedural: 0 = sólido, 1 = ajedrez (para el suelo).
    pub pattern: u8,
}

impl Material {
    pub const fn lambert(albedo: Vec3) -> Self {
        Self { albedo, specular: 0.1, shininess: 8.0, reflectivity: 0.0, pattern: 0 }
    }

    pub const fn shiny(albedo: Vec3, reflectivity: f32) -> Self {
        Self { albedo, specular: 0.6, shininess: 64.0, reflectivity, pattern: 0 }
    }

    pub const fn checker(a: Vec3) -> Self {
        Self { albedo: a, specular: 0.05, shininess: 4.0, reflectivity: 0.0, pattern: 1 }
    }

    pub fn sample_albedo(&self, p: Vec3) -> Vec3 {
        match self.pattern {
            1 => {
                let s = 1.0;
                let ix = (p.x * s).floor() as i32;
                let iz = (p.z * s).floor() as i32;
                if (ix + iz).rem_euclid(2) == 0 {
                    self.albedo
                } else {
                    self.albedo * 0.25
                }
            }
            _ => self.albedo,
        }
    }
}

pub struct Sphere {
    pub center: Vec3,
    pub radius: f32,
    pub material: usize,
}

impl Sphere {
    pub fn hit(&self, r: &Ray, t_min: f32, t_max: f32) -> Option<Hit> {
        let oc = r.origin - self.center;
        let b = oc.dot(r.dir);
        let c = oc.dot(oc) - self.radius * self.radius;
        let disc = b * b - c;
        if disc < 0.0 {
            return None;
        }
        let sq = disc.sqrt();
        let mut t = -b - sq;
        if t < t_min || t > t_max {
            t = -b + sq;
            if t < t_min || t > t_max {
                return None;
            }
        }
        let point = r.at(t);
        let normal = (point - self.center) / self.radius;
        Some(Hit { t, point, normal, material: self.material })
    }
}

pub struct Plane {
    pub point: Vec3,
    pub normal: Vec3,
    pub material: usize,
}

impl Plane {
    pub fn hit(&self, r: &Ray, t_min: f32, t_max: f32) -> Option<Hit> {
        let denom = self.normal.dot(r.dir);
        if denom.abs() < 1e-5 {
            return None;
        }
        let t = (self.point - r.origin).dot(self.normal) / denom;
        if t < t_min || t > t_max {
            return None;
        }
        let n = if denom < 0.0 { self.normal } else { -self.normal };
        Some(Hit { t, point: r.at(t), normal: n, material: self.material })
    }
}

pub struct Light {
    pub position: Vec3,
    pub color: Vec3,
    pub intensity: f32,
}

pub struct Scene {
    pub spheres: Vec<Sphere>,
    pub planes: Vec<Plane>,
    pub materials: Vec<Material>,
    pub lights: Vec<Light>,
    pub ambient: Vec3,
    pub sky_top: Vec3,
    pub sky_bottom: Vec3,
}

impl Scene {
    pub fn hit(&self, r: &Ray, t_min: f32, t_max: f32) -> Option<Hit> {
        let mut closest = t_max;
        let mut best: Option<Hit> = None;
        for s in &self.spheres {
            if let Some(h) = s.hit(r, t_min, closest) {
                closest = h.t;
                best = Some(h);
            }
        }
        for p in &self.planes {
            if let Some(h) = p.hit(r, t_min, closest) {
                closest = h.t;
                best = Some(h);
            }
        }
        best
    }

    pub fn sky(&self, dir: Vec3) -> Vec3 {
        let t = (dir.y * 0.5 + 0.5).clamp(0.0, 1.0);
        self.sky_bottom.lerp(self.sky_top, t)
    }

    pub fn demo() -> Self {
        let materials = vec![
            Material::checker(Vec3::new(0.85, 0.85, 0.85)),               // 0 suelo
            Material::shiny(Vec3::new(0.95, 0.25, 0.35), 0.35),          // 1 rojo brillante
            Material::shiny(Vec3::new(0.20, 0.55, 0.95), 0.45),          // 2 azul
            Material::lambert(Vec3::new(0.95, 0.85, 0.20)),              // 3 amarillo mate
            Material::shiny(Vec3::new(0.85, 0.85, 0.90), 0.85),          // 4 espejo
        ];
        let spheres = vec![
            Sphere { center: Vec3::new(0.0, 1.0, 0.0), radius: 1.0, material: 1 },
            Sphere { center: Vec3::new(-2.4, 0.8, -0.5), radius: 0.8, material: 2 },
            Sphere { center: Vec3::new(2.0, 0.6, 0.8), radius: 0.6, material: 3 },
            Sphere { center: Vec3::new(0.6, 0.4, 2.2), radius: 0.4, material: 4 },
        ];
        let planes = vec![
            Plane { point: Vec3::ZERO, normal: Vec3::Y, material: 0 },
        ];
        let lights = vec![
            Light { position: Vec3::new(4.0, 6.0, 3.0), color: Vec3::new(1.0, 0.95, 0.85), intensity: 1.2 },
            Light { position: Vec3::new(-5.0, 4.0, -2.0), color: Vec3::new(0.5, 0.6, 1.0), intensity: 0.6 },
        ];
        Self {
            spheres,
            planes,
            materials,
            lights,
            ambient: Vec3::new(0.08, 0.09, 0.12),
            sky_top: Vec3::new(0.45, 0.65, 0.95),
            sky_bottom: Vec3::new(0.85, 0.80, 0.70),
        }
    }
}