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::lod::manager::LodManager;
use crate::core::engine::rendering::raytracing::shading::{TraceContext, make_seed, trace_ray};
use crate::core::engine::rendering::raytracing::{Ray, Scene, Vec3};

pub const DDGI_IRRADIANCE_TEXELS: usize = 8;
pub const DDGI_VISIBILITY_TEXELS: usize = 16;
pub const DDGI_RAYS_PER_PROBE: usize = 64;

#[derive(Debug, Clone, Copy)]
pub struct ProbeGrid {
    pub origin: Vec3,
    pub spacing: Vec3,
    pub counts: [u32; 3],
}

impl ProbeGrid {
    pub fn new(origin: Vec3, spacing: Vec3, counts: [u32; 3]) -> Self {
        Self {
            origin,
            spacing,
            counts,
        }
    }

    pub fn probe_count(&self) -> usize {
        (self.counts[0] * self.counts[1] * self.counts[2]) as usize
    }

    pub fn probe_position(&self, index: usize) -> Vec3 {
        let nx = self.counts[0] as usize;
        let ny = self.counts[1] as usize;
        let x = index % nx;
        let y = (index / nx) % ny;
        let z = index / (nx * ny);
        self.origin
            + Vec3::new(
                x as f64 * self.spacing.x,
                y as f64 * self.spacing.y,
                z as f64 * self.spacing.z,
            )
    }

    pub fn find_enclosing_probes(&self, pos: Vec3) -> [usize; 8] {
        let local = pos - self.origin;
        let fx = (local.x / self.spacing.x).max(0.0);
        let fy = (local.y / self.spacing.y).max(0.0);
        let fz = (local.z / self.spacing.z).max(0.0);
        let ix = (fx as usize).min(self.counts[0] as usize - 2);
        let iy = (fy as usize).min(self.counts[1] as usize - 2);
        let iz = (fz as usize).min(self.counts[2] as usize - 2);
        let nx = self.counts[0] as usize;
        let ny = self.counts[1] as usize;
        [
            ix + iy * nx + iz * nx * ny,
            (ix + 1) + iy * nx + iz * nx * ny,
            ix + (iy + 1) * nx + iz * nx * ny,
            (ix + 1) + (iy + 1) * nx + iz * nx * ny,
            ix + iy * nx + (iz + 1) * nx * ny,
            (ix + 1) + iy * nx + (iz + 1) * nx * ny,
            ix + (iy + 1) * nx + (iz + 1) * nx * ny,
            (ix + 1) + (iy + 1) * nx + (iz + 1) * nx * ny,
        ]
    }

    pub fn trilinear_weights(&self, pos: Vec3) -> [f64; 8] {
        let local = pos - self.origin;
        let tx = (local.x / self.spacing.x).fract().clamp(0.0, 1.0);
        let ty = (local.y / self.spacing.y).fract().clamp(0.0, 1.0);
        let tz = (local.z / self.spacing.z).fract().clamp(0.0, 1.0);
        [
            (1.0 - tx) * (1.0 - ty) * (1.0 - tz),
            tx * (1.0 - ty) * (1.0 - tz),
            (1.0 - tx) * ty * (1.0 - tz),
            tx * ty * (1.0 - tz),
            (1.0 - tx) * (1.0 - ty) * tz,
            tx * (1.0 - ty) * tz,
            (1.0 - tx) * ty * tz,
            tx * ty * tz,
        ]
    }
}

#[derive(Debug, Clone)]
pub struct IrradianceProbe {
    pub irradiance: [[Vec3; DDGI_IRRADIANCE_TEXELS]; DDGI_IRRADIANCE_TEXELS],
    pub mean_distance: [[f64; DDGI_VISIBILITY_TEXELS]; DDGI_VISIBILITY_TEXELS],
    pub mean_distance_sq: [[f64; DDGI_VISIBILITY_TEXELS]; DDGI_VISIBILITY_TEXELS],
}

impl Default for IrradianceProbe {
    fn default() -> Self {
        Self {
            irradiance: [[Vec3::ZERO; DDGI_IRRADIANCE_TEXELS]; DDGI_IRRADIANCE_TEXELS],
            mean_distance: [[0.0; DDGI_VISIBILITY_TEXELS]; DDGI_VISIBILITY_TEXELS],
            mean_distance_sq: [[0.0; DDGI_VISIBILITY_TEXELS]; DDGI_VISIBILITY_TEXELS],
        }
    }
}

