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::math::Vec4;
use crate::core::engine::rendering::raytracing::Vec3 as RaytracingVec3;

#[derive(Debug, Clone)]
pub struct PbrMaterial {
    pub base_color: Vec4,
    pub metallic: f32,
    pub roughness: f32,
    pub normal_map: Option<String>,
    pub metallic_map: Option<String>,
    pub roughness_map: Option<String>,
    pub ambient_occlusion: f32,
}

impl Default for PbrMaterial {
    fn default() -> Self {
        PbrMaterial {
            base_color: Vec4::new(0.8, 0.8, 0.8, 1.0),
            metallic: 0.0,
            roughness: 0.5,
            normal_map: None,
            metallic_map: None,
            roughness_map: None,
            ambient_occlusion: 1.0,
        }
    }
}

pub trait Material {
    fn base_color(&self) -> Vec4;
    fn metallic(&self) -> f32;
    fn roughness(&self) -> f32;
    fn normal_map(&self) -> Option<&str>;
}

impl Material for PbrMaterial {
    fn base_color(&self) -> Vec4 {
        self.base_color
    }

    fn metallic(&self) -> f32 {
        self.metallic
    }

    fn roughness(&self) -> f32 {
        self.roughness
    }

    fn normal_map(&self) -> Option<&str> {
        self.normal_map.as_deref()
    }
}

impl PbrMaterial {
    pub fn albedo_vec3(&self) -> RaytracingVec3 {
        RaytracingVec3::new(
            self.base_color.x as f64,
            self.base_color.y as f64,
            self.base_color.z as f64,
        )
    }

    pub fn shader_sources() -> (&'static str, &'static str) {
        (pbr_shader::VERTEX_SHADER, pbr_shader::FRAGMENT_SHADER)
    }
}

pub mod pbr_shader {
    pub const VERTEX_SHADER: &str = r#"
        #version 300 es
        precision highp float;

        layout(location = 0) in vec3 position;
        layout(location = 1) in vec3 normal;
        layout(location = 2) in vec2 texcoord;

        uniform mat4 model;
        uniform mat4 view;
        uniform mat4 projection;

        out vec3 v_position;
        out vec3 v_normal;
        out vec2 v_texcoord;

        void main() {
            v_position = vec3(model * vec4(position, 1.0));
            v_normal = normalize(mat3(model) * normal);
            v_texcoord = texcoord;
            gl_Position = projection * view * vec4(v_position, 1.0);
        }
    "#;

    pub const FRAGMENT_SHADER: &str = r#"
        #version 300 es
        precision highp float;

        in vec3 v_position;
        in vec3 v_normal;
        in vec2 v_texcoord;

        uniform vec4 base_color;
        uniform float metallic;
        uniform float roughness;
        uniform sampler2D normal_map;
        uniform vec3 light_pos;
        uniform vec3 view_pos;

        out vec4 out_color;

        const float PI = 3.14159265359;

        vec3 fresnelSchlick(float cosTheta, vec3 F0) {
            return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
        }

        float DistributionGGX(vec3 N, vec3 H, float roughness) {
            float a = roughness * roughness;
            float a2 = a * a;
            float NdotH = max(dot(N, H), 0.0);
            float NdotH2 = NdotH * NdotH;

            float nom = a2;
            float denom = (NdotH2 * (a2 - 1.0) + 1.0);
            denom = PI * denom * denom;

            return nom / denom;
        }

        float GeometrySchlickGGX(float NdotV, float roughness) {
            float r = (roughness + 1.0);
            float k = (r * r) / 8.0;

            float nom = NdotV;
            float denom = NdotV * (1.0 - k) + k;

            return nom / denom;
        }

        float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
            float NdotV = max(dot(N, V), 0.0);
            float NdotL = max(dot(N, L), 0.0);
            float ggx2 = GeometrySchlickGGX(NdotV, roughness);
            float ggx1 = GeometrySchlickGGX(NdotL, roughness);

            return ggx1 * ggx2;
        }

        void main() {
            vec3 N = normalize(v_normal);
            vec3 V = normalize(view_pos - v_position);
            vec3 L = normalize(light_pos - v_position);
            vec3 H = normalize(V + L);

            float distance = length(light_pos - v_position);
            float attenuation = 1.0 / (distance * distance);
            vec3 radiance = vec3(1.0) * attenuation;

            vec3 F0 = vec3(0.04);
            F0 = mix(F0, base_color.rgb, metallic);
            vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);

            vec3 kS = F;
            vec3 kD = vec3(1.0) - kS;
            kD *= 1.0 - metallic;

            float NDF = DistributionGGX(N, H, roughness);
            float G = GeometrySmith(N, V, L, roughness);
            vec3 numerator = NDF * G * F;
            float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001;
            vec3 specular = numerator / denominator;

            float NdotL = max(dot(N, L), 0.0);
            vec3 Lo = (kD * base_color.rgb / PI + specular) * radiance * NdotL;

            vec3 ambient = vec3(0.03) * base_color.rgb;
            vec3 color = ambient + Lo;

            color = color / (color + vec3(1.0));
            color = pow(color, vec3(1.0 / 2.2));

            out_color = vec4(color, base_color.a);
        }
    "#;
}