codecraft 0.1.2

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
//! Packing an [`OpenPbrSurface`] into the GPU [`Material`] block a shader reads.
//! Field order matches `uniforms.wgsl`; never insert in the middle, only append.

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.
    pub thin_film_thickness: f32,
    pub thin_film_ior: f32,
    pub geometry_opacity: f32,
    pub geometry_thin_walled: u32,
    /// Non-zero: +Y along `dP/dv` (USD); zero: along `cross(N, T)` (glTF).
    pub normal_map_y_along_v: u32,
    /// Texture-array layer, negative where there is no map.
    pub base_color_layer: f32,
    pub roughness_layer: f32,
    pub metalness_layer: f32,
    pub normal_layer: f32,
}

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 length inputs (`transmission_depth`, `subsurface_radius`) by `length_scale`.
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,
        // 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,
        // Map layers are filled in where the texture table is built.
        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);
        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() {
        assert_eq!(std::mem::size_of::<Material>(), 9 * 16 + 24 * 4);
    }
}