impl IrradianceProbe {
    pub fn sample_irradiance(&self, direction: Vec3) -> Vec3 {
        let (u, v) = oct_encode(direction.normalize());
        let tx = ((u * 0.5 + 0.5) * (DDGI_IRRADIANCE_TEXELS - 1) as f64)
            .clamp(0.0, (DDGI_IRRADIANCE_TEXELS - 1) as f64);
        let ty = ((v * 0.5 + 0.5) * (DDGI_IRRADIANCE_TEXELS - 1) as f64)
            .clamp(0.0, (DDGI_IRRADIANCE_TEXELS - 1) as f64);
        let base = bilinear_irradiance(&self.irradiance, tx, ty);
        let vx = (tx * (DDGI_VISIBILITY_TEXELS - 1) as f64
            / (DDGI_IRRADIANCE_TEXELS - 1).max(1) as f64)
            .clamp(0.0, (DDGI_VISIBILITY_TEXELS - 1) as f64) as usize;
        let vy = (ty * (DDGI_VISIBILITY_TEXELS - 1) as f64
            / (DDGI_IRRADIANCE_TEXELS - 1).max(1) as f64)
            .clamp(0.0, (DDGI_VISIBILITY_TEXELS - 1) as f64) as usize;
        let mean_d = self.mean_distance[vy][vx];
        let mean_d_sq = self.mean_distance_sq[vy][vx];
        let variance = (mean_d_sq - mean_d * mean_d).max(0.0);
        let visibility = if variance < 1e-6 {
            1.0
        } else {
            (variance / (variance + mean_d * mean_d * 0.02)).clamp(0.1, 1.0)
        };
        base * visibility
    }

