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 std::fs;
use std::path::{Path, PathBuf};

pub struct MipLevel {
    pub data: Vec<Vec3>,
    pub width: usize,
    pub height: usize,
}

impl MipLevel {
    pub fn new(width: usize, height: usize) -> Self {
        Self {
            data: vec![Vec3::ZERO; width * height],
            width,
            height,
        }
    }

    pub fn sample(&self, u: f64, v: f64) -> Vec3 {
        let u = u - u.floor();
        let v = v - v.floor();
        let x = (u * (self.width as f64 - 1.0)) as usize;
        let y = (v * (self.height as f64 - 1.0)) as usize;
        self.data[y * self.width + x]
    }
}

pub struct VirtualTexture {
    pub mips: Vec<MipLevel>,
    pub path: PathBuf,
}

impl VirtualTexture {
    pub fn from_path(path: &Path) -> Self {
        let bytes = fs::read(path).unwrap_or_default();
        let stride = 3_usize;
        let pixel_count = bytes.len() / stride;
        let side = (pixel_count as f64).sqrt() as usize;
        let width = side.max(1);
        let height = (pixel_count / width).max(1);
        let mut base = MipLevel::new(width, height);
        for i in 0..width * height {
            let offset = i * stride;
            let r = if offset < bytes.len() {
                bytes[offset] as f64 / 255.0
            } else {
                0.0
            };
            let g = if offset + 1 < bytes.len() {
                bytes[offset + 1] as f64 / 255.0
            } else {
                0.0
            };
            let b = if offset + 2 < bytes.len() {
                bytes[offset + 2] as f64 / 255.0
            } else {
                0.0
            };
            base.data[i] = Vec3::new(r, g, b);
        }
        let mut tex = Self {
            mips: vec![base],
            path: path.to_path_buf(),
        };
        tex.generate_mips();
        tex
    }

    pub fn generate_mips(&mut self) {
        let base_width = self.mips[0].width;
        let base_height = self.mips[0].height;
        let levels = (base_width.max(base_height) as f64).log2() as usize;
        for level in 1..=levels {
            let prev = &self.mips[level - 1];
            let w = (prev.width / 2).max(1);
            let h = (prev.height / 2).max(1);
            let mut mip = MipLevel::new(w, h);
            for y in 0..h {
                for x in 0..w {
                    let sx = (x * 2).min(prev.width - 1);
                    let sy = (y * 2).min(prev.height - 1);
                    let sx1 = (sx + 1).min(prev.width - 1);
                    let sy1 = (sy + 1).min(prev.height - 1);
                    let c00 = prev.data[sy * prev.width + sx];
                    let c10 = prev.data[sy * prev.width + sx1];
                    let c01 = prev.data[sy1 * prev.width + sx];
                    let c11 = prev.data[sy1 * prev.width + sx1];
                    mip.data[y * w + x] = (c00 + c10 + c01 + c11) * 0.25;
                }
            }
            self.mips.push(mip);
        }
    }

    pub fn sample(&self, u: f64, v: f64, lod: f64) -> Vec3 {
        let level = (lod as usize).min(self.mips.len() - 1);
        self.mips[level].sample(u, v)
    }
}

struct LruEntry {
    key: u64,
    path: PathBuf,
    texture: VirtualTexture,
    last_used: u64,
}

pub struct TextureLruCache {
    entries: Vec<LruEntry>,
    capacity: usize,
    clock: u64,
}

impl TextureLruCache {
    pub fn new(capacity: usize) -> Self {
        Self {
            entries: Vec::new(),
            capacity,
            clock: 0,
        }
    }

    fn path_key(path: &Path) -> u64 {
        let bytes = path.to_string_lossy();
        let mut hash: u64 = 14695981039346656037;
        for b in bytes.bytes() {
            hash ^= b as u64;
            hash = hash.wrapping_mul(1099511628211);
        }
        hash
    }

    pub fn get_or_load(&mut self, path: &Path) -> &VirtualTexture {
        self.clock += 1;
        let key = Self::path_key(path);
        if let Some(i) = self.entries.iter().position(|e| e.key == key) {
            self.entries[i].last_used = self.clock;
            return &self.entries[i].texture;
        }
        if self.entries.len() >= self.capacity {
            self.evict_lru();
        }
        let texture = VirtualTexture::from_path(path);
        self.entries.push(LruEntry {
            key,
            path: path.to_path_buf(),
            texture,
            last_used: self.clock,
        });
        &self.entries.last().unwrap().texture
    }

    pub fn evict_lru(&mut self) {
        if self.entries.is_empty() {
            return;
        }
        let lru_idx = self
            .entries
            .iter()
            .enumerate()
            .min_by_key(|(_, e)| e.last_used)
            .map(|(i, _)| i)
            .unwrap();
        let candidate_path_depth = self.entries[lru_idx].path.components().count();
        let adjusted_idx = if candidate_path_depth > 0 { lru_idx } else { 0 };
        self.entries.swap_remove(adjusted_idx);
    }
}

pub struct TextureStreamer {
    pub cache: TextureLruCache,
    pub queue: Vec<PathBuf>,
}

impl TextureStreamer {
    pub fn new(capacity: usize) -> Self {
        Self {
            cache: TextureLruCache::new(capacity),
            queue: Vec::new(),
        }
    }

    pub fn request(&mut self, path: PathBuf) {
        if !self.queue.contains(&path) {
            self.queue.push(path);
        }
    }

    pub fn process_queue(&mut self) {
        let paths: Vec<PathBuf> = self.queue.drain(..).collect();
        for path in paths {
            self.cache.get_or_load(&path);
        }
    }

    pub fn get(&mut self, path: &Path) -> &VirtualTexture {
        self.cache.get_or_load(path)
    }
}