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
//! Packing a surface into the bytes a shader reads.
//!
//! [`OpenPbrSurface`] is the authoring form: one named field per input, in the
//! units the spec uses. [`Material`] is the GPU form: `vec4`s first so the
//! block is std140-friendly, colours carrying their weight in `w` to save a
//! row, and lengths in the units the shader works in. This is the one place
//! that knows which is which.
//!
//! The field order is the OpenPBR viewer's, which is also the order
//! `uniforms.wgsl` declares its `Material` struct in. The two are read as the
//! same bytes, so **nothing may be inserted in the middle** -- a new field
//! goes on the end, where it cannot shift what is already there.

use bytemuck::{Pod, Zeroable};

use super::{Color3, OpenPbrSurface};

/// An OpenPBR material as the shader reads it.
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct Material {
    pub base_color_weight: [f32; 4],
    pub specular_color_weight: [f32; 4],
    pub transmission_color_depth: [f32; 4],
    pub transmission_scatter_aniso: [f32; 4],
    pub subsurface_color_weight: [f32; 4],
    pub subsurface_radius_scale_radius: [f32; 4],
    pub coat_color_weight: [f32; 4],
    pub fuzz_color_weight: [f32; 4],
    pub emission_color_luminance: [f32; 4],
    pub base_diffuse_roughness: f32,
    pub base_metalness: f32,
    pub specular_roughness: f32,
    pub specular_anisotropy: f32,
    pub specular_ior: f32,
    pub transmission_weight: f32,
    pub transmission_dispersion_abbe_number: f32,
    pub transmission_dispersion_scale: f32,
    pub subsurface_anisotropy: f32,
    pub coat_roughness: f32,
    pub coat_anisotropy: f32,
    pub coat_ior: f32,
    pub coat_darkening: f32,
    pub fuzz_roughness: f32,
    pub thin_film_weight: f32,
    /// Nanometres, which is the scale interference happens at.
    pub thin_film_thickness: f32,
    pub thin_film_ior: f32,
    pub geometry_opacity: f32,
    pub geometry_thin_walled: u32,
    /// Non-zero: the normal map's +Y runs along `dP/dv`, as USD reads it;
    /// zero: along `cross(N, T)`, as glTF does.
    pub normal_map_y_along_v: u32,
    /// Which layer of the base-colour array this material samples, negative
    /// where it has no map.
    pub base_color_layer: f32,
    pub roughness_layer: f32,
    pub metalness_layer: f32,
    pub normal_layer: f32,
}

/// A colour and the weight that goes with it, as one row.
fn c4(c: Color3, w: f32) -> [f32; 4] {
    [c.r, c.g, c.b, w]
}

/// Pack `surface` for the GPU, with lengths in scene units.
pub fn material_of(surface: &OpenPbrSurface) -> Material {
    material_of_scaled(surface, 1.0)
}

/// Pack `surface`, scaling the two inputs that are lengths --
/// `transmission_depth` and `subsurface_radius` -- by `length_scale`.
///
/// A scene that models a chess piece in centimetres and one that models it in
/// metres want the same material to scatter the same distance *through the
/// piece*, and that distance is the only thing about a material that depends
/// on how big the world is.
pub fn material_of_scaled(surface: &OpenPbrSurface, length_scale: f32) -> Material {
    let s = surface;
    Material {
        base_color_weight: c4(s.base_color, s.base_weight),
        specular_color_weight: c4(s.specular_color, s.specular_weight),
        transmission_color_depth: c4(s.transmission_color, s.transmission_depth * length_scale),
        transmission_scatter_aniso: c4(s.transmission_scatter, s.transmission_scatter_anisotropy),
        subsurface_color_weight: c4(s.subsurface_color, s.subsurface_weight),
        subsurface_radius_scale_radius: c4(
            s.subsurface_radius_scale,
            s.subsurface_radius * length_scale,
        ),
        coat_color_weight: c4(s.coat_color, s.coat_weight),
        fuzz_color_weight: c4(s.fuzz_color, s.fuzz_weight),
        emission_color_luminance: c4(s.emission_color, s.emission_luminance),
        base_diffuse_roughness: s.base_diffuse_roughness,
        base_metalness: s.base_metalness,
        specular_roughness: s.specular_roughness,
        specular_anisotropy: s.specular_roughness_anisotropy,
        specular_ior: s.specular_ior,
        transmission_weight: s.transmission_weight,
        transmission_dispersion_abbe_number: s.transmission_dispersion_abbe_number,
        transmission_dispersion_scale: s.transmission_dispersion_scale,
        subsurface_anisotropy: s.subsurface_scatter_anisotropy,
        coat_roughness: s.coat_roughness,
        coat_anisotropy: s.coat_roughness_anisotropy,
        coat_ior: s.coat_ior,
        coat_darkening: s.coat_darkening,
        fuzz_roughness: s.fuzz_roughness,
        thin_film_weight: s.thin_film_weight,
        // The spec authors thin film in micrometres; the shader works in
        // nanometres.
        thin_film_thickness: s.thin_film_thickness * 1000.0,
        thin_film_ior: s.thin_film_ior,
        geometry_opacity: s.geometry_opacity,
        geometry_thin_walled: s.geometry_thin_walled as u32,
        normal_map_y_along_v: 0,
        // An `OpenPbrSurface` is constants and nothing else. A material with
        // maps gets its layers filled in wherever the table is built, which is
        // the only place that knows which layer a file landed on.
        base_color_layer: -1.0,
        roughness_layer: -1.0,
        metalness_layer: -1.0,
        normal_layer: -1.0,
    }
}

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

    #[test]
    fn a_colour_and_its_weight_share_a_row() {
        let mut surface = OpenPbrSurface::default();
        surface.base_color = Color3::new(0.1, 0.2, 0.3);
        surface.base_weight = 0.5;

        let m = material_of(&surface);
        assert_eq!(m.base_color_weight, [0.1, 0.2, 0.3, 0.5]);
    }

    #[test]
    fn thin_film_crosses_into_nanometres() {
        let mut surface = OpenPbrSurface::default();
        surface.thin_film_thickness = 0.5;
        assert_eq!(material_of(&surface).thin_film_thickness, 500.0);
    }

    #[test]
    fn only_the_lengths_answer_to_scale() {
        let mut surface = OpenPbrSurface::default();
        surface.transmission_depth = 2.0;
        surface.subsurface_radius = 3.0;
        surface.specular_roughness = 0.4;

        let m = material_of_scaled(&surface, 10.0);
        assert_eq!(m.transmission_color_depth[3], 20.0);
        assert_eq!(m.subsurface_radius_scale_radius[3], 30.0);
        // A roughness is not a length.
        assert_eq!(m.specular_roughness, 0.4);
    }

    #[test]
    fn a_material_with_no_maps_says_so() {
        let m = material_of(&OpenPbrSurface::default());
        for layer in [
            m.base_color_layer,
            m.roughness_layer,
            m.metalness_layer,
            m.normal_layer,
        ] {
            assert!(
                layer < 0.0,
                "a missing map is a negative layer, got {layer}"
            );
        }
    }

    #[test]
    fn the_block_is_the_size_the_shader_reads() {
        // Nine vec4 rows, then 24 scalars: what `uniforms.wgsl` declares.
        assert_eq!(std::mem::size_of::<Material>(), 9 * 16 + 24 * 4);
    }
}