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 super::asset::MeshAsset;
use super::vertex::Vertex;
use crate::core::engine::rendering::raytracing::Vec3;

pub const CLUSTER_SIZE: usize = 128;

#[derive(Debug, Clone)]
pub struct MeshCluster {
    pub triangle_start: usize,
    pub triangle_count: usize,
    pub bounds_center: Vec3,
    pub bounds_radius: f64,
    pub lod_error: f64,
}

#[derive(Debug, Clone)]
pub struct ClusteredMesh {
    pub vertices: Vec<Vertex>,
    pub indices: Vec<usize>,
    pub clusters: Vec<MeshCluster>,
}

impl ClusteredMesh {
    pub fn build(asset: &MeshAsset) -> Self {
        let tri_count = asset.indices.len() / 3;
        let mut clusters = Vec::new();
        let cluster_tri_count = CLUSTER_SIZE;

        let mut start = 0;
        while start < tri_count {
            let count = (cluster_tri_count).min(tri_count - start);
            let mut bounds_min = Vec3::new(f64::MAX, f64::MAX, f64::MAX);
            let mut bounds_max = Vec3::new(f64::MIN, f64::MIN, f64::MIN);
            for ti in start..start + count {
                for &vi in &asset.indices[ti * 3..ti * 3 + 3] {
                    let p = asset.vertices[vi].position;
                    bounds_min.x = bounds_min.x.min(p.x);
                    bounds_min.y = bounds_min.y.min(p.y);
                    bounds_min.z = bounds_min.z.min(p.z);
                    bounds_max.x = bounds_max.x.max(p.x);
                    bounds_max.y = bounds_max.y.max(p.y);
                    bounds_max.z = bounds_max.z.max(p.z);
                }
            }
            let center = (bounds_min + bounds_max) * 0.5;
            let radius = (bounds_max - bounds_min).length() * 0.5;
            clusters.push(MeshCluster {
                triangle_start: start,
                triangle_count: count,
                bounds_center: center,
                bounds_radius: radius,
                lod_error: 0.0,
            });
            start += count;
        }

        Self {
            vertices: asset.vertices.clone(),
            indices: asset.indices.clone(),
            clusters,
        }
    }

    pub fn simplify(&self, target_tri_count: usize) -> MeshAsset {
        let original_tri = self.indices.len() / 3;
        if target_tri_count >= original_tri {
            return MeshAsset {
                name: String::new(),
                descriptor: crate::core::engine::rendering::mesh::vertex::MeshDescriptor {
                    vertex_count: self.vertices.len(),
                    triangle_count: original_tri,
                    bounding_radius: 1.0,
                },
                vertices: self.vertices.clone(),
                indices: self.indices.clone(),
                preferred_material: None,
                base_translation: Vec3::ZERO,
                base_scale: Vec3::new(1.0, 1.0, 1.0),
                base_rotation: [0.0, 0.0, 0.0, 1.0],
            };
        }

        let ratio = target_tri_count as f64 / original_tri as f64;
        let kept_clusters = ((self.clusters.len() as f64 * ratio).ceil() as usize).max(1);
        let mut new_indices = Vec::new();
        for cluster in self.clusters.iter().take(kept_clusters) {
            let start = cluster.triangle_start * 3;
            let end = (cluster.triangle_start + cluster.triangle_count) * 3;
            new_indices.extend_from_slice(&self.indices[start..end]);
        }

        let new_tri = new_indices.len() / 3;
        MeshAsset {
            name: String::new(),
            descriptor: crate::core::engine::rendering::mesh::vertex::MeshDescriptor {
                vertex_count: self.vertices.len(),
                triangle_count: new_tri,
                bounding_radius: 1.0,
            },
            vertices: self.vertices.clone(),
            indices: new_indices,
            preferred_material: None,
            base_translation: Vec3::ZERO,
            base_scale: Vec3::new(1.0, 1.0, 1.0),
            base_rotation: [0.0, 0.0, 0.0, 1.0],
        }
    }

    pub fn select_lod(
        &self,
        screen_error_threshold: f64,
        camera_dist: f64,
        object_radius: f64,
    ) -> Vec<usize> {
        let projected_error = object_radius / camera_dist.max(f64::EPSILON);
        let mut visible = Vec::new();
        for (i, cluster) in self.clusters.iter().enumerate() {
            let cluster_error = cluster.lod_error + projected_error;
            if cluster_error >= screen_error_threshold || cluster.lod_error == 0.0 {
                visible.push(i);
            }
        }
        visible
    }
}

pub struct ClusterLodChain {
    pub levels: Vec<ClusteredMesh>,
}

impl ClusterLodChain {
    pub fn build(asset: &MeshAsset, lod_count: usize) -> Self {
        let base = ClusteredMesh::build(asset);
        let tri_count = asset.indices.len() / 3;
        let mut levels = vec![base];
        for level in 1..lod_count {
            let factor = 1.0 / (1 << level) as f64;
            let target = ((tri_count as f64 * factor) as usize).max(4);
            let simplified = levels[level - 1].simplify(target);
            levels.push(ClusteredMesh::build(&simplified));
        }
        Self { levels }
    }

    pub fn level_count(&self) -> usize {
        self.levels.len()
    }

    pub fn get_level(&self, camera_dist: f64, object_radius: f64) -> &ClusteredMesh {
        let projected = object_radius / camera_dist.max(f64::EPSILON);
        let level = if projected > 0.05 {
            0
        } else if projected > 0.02 {
            1
        } else if projected > 0.008 {
            2
        } else {
            self.levels.len() - 1
        };
        &self.levels[level.min(self.levels.len() - 1)]
    }
}