use serde_json::{json, Value};
#[derive(Clone)]
pub struct Prim {
pub positions: Vec<[f32; 3]>,
pub normals: Option<Vec<[f32; 3]>>,
pub uvs: Option<Vec<[f32; 2]>>,
pub indices: Vec<u32>,
pub jpeg: Option<Vec<u8>>,
}
#[derive(Default)]
struct Bin {
data: Vec<u8>,
views: Vec<Value>,
}
impl Bin {
fn view(&mut self, bytes: &[u8], target: Option<u32>) -> usize {
while self.data.len() % 4 != 0 {
self.data.push(0);
}
let offset = self.data.len();
self.data.extend_from_slice(bytes);
let mut v = json!({ "buffer": 0, "byteOffset": offset, "byteLength": bytes.len() });
if let Some(t) = target {
v["target"] = json!(t);
}
self.views.push(v);
self.views.len() - 1
}
}
fn f32_bytes(v: &[f32]) -> Vec<u8> {
let mut b = Vec::with_capacity(v.len() * 4);
for x in v {
b.extend_from_slice(&x.to_le_bytes());
}
b
}
pub fn write(prims: &[Prim]) -> Vec<u8> {
let mut bin = Bin::default();
let mut accessors: Vec<Value> = Vec::new();
let mut images: Vec<Value> = Vec::new();
let mut textures: Vec<Value> = Vec::new();
let mut materials: Vec<Value> = Vec::new();
let mut gltf_prims: Vec<Value> = Vec::new();
for p in prims {
if p.positions.is_empty() || p.indices.is_empty() {
continue;
}
let idx_bytes: Vec<u8> = p.indices.iter().flat_map(|i| i.to_le_bytes()).collect();
let idx_view = bin.view(&idx_bytes, Some(34963)); let idx_acc = accessors.len();
accessors.push(json!({
"bufferView": idx_view, "componentType": 5125, "count": p.indices.len(), "type": "SCALAR",
}));
let mut min = [f32::INFINITY; 3];
let mut max = [f32::NEG_INFINITY; 3];
for v in &p.positions {
for k in 0..3 {
min[k] = min[k].min(v[k]);
max[k] = max[k].max(v[k]);
}
}
let pos_flat: Vec<f32> = p.positions.iter().flatten().copied().collect();
let pos_view = bin.view(&f32_bytes(&pos_flat), Some(34962)); let pos_acc = accessors.len();
accessors.push(json!({
"bufferView": pos_view, "componentType": 5126, "count": p.positions.len(), "type": "VEC3", "min": min, "max": max,
}));
let mut attributes = json!({ "POSITION": pos_acc });
if let Some(uvs) = &p.uvs {
let flat: Vec<f32> = uvs.iter().flatten().copied().collect();
let view = bin.view(&f32_bytes(&flat), Some(34962));
attributes["TEXCOORD_0"] = json!(accessors.len());
accessors.push(json!({
"bufferView": view, "componentType": 5126, "count": uvs.len(), "type": "VEC2",
}));
}
let mut prim = json!({ "attributes": attributes, "indices": idx_acc });
if let Some(jpeg) = &p.jpeg {
let view = bin.view(jpeg, None);
let img = images.len();
images.push(json!({ "bufferView": view, "mimeType": "image/jpeg" }));
let tex = textures.len();
textures.push(json!({ "source": img, "sampler": 0 }));
let mat = materials.len();
materials.push(json!({
"pbrMetallicRoughness": { "baseColorTexture": { "index": tex }, "metallicFactor": 0.0, "roughnessFactor": 1.0 },
"extensions": { "KHR_materials_unlit": {} },
"doubleSided": true,
}));
prim["material"] = json!(mat);
}
gltf_prims.push(prim);
}
let mut gltf = json!({
"asset": { "version": "2.0", "generator": "mxmxmx2tiles" },
"scene": 0,
"scenes": [ { "nodes": [0] } ],
"nodes": [ { "mesh": 0 } ],
"meshes": [ { "primitives": gltf_prims } ],
"accessors": accessors,
"bufferViews": bin.views,
"buffers": [ { "byteLength": bin.data.len() } ],
});
if !images.is_empty() {
gltf["images"] = json!(images);
gltf["textures"] = json!(textures);
gltf["materials"] = json!(materials);
gltf["samplers"] = json!([ { "wrapS": 33071, "wrapT": 33071, "minFilter": 9987, "magFilter": 9729 } ]);
gltf["extensionsUsed"] = json!(["KHR_materials_unlit"]);
}
pack_glb(&gltf, &bin.data)
}
pub(crate) fn pack_glb(gltf: &Value, bin: &[u8]) -> Vec<u8> {
let mut json_chunk = serde_json::to_vec(gltf).unwrap();
while json_chunk.len() % 4 != 0 {
json_chunk.push(b' ');
}
let mut bin_chunk = bin.to_vec();
while bin_chunk.len() % 4 != 0 {
bin_chunk.push(0);
}
let total = 12 + 8 + json_chunk.len() + 8 + bin_chunk.len();
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&0x46546C67u32.to_le_bytes()); out.extend_from_slice(&2u32.to_le_bytes());
out.extend_from_slice(&(total as u32).to_le_bytes());
out.extend_from_slice(&(json_chunk.len() as u32).to_le_bytes());
out.extend_from_slice(&0x4E4F534Au32.to_le_bytes()); out.extend_from_slice(&json_chunk);
out.extend_from_slice(&(bin_chunk.len() as u32).to_le_bytes());
out.extend_from_slice(&0x004E4942u32.to_le_bytes()); out.extend_from_slice(&bin_chunk);
out
}