Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-model3d/src/glb.rs

//! **A tiny binary glTF (`.glb`) writer** for a [`Mesh3d`]. Used by the
//! `gen_skull` bin to bake the procedural skull into `assets/skull.glb`, which the
//! component then loads back through the real `gltf` reader ([`crate::mesh::load_glb`]).
//! One node + mesh + material **per group**, each named after the group, so the
//! loader recovers the `"eyes"` group by name. Little-endian; chunk-padded to 4.

use crate::mesh::Mesh3d;

const FLOAT: u32 = 5126;
const UNSIGNED_INT: u32 = 5125;
const ARRAY_BUFFER: u32 = 34962;
const ELEMENT_ARRAY_BUFFER: u32 = 34963;

/// Serialise `mesh` to a self-contained `.glb` byte vector.
pub fn to_glb(mesh: &Mesh3d) -> Vec<u8> {
    let mut bin: Vec<u8> = Vec::new();
    let mut buffer_views = Vec::new();
    let mut accessors = Vec::new();
    let mut materials = Vec::new();
    let mut meshes = Vec::new();
    let mut nodes = Vec::new();
    let mut scene_nodes = Vec::new();

    for (gi, g) in mesh.groups.iter().enumerate() {
        // POSITION bufferView + accessor (with required min/max).
        let pos_off = bin.len();
        let mut min = [f32::MAX; 3];
        let mut max = [f32::MIN; 3];
        for p in &g.positions {
            for c in 0..3 {
                min[c] = min[c].min(p[c]);
                max[c] = max[c].max(p[c]);
            }
            for &v in p {
                bin.extend_from_slice(&v.to_le_bytes());
            }
        }
        let pos_view = buffer_views.len();
        buffer_views.push(serde_json::json!({
            "buffer": 0, "byteOffset": pos_off, "byteLength": g.positions.len() * 12, "target": ARRAY_BUFFER
        }));
        let pos_acc = accessors.len();
        accessors.push(serde_json::json!({
            "bufferView": pos_view, "componentType": FLOAT, "count": g.positions.len(),
            "type": "VEC3", "min": min, "max": max
        }));

        // NORMAL bufferView + accessor.
        let nrm_off = bin.len();
        for n in &g.normals {
            for &v in n {
                bin.extend_from_slice(&v.to_le_bytes());
            }
        }
        let nrm_view = buffer_views.len();
        buffer_views.push(serde_json::json!({
            "buffer": 0, "byteOffset": nrm_off, "byteLength": g.normals.len() * 12, "target": ARRAY_BUFFER
        }));
        let nrm_acc = accessors.len();
        accessors.push(serde_json::json!({
            "bufferView": nrm_view, "componentType": FLOAT, "count": g.normals.len(), "type": "VEC3"
        }));

        // Indices bufferView + accessor.
        let idx_off = bin.len();
        for &i in &g.indices {
            bin.extend_from_slice(&i.to_le_bytes());
        }
        let idx_view = buffer_views.len();
        buffer_views.push(serde_json::json!({
            "buffer": 0, "byteOffset": idx_off, "byteLength": g.indices.len() * 4, "target": ELEMENT_ARRAY_BUFFER
        }));
        let idx_acc = accessors.len();
        accessors.push(serde_json::json!({
            "bufferView": idx_view, "componentType": UNSIGNED_INT, "count": g.indices.len(), "type": "SCALAR"
        }));

        materials.push(serde_json::json!({
            "name": g.name,
            "pbrMetallicRoughness": { "baseColorFactor": g.base_color, "metallicFactor": 0.0, "roughnessFactor": 0.9 }
        }));
        meshes.push(serde_json::json!({
            "name": g.name,
            "primitives": [{ "attributes": { "POSITION": pos_acc, "NORMAL": nrm_acc }, "indices": idx_acc, "material": gi }]
        }));
        nodes.push(serde_json::json!({ "name": g.name, "mesh": gi }));
        scene_nodes.push(gi);
    }

    let doc = serde_json::json!({
        "asset": { "version": "2.0", "generator": "facett-model3d gen_skull" },
        "scene": 0,
        "scenes": [{ "nodes": scene_nodes }],
        "nodes": nodes,
        "meshes": meshes,
        "materials": materials,
        "accessors": accessors,
        "bufferViews": buffer_views,
        "buffers": [{ "byteLength": bin.len() }],
    });

    // ── GLB framing ──────────────────────────────────────────────────────────
    let mut json = serde_json::to_vec(&doc).expect("serialise glTF JSON");
    while !json.len().is_multiple_of(4) {
        json.push(b' ');
    }
    while !bin.len().is_multiple_of(4) {
        bin.push(0);
    }
    let total = 12 + 8 + json.len() + 8 + bin.len();
    let mut out = Vec::with_capacity(total);
    out.extend_from_slice(&0x4654_6C67u32.to_le_bytes()); // "glTF"
    out.extend_from_slice(&2u32.to_le_bytes()); // version
    out.extend_from_slice(&(total as u32).to_le_bytes());
    out.extend_from_slice(&(json.len() as u32).to_le_bytes());
    out.extend_from_slice(&0x4E4F_534Au32.to_le_bytes()); // "JSON"
    out.extend_from_slice(&json);
    out.extend_from_slice(&(bin.len() as u32).to_le_bytes());
    out.extend_from_slice(&0x004E_4942u32.to_le_bytes()); // "BIN\0"
    out.extend_from_slice(&bin);
    out
}