enginerenderer 0.0.1

A zero-dependency offline rendering engine in pure Rust — CPU path tracing, BVH acceleration, 16-band spectral rendering, PBR materials, animation & video export.
Documentation
use crate::core::engine::rendering::raytracing::Vec3;
use crate::core::engine::rendering::raytracing::hair_bsdf::HairMaterial;

#[derive(Debug, Clone)]
pub struct HairStrand {
    pub control_points: Vec<Vec3>,
    pub width_root: f64,
    pub width_tip: f64,
    pub material: HairMaterial,
}

impl HairStrand {
    pub fn new(control_points: Vec<Vec3>, width_root: f64, width_tip: f64) -> Self {
        Self {
            control_points,
            width_root,
            width_tip,
            material: HairMaterial::default(),
        }
    }

    pub fn segment_count(&self) -> usize {
        self.control_points.len().saturating_sub(1)
    }

    pub fn width_at(&self, t: f64) -> f64 {
        self.width_root + (self.width_tip - self.width_root) * t
    }

    pub fn tangent_at(&self, segment: usize) -> Vec3 {
        let a = self.control_points[segment];
        let b = self.control_points[segment + 1];
        let d = b - a;
        let len = d.length();
        if len > f64::EPSILON {
            d * (1.0 / len)
        } else {
            Vec3::new(0.0, 1.0, 0.0)
        }
    }

    pub fn catmull_rom(&self, t: f64) -> Vec3 {
        let n = self.control_points.len();
        if n < 2 {
            return self.control_points[0];
        }
        let seg_f = t * (n - 1) as f64;
        let i = (seg_f as usize).min(n - 2);
        let lt = seg_f - i as f64;
        let p0 = if i > 0 {
            self.control_points[i - 1]
        } else {
            self.control_points[0]
        };
        let p1 = self.control_points[i];
        let p2 = self.control_points[i + 1];
        let p3 = if i + 2 < n {
            self.control_points[i + 2]
        } else {
            self.control_points[n - 1]
        };
        let t2 = lt * lt;
        let t3 = t2 * lt;
        (p0 * (-0.5 * t3 + t2 - 0.5 * lt))
            + (p1 * (1.5 * t3 - 2.5 * t2 + 1.0))
            + (p2 * (-1.5 * t3 + 2.0 * t2 + 0.5 * lt))
            + (p3 * (0.5 * t3 - 0.5 * t2))
    }
}

#[derive(Debug, Clone)]
pub struct HairGroom {
    pub strands: Vec<HairStrand>,
    pub root_offset: Vec3,
}

impl HairGroom {
    pub fn new(strands: Vec<HairStrand>, root_offset: Vec3) -> Self {
        Self {
            strands,
            root_offset,
        }
    }

    pub fn strand_count(&self) -> usize {
        self.strands.len()
    }

    pub fn total_segments(&self) -> usize {
        self.strands.iter().map(|s| s.segment_count()).sum()
    }

    pub fn apply_gravity(&mut self, gravity: Vec3, stiffness: f64, dt: f64) {
        for strand in &mut self.strands {
            let n = strand.control_points.len();
            for i in 1..n {
                let t = i as f64 / (n - 1) as f64;
                strand.control_points[i] += gravity * (1.0 - stiffness) * t * dt;
            }
        }
    }

    pub fn rasterize_to_buffer(
        &self,
        fb: &mut [[f32; 4]],
        width: usize,
        height: usize,
        view_proj: &[[f32; 4]; 4],
    ) {
        for strand in &self.strands {
            let steps = strand.segment_count() * 4;
            if steps == 0 {
                continue;
            }
            for step in 0..steps {
                let t = step as f64 / steps as f64;
                let pos = strand.catmull_rom(t);
                let clip =
                    transform_vec(view_proj, [pos.x as f32, pos.y as f32, pos.z as f32, 1.0]);
                if clip[3] <= 0.0 {
                    continue;
                }
                let nx = clip[0] / clip[3];
                let ny = clip[1] / clip[3];
                let sx = ((nx + 1.0) * 0.5 * width as f32) as usize;
                let sy = ((1.0 - ny) * 0.5 * height as f32) as usize;
                let w = (strand.width_at(t) * 0.5) as usize + 1;
                let mat = strand.material;
                let color = [
                    mat.melanin as f32 * 0.5,
                    mat.melanin as f32 * 0.3,
                    (1.0 - mat.melanin_redness) as f32 * 0.1,
                    1.0,
                ];
                for dy in 0..w {
                    for dx in 0..w {
                        let px = sx.wrapping_add(dx).wrapping_sub(w / 2);
                        let py = sy.wrapping_add(dy).wrapping_sub(w / 2);
                        if px < width && py < height {
                            fb[py * width + px] = color;
                        }
                    }
                }
            }
        }
    }
}

fn transform_vec(m: &[[f32; 4]; 4], v: [f32; 4]) -> [f32; 4] {
    [
        m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2] + m[0][3] * v[3],
        m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2] + m[1][3] * v[3],
        m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2] + m[2][3] * v[3],
        m[3][0] * v[0] + m[3][1] * v[1] + m[3][2] * v[2] + m[3][3] * v[3],
    ]
}