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::utils::{fbm_3d, smoothstep, value_noise_3d};

use super::scattering::AtmosphereParams;

#[derive(Debug, Clone)]
pub struct ProceduralEnvironment {
    pub horizon_tint: Vec3,
    pub zenith_tint: Vec3,
    pub ground_tint: Vec3,
    pub exposure_bias: f64,
    pub rotation_angle: f64,
    pub atmosphere: Option<AtmosphereParams>,
}

impl ProceduralEnvironment {
    pub fn cinematic_space() -> Self {
        Self {
            horizon_tint: Vec3::new(0.08, 0.04, 0.12),
            zenith_tint: Vec3::new(0.01, 0.005, 0.03),
            ground_tint: Vec3::new(0.005, 0.003, 0.008),
            exposure_bias: 0.0,
            rotation_angle: 0.0,
            atmosphere: None,
        }
    }

    pub fn with_atmosphere(mut self, params: AtmosphereParams) -> Self {
        self.atmosphere = Some(params);
        self
    }

    pub fn with_rotation(mut self, radians: f64) -> Self {
        self.rotation_angle = radians;
        self
    }

    pub fn rotate_direction(&self, dir: Vec3) -> Vec3 {
        let (s, c) = self.rotation_angle.sin_cos();
        Vec3::new(dir.x * c + dir.z * s, dir.y, -dir.x * s + dir.z * c)
    }

    pub fn sky_colors(&self, direction: Vec3, sun_dir: Vec3) -> (Vec3, bool) {
        let dir = self.rotate_direction(direction.normalize());
        let up = dir.y.clamp(-1.0, 1.0);
        let below = up < 0.0;

        let base = if below {
            Vec3::lerp(self.horizon_tint, self.ground_tint, (-up).min(1.0))
        } else {
            Vec3::lerp(self.horizon_tint, self.zenith_tint, up)
        };

        let sky = match &self.atmosphere {
            Some(atmo) => {
                let atmo_color = atmo.compute_sky_color(dir, sun_dir, 32);
                base + atmo_color
            }
            None => base,
        };

        (sky, below)
    }

    pub fn sun_color(&self, direction: Vec3, sun_dir: Vec3) -> Vec3 {
        let dir = self.rotate_direction(direction.normalize());
        let cos_angle = dir.dot(sun_dir);
        let sun_angular_radius = 0.00465;
        if cos_angle > (1.0 - sun_angular_radius) {
            let t = ((cos_angle - (1.0 - sun_angular_radius)) / sun_angular_radius).min(1.0);
            let limb = smoothstep(0.0, 1.0, t);
            Vec3::new(10.0, 8.5, 7.0) * limb
        } else {
            Vec3::ZERO
        }
    }

    pub fn hdri_probe(&self, direction: Vec3, sun_dir: Vec3) -> Vec3 {
        let (sky, below) = self.sky_colors(direction, sun_dir);
        if below {
            return sky;
        }

        let dir = self.rotate_direction(direction.normalize());
        let sun = self.sun_color(direction, sun_dir);

        let nebula_noise = fbm_3d(dir * 3.0, 5, 2.0, 0.5) * 0.5 + 0.5;
        let nebula = Vec3::new(0.15, 0.05, 0.2) * nebula_noise * 0.08;

        let aurora = self.aurora_contribution(dir);

        let star_noise = value_noise_3d(dir * 400.0);
        let stars = if star_noise > 0.97 {
            let brightness = (star_noise - 0.97) / 0.03;
            Vec3::splat(brightness * 0.5)
        } else {
            Vec3::ZERO
        };

        sky + sun + nebula + aurora + stars
    }

    pub fn aurora_contribution(&self, dir: Vec3) -> Vec3 {
        let latitude = dir.y;
        let aurora_band = smoothstep(0.3, 0.5, latitude) * (1.0 - smoothstep(0.6, 0.8, latitude));
        if aurora_band < 0.001 {
            return Vec3::ZERO;
        }
        let wave = (dir.x * 8.0 + dir.z * 4.0).sin() * 0.5 + 0.5;
        let noise = fbm_3d(dir * 6.0, 3, 2.0, 0.5) * 0.5 + 0.5;
        let intensity = aurora_band * wave * noise * 0.12;
        Vec3::new(0.1, 0.8, 0.4) * intensity
    }

    pub fn exposure_for_detail(&self, key_light_luminance: f64) -> f64 {
        let base = if key_light_luminance > 0.001 {
            0.5 / key_light_luminance
        } else {
            4.0
        };
        (base + self.exposure_bias).max(0.01)
    }

    pub fn ambient_color(&self) -> Vec3 {
        (self.zenith_tint + self.horizon_tint) * 0.15
    }
}