    pub fn update_from_rays(&mut self, rays: &[ProbeRaySample], hysteresis: f64) {
        let mut acc = [[Vec3::ZERO; DDGI_IRRADIANCE_TEXELS]; DDGI_IRRADIANCE_TEXELS];
        let mut weights = [[0.0_f64; DDGI_IRRADIANCE_TEXELS]; DDGI_IRRADIANCE_TEXELS];
        let mut acc_dist = [[0.0_f64; DDGI_VISIBILITY_TEXELS]; DDGI_VISIBILITY_TEXELS];
        let mut acc_dist_sq = [[0.0_f64; DDGI_VISIBILITY_TEXELS]; DDGI_VISIBILITY_TEXELS];
        let mut vis_counts = [[0.0_f64; DDGI_VISIBILITY_TEXELS]; DDGI_VISIBILITY_TEXELS];

        for ray in rays {
            let (u, v) = oct_encode(ray.direction);
            let tx = ((u * 0.5 + 0.5) * (DDGI_IRRADIANCE_TEXELS - 1) as f64)
                .clamp(0.0, (DDGI_IRRADIANCE_TEXELS - 1) as f64);
            let ty = ((v * 0.5 + 0.5) * (DDGI_IRRADIANCE_TEXELS - 1) as f64)
                .clamp(0.0, (DDGI_IRRADIANCE_TEXELS - 1) as f64);
            let ix = tx as usize;
            let iy = ty as usize;
            let w = ray.direction.dot(ray.direction).max(0.0001);
            acc[iy][ix] += ray.radiance * w;
            weights[iy][ix] += w;
            let vx = (tx * (DDGI_VISIBILITY_TEXELS - 1) as f64
                / (DDGI_IRRADIANCE_TEXELS - 1) as f64) as usize;
            let vy = (ty * (DDGI_VISIBILITY_TEXELS - 1) as f64
                / (DDGI_IRRADIANCE_TEXELS - 1) as f64) as usize;
            let vx = vx.min(DDGI_VISIBILITY_TEXELS - 1);
            let vy = vy.min(DDGI_VISIBILITY_TEXELS - 1);
            acc_dist[vy][vx] += ray.hit_distance;
            acc_dist_sq[vy][vx] += ray.hit_distance * ray.hit_distance;
            vis_counts[vy][vx] += 1.0;
        }

        for y in 0..DDGI_IRRADIANCE_TEXELS {
            for x in 0..DDGI_IRRADIANCE_TEXELS {
                if weights[y][x] > 1e-6 {
                    let new_val = acc[y][x] * (1.0 / weights[y][x]);
                    self.irradiance[y][x] =
                        self.irradiance[y][x] * hysteresis + new_val * (1.0 - hysteresis);
                }
            }
        }

        for y in 0..DDGI_VISIBILITY_TEXELS {
            for x in 0..DDGI_VISIBILITY_TEXELS {
                if vis_counts[y][x] > 0.0 {
                    let mean = acc_dist[y][x] / vis_counts[y][x];
                    let mean_sq = acc_dist_sq[y][x] / vis_counts[y][x];
                    self.mean_distance[y][x] =
                        self.mean_distance[y][x] * hysteresis + mean * (1.0 - hysteresis);
                    self.mean_distance_sq[y][x] =
                        self.mean_distance_sq[y][x] * hysteresis + mean_sq * (1.0 - hysteresis);
                }
            }
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct ProbeRaySample {
    pub direction: Vec3,
    pub radiance: Vec3,
    pub hit_distance: f64,
}

pub struct DdgiVolume {
    pub grid: ProbeGrid,
    pub probes: Vec<IrradianceProbe>,
    pub hysteresis: f64,
    pub normal_bias: f64,
    pub view_bias: f64,
    pub frame: u64,
}

impl DdgiVolume {
    pub fn new(grid: ProbeGrid) -> Self {
        let n = grid.probe_count();
        Self {
            probes: (0..n).map(|_| IrradianceProbe::default()).collect(),
            grid,
            hysteresis: 0.97,
            normal_bias: 0.25,
            view_bias: 0.01,
            frame: 0,
        }
    }

    pub fn update(
        &mut self,
        scene: &Scene,
        lod: &LodManager,
        max_bounces: u32,
        sdf: Option<&crate::core::engine::rendering::sdf::world_sdf::WorldSdf>,
    ) {
        let probe_count = self.grid.probe_count();
        for probe_idx in 0..probe_count {
            let probe_pos = self.grid.probe_position(probe_idx);
            let mut ray_samples = Vec::with_capacity(DDGI_RAYS_PER_PROBE);

            for ray_idx in 0..DDGI_RAYS_PER_PROBE {
                let seed = make_seed(probe_idx as u32, ray_idx as u32, self.frame as u32);
                let dir = fibonacci_sphere(ray_idx, DDGI_RAYS_PER_PROBE, seed);
                let ray = Ray::new(probe_pos + dir * 0.001, dir);
                let ctx = TraceContext {
                    scene,
                    lod_manager: lod,
                    global_bounce_limit: max_bounces,
                    seed,
                    bvh: None,
                    sdf,
                };
                let radiance = trace_ray(ray, 0, ctx);
                ray_samples.push(ProbeRaySample {
                    direction: dir,
                    radiance,
                    hit_distance: 0.0,
                });
            }

            self.probes[probe_idx].update_from_rays(&ray_samples, self.hysteresis);
        }
        self.frame += 1;
    }

    pub fn sample_irradiance(
        &self,
        pos: Vec3,
        normal: Vec3,
        view_dir: Vec3,
        sdf: Option<&crate::core::engine::rendering::sdf::world_sdf::WorldSdf>,
    ) -> Vec3 {
        let biased_pos = pos + normal * self.normal_bias + view_dir * self.view_bias;
        let indices = self.grid.find_enclosing_probes(biased_pos);
        let weights = self.grid.trilinear_weights(biased_pos);

        let mut irradiance = Vec3::ZERO;
        let mut total_weight = 0.0_f64;

        for (i, &probe_idx) in indices.iter().enumerate() {
            if probe_idx >= self.probes.len() {
                continue;
            }
            let probe_pos = self.grid.probe_position(probe_idx);
            let to_probe = (probe_pos - pos).normalize();
            let crush = to_probe.dot(normal).max(0.0001);
            let sdf_vis = if let Some(sdf_ref) = sdf {
                let probe_dist = (probe_pos - pos).length().max(1e-6);
                let midpoint = pos + to_probe * probe_dist * 0.5;
                (sdf_ref.sample(midpoint) * 4.0 / probe_dist.max(0.25)).clamp(0.0, 1.0)
            } else {
                1.0
            };
            let w = weights[i] * crush * sdf_vis;
            irradiance += self.probes[probe_idx].sample_irradiance(normal) * w;
            total_weight += w;
        }

        if total_weight > 1e-6 {
            irradiance * (1.0 / total_weight)
        } else {
            Vec3::ZERO
        }
    }
}

fn fibonacci_sphere(index: usize, total: usize, seed: u32) -> Vec3 {
    use std::f64::consts::TAU;
    let golden_ratio = (1.0 + 5.0_f64.sqrt()) * 0.5;
    let theta = (1.0 - 2.0 * (index as f64 + 0.5) / total as f64).acos();
    let phi = TAU * (index as f64 / golden_ratio);
    let jitter = (seed.wrapping_mul(0x9E37_79B9) >> 8) as f64 / 16_777_216.0 - 0.5;
    let (sin_t, cos_t) = theta.sin_cos();
    let (sin_p, cos_p) = (phi + jitter * 0.1).sin_cos();
    Vec3::new(sin_t * cos_p, cos_t, sin_t * sin_p)
}

fn oct_encode(n: Vec3) -> (f64, f64) {
    let l1 = n.x.abs() + n.y.abs() + n.z.abs();
    let mut x = n.x / l1;
    let mut y = n.z / l1;
    if n.y < 0.0 {
        let ox = x;
        x = (1.0 - y.abs()) * ox.signum();
        y = (1.0 - ox.abs()) * y.signum();
    }
    (x, y)
}

fn bilinear_irradiance(
    tex: &[[Vec3; DDGI_IRRADIANCE_TEXELS]; DDGI_IRRADIANCE_TEXELS],
    tx: f64,
    ty: f64,
) -> Vec3 {
    let x0 = (tx as usize).min(DDGI_IRRADIANCE_TEXELS - 2);
    let y0 = (ty as usize).min(DDGI_IRRADIANCE_TEXELS - 2);
    let fx = tx - x0 as f64;
    let fy = ty - y0 as f64;
    tex[y0][x0] * ((1.0 - fx) * (1.0 - fy))
        + tex[y0][x0 + 1] * (fx * (1.0 - fy))
        + tex[y0 + 1][x0] * ((1.0 - fx) * fy)
        + tex[y0 + 1][x0 + 1] * (fx * fy)
}