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
}