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
//! Loading meshes out of a `.glb` (binary glTF) by node name.
//!
//! Blender's exporter keeps object names on glTF *nodes*, while mesh data
//! keeps its own name (`Cylinder.002` and friends), so lookup here is by node
//! name — that is what an artist sees in the outliner.
use bytemuck::{Pod, Zeroable};
use glam::{Mat4, Vec3};

use crate::ui::Color;

/// One vertex of a loaded mesh, in the layout the 3D pipeline expects.
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Vertex {
    pub position: [f32; 3],
    pub normal: [f32; 3],
}

/// CPU-side mesh data, ready to upload.
pub struct MeshData {
    /// The node name from the source file — `chess_piece_king`, say.
    pub name: String,
    pub vertices: Vec<Vertex>,
    pub indices: Vec<u32>,
    /// The material's base color, when the source file gave the mesh one.
    pub base_color: Option<Color>,
    /// Local-space bounds, used to fit the shadow map around what is drawn.
    pub min: Vec3,
    pub max: Vec3,
}

#[derive(Debug)]
pub enum MeshError {
    Parse(gltf::Error),
    /// A `.glb` should carry its buffer inline; a `.gltf` pointing at
    /// external files is not supported.
    MissingBinaryChunk,
    MissingPositions(String),
}

impl std::fmt::Display for MeshError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Parse(e) => write!(f, "could not parse glTF: {e}"),
            Self::MissingBinaryChunk => write!(f, "glb has no binary chunk"),
            Self::MissingPositions(name) => write!(f, "mesh {name:?} has no positions"),
        }
    }
}

impl std::error::Error for MeshError {}

/// Reads every named node with a mesh out of a `.glb`.
///
/// Node transforms are baked into the vertices, so a mesh comes back in the
/// pose the artist left it in and callers only deal with their own placement.
pub fn load_glb(bytes: &[u8]) -> Result<Vec<MeshData>, MeshError> {
    let gltf = gltf::Gltf::from_slice(bytes).map_err(MeshError::Parse)?;
    let blob = gltf.blob.as_deref().ok_or(MeshError::MissingBinaryChunk)?;

    let mut meshes = Vec::new();
    for node in gltf.nodes() {
        let (Some(name), Some(mesh)) = (node.name(), node.mesh()) else {
            continue;
        };
        let transform = Mat4::from_cols_array_2d(&node.transform().matrix());
        meshes.push(load_node(name, &mesh, transform, blob)?);
    }
    Ok(meshes)
}

fn load_node(
    name: &str,
    mesh: &gltf::Mesh<'_>,
    transform: Mat4,
    blob: &[u8],
) -> Result<MeshData, MeshError> {
    let normal_transform = Mat4::from_mat3(glam::Mat3::from_mat4(transform).inverse().transpose());
    let mut vertices: Vec<Vertex> = Vec::new();
    let mut indices: Vec<u32> = Vec::new();
    let mut base_color = None;

    for primitive in mesh.primitives() {
        let reader = primitive.reader(|_| Some(blob));
        let positions = reader
            .read_positions()
            .ok_or_else(|| MeshError::MissingPositions(name.to_string()))?;

        // Indices are per-primitive; offset them as primitives are appended.
        let base_vertex = vertices.len() as u32;
        let normals: Option<Vec<[f32; 3]>> = reader.read_normals().map(|n| n.collect());
        for (i, position) in positions.enumerate() {
            let position = transform.transform_point3(Vec3::from(position));
            let normal = normals
                .as_ref()
                .and_then(|normals| normals.get(i).copied())
                .unwrap_or([0.0, 1.0, 0.0]);
            let normal = normal_transform
                .transform_vector3(Vec3::from(normal))
                .normalize_or_zero();
            vertices.push(Vertex {
                position: position.to_array(),
                normal: normal.to_array(),
            });
        }

        match reader.read_indices() {
            Some(read) => indices.extend(read.into_u32().map(|i| i + base_vertex)),
            // An unindexed primitive draws its vertices in order.
            None => indices.extend(base_vertex..vertices.len() as u32),
        }

        if base_color.is_none() {
            let pbr = primitive.material().pbr_metallic_roughness();
            if primitive.material().index().is_some() {
                let [r, g, b, a] = pbr.base_color_factor();
                base_color = Some(Color::linear_rgba(r, g, b, a));
            }
        }
    }

    let mut min = Vec3::splat(f32::MAX);
    let mut max = Vec3::splat(f32::MIN);
    for vertex in &vertices {
        min = min.min(Vec3::from(vertex.position));
        max = max.max(Vec3::from(vertex.position));
    }

    Ok(MeshData {
        name: name.to_string(),
        vertices,
        indices,
        base_color,
        min,
        max,
    })
}