codecraft 0.1.1

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, an immediate-mode UI, audio and gamepad haptics
Documentation
//! How a surface answers light.
//!
//! Kept apart from the mesh: the same shape can be a matte board tile or a
//! polished piece, and which it is has nothing to do with its geometry.
//!
//! ```
//! # use codecraft::material::Material;
//! let piece = Material::gloss(0.6, 48.0);
//! let tile = Material::matte();
//! assert!(piece.specular > tile.specular);
//! ```

/// Matte, or polished enough to hold a highlight and a little of the sky.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Material {
    /// How bright a highlight it takes.
    pub specular: f32,
    /// How tight that highlight is. Small is a wide sheen, large a point.
    pub shininess: f32,
    /// How much of the sky it shows back, at a glancing angle.
    pub reflectivity: f32,
}

impl Default for Material {
    fn default() -> Self {
        // A little sheen on everything: a scene of pure matte surfaces reads
        // as paper whatever the lighting does.
        Self {
            specular: 0.12,
            shininess: 24.0,
            reflectivity: 0.05,
        }
    }
}

impl Material {
    /// A polished surface: a brighter, tighter highlight, and enough of the
    /// sky in it to read as a material rather than a colour.
    pub fn gloss(specular: f32, shininess: f32) -> Self {
        Self {
            specular,
            shininess,
            reflectivity: (specular * 0.5).min(1.0),
        }
    }

    /// Nothing to catch the light at all.
    pub fn matte() -> Self {
        Self {
            specular: 0.0,
            shininess: 1.0,
            reflectivity: 0.0,
        }
    }

    /// How much of the sky it shows back, for a surface that wants a
    /// different amount than its polish implies.
    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 {
    /// The same surface as an OpenPBR one.
    ///
    /// The three Blinn-Phong numbers do not describe a material -- they
    /// describe how a particular shader was told to behave -- so this is a
    /// mapping and not a conversion, and it exists so that a scene written
    /// against [`Material::gloss`] keeps looking like itself once the shading
    /// is done properly. Anything that wants a real material should author an
    /// [`OpenPbrSurface`] and skip this.
    ///
    /// - `shininess` is a Phong exponent, which is a sharpness; roughness is
    ///   the opposite, and `sqrt(2 / (n + 2))` is the usual bridge between
    ///   them.
    /// - `specular` scales the lobe, which is what `specular_weight` does.
    /// - `reflectivity` asked for more of the sky at a glancing angle, which
    ///   is Fresnel, which is the IOR. More of it means a higher index.
    pub fn to_openpbr(self) -> OpenPbrSurface {
        let roughness = (2.0 / (self.shininess.max(1.0) + 2.0))
            .sqrt()
            .clamp(0.02, 1.0);
        OpenPbrSurface {
            // The base colour is the instance's tint, applied in the shader:
            // one material serves every piece on the board.
            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 specular lobe is a delta function, which an
        // analytic light can never hit.
        let mirror = Material::gloss(1.0, 1.0e9).to_openpbr();
        assert!(mirror.specular_roughness >= 0.02);
    }
}