codecraft 0.2.0

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, a yakui-drawn UI, audio and gamepad haptics; its binary maps any folder, and the symbols of its Rust files, as a 3D wall of boxes
Documentation
//! How a surface answers light.
//!
//! ```
//! # use codecraft::material::Material;
//! let piece = Material::gloss(0.6, 48.0);
//! let tile = Material::matte();
//! assert!(piece.specular > tile.specular);
//! ```

/// Blinn-Phong surface parameters.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Material {
    pub specular: f32,
    /// Phong exponent.
    pub shininess: f32,
    pub reflectivity: f32,
}

impl Default for Material {
    fn default() -> Self {
        Self {
            specular: 0.12,
            shininess: 24.0,
            reflectivity: 0.05,
        }
    }
}

impl Material {
    /// A polished surface with a matching amount of reflectivity.
    pub fn gloss(specular: f32, shininess: f32) -> Self {
        Self {
            specular,
            shininess,
            reflectivity: (specular * 0.5).min(1.0),
        }
    }

    pub fn matte() -> Self {
        Self {
            specular: 0.0,
            shininess: 1.0,
            reflectivity: 0.0,
        }
    }

    pub fn reflectivity(mut self, reflectivity: f32) -> Self {
        self.reflectivity = reflectivity;
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_matte_surface_takes_no_highlight_and_shows_nothing_back() {
        let matte = Material::matte();
        assert_eq!(matte.specular, 0.0);
        assert_eq!(matte.reflectivity, 0.0);
    }

    #[test]
    fn polish_brings_the_sky_with_it() {
        let dull = Material::gloss(0.2, 16.0);
        let polished = Material::gloss(0.8, 64.0);

        assert!(polished.reflectivity > dull.reflectivity);
        assert!(polished.shininess > dull.shininess);
        assert!(
            polished.reflectivity <= 1.0,
            "and never more than all of it"
        );
    }
}

impl Material {
    /// An approximate OpenPBR equivalent; anything wanting a real material should author an [`OpenPbrSurface`] directly.
    pub fn to_openpbr(self) -> OpenPbrSurface {
        // sqrt(2 / (n + 2)) is the usual Phong-exponent-to-roughness bridge.
        let roughness = (2.0 / (self.shininess.max(1.0) + 2.0))
            .sqrt()
            .clamp(0.02, 1.0);
        OpenPbrSurface {
            // Base colour is the instance tint, applied in the shader.
            base_color: Color3::new(1.0, 1.0, 1.0),
            specular_weight: self.specular.clamp(0.0, 1.0),
            specular_roughness: roughness,
            specular_ior: 1.5 + self.reflectivity.clamp(0.0, 1.0) * 0.5,
            ..OpenPbrSurface::default()
        }
    }
}

use crate::materials::openpbr::{Color3, OpenPbrSurface};

#[cfg(test)]
mod openpbr_tests {
    use super::*;

    #[test]
    fn polish_becomes_smoothness() {
        let dull = Material::gloss(0.2, 8.0).to_openpbr();
        let polished = Material::gloss(0.8, 128.0).to_openpbr();
        assert!(
            polished.specular_roughness < dull.specular_roughness,
            "a tighter highlight is a smoother surface",
        );
        assert!(polished.specular_weight > dull.specular_weight);
    }

    #[test]
    fn a_matte_surface_keeps_its_specular_off() {
        assert_eq!(Material::matte().to_openpbr().specular_weight, 0.0);
    }

    #[test]
    fn roughness_never_reaches_zero() {
        // A perfectly smooth lobe is a delta function an analytic light can never hit.
        let mirror = Material::gloss(1.0, 1.0e9).to_openpbr();
        assert!(mirror.specular_roughness >= 0.02);
    }
}