use crate::glb::Prim;
use serde_json::{json, Value};
const FLOAT: u32 = 5126;
const USHORT: u32 = 5123;
const UINT: u32 = 5125;
struct Builder {
comp: Vec<u8>,
fb_len: usize,
views: Vec<Value>,
accessors: Vec<Value>,
}
impl Builder {
fn align4(v: usize) -> usize {
(v + 3) & !3
}
fn add(&mut self, decompressed_len: usize, stride: usize, count: usize, mode: &str, compressed: &[u8]) -> usize {
self.fb_len = Self::align4(self.fb_len);
let fb_off = self.fb_len;
self.fb_len += decompressed_len;
while self.comp.len() % 4 != 0 {
self.comp.push(0);
}
let c_off = self.comp.len();
self.comp.extend_from_slice(compressed);
let mut bv = json!({
"buffer": 1,
"byteOffset": fb_off,
"byteLength": decompressed_len,
"extensions": { "EXT_meshopt_compression": {
"buffer": 0,
"byteOffset": c_off,
"byteLength": compressed.len(),
"byteStride": stride,
"count": count,
"mode": mode,
}},
});
if mode == "ATTRIBUTES" {
bv["byteStride"] = json!(stride);
}
self.views.push(bv);
self.views.len() - 1
}
}
fn encodable(p: &Prim) -> bool {
!p.positions.is_empty() && !p.indices.is_empty() && p.indices.len() % 3 == 0
}
fn enc<T>(rows: &[T]) -> Result<Vec<u8>, String> {
meshopt::encode_vertex_buffer(rows).map_err(|e| format!("encode attribute: {e:?}"))
}
pub fn write(prims: &[Prim], quantize: bool) -> Result<Vec<u8>, String> {
let usable: Vec<&Prim> = prims.iter().filter(|p| encodable(p)).collect();
if usable.is_empty() {
return Ok(crate::glb::write(prims));
}
let (offset, inv, scale) = if quantize {
let mut all = Vec::new();
for p in &usable {
for v in &p.positions {
all.extend_from_slice(v);
}
}
let (o, s) = meshopt::calc_pos_offset_and_scale(&all);
(o, if s > 0.0 { 65535.0 / s } else { 0.0 }, s)
} else {
([0.0; 3], 0.0, 0.0)
};
let mut b = Builder { comp: Vec::new(), fb_len: 0, views: Vec::new(), accessors: 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();
let mut image_bin: Vec<u8> = Vec::new();
for p in &usable {
let vcount = p.positions.len();
let pos_acc = b.accessors.len();
if quantize {
let mut q: Vec<[u16; 4]> = Vec::with_capacity(vcount);
let mut lo = [u16::MAX; 3];
let mut hi = [u16::MIN; 3];
for v in &p.positions {
let c = |i: usize| (((v[i] - offset[i]) * inv).round()).clamp(0.0, 65535.0) as u16;
let w = [c(0), c(1), c(2)];
for k in 0..3 {
lo[k] = lo[k].min(w[k]);
hi[k] = hi[k].max(w[k]);
}
q.push([w[0], w[1], w[2], 0]);
}
let view = b.add(q.len() * 8, 8, vcount, "ATTRIBUTES", &enc(&q)?);
b.accessors.push(json!({
"bufferView": view, "componentType": USHORT, "count": vcount,
"type": "VEC3", "min": lo, "max": hi,
}));
} else {
let mut lo = [f32::INFINITY; 3];
let mut hi = [f32::NEG_INFINITY; 3];
for v in &p.positions {
for k in 0..3 {
lo[k] = lo[k].min(v[k]);
hi[k] = hi[k].max(v[k]);
}
}
let view = b.add(vcount * 12, 12, vcount, "ATTRIBUTES", &enc(&p.positions)?);
b.accessors.push(json!({
"bufferView": view, "componentType": FLOAT, "count": vcount,
"type": "VEC3", "min": lo, "max": hi,
}));
}
let mut attributes = json!({ "POSITION": pos_acc });
if let Some(uvs) = &p.uvs {
let uv_acc = b.accessors.len();
if quantize {
let q: Vec<[u16; 2]> = uvs
.iter()
.map(|t| [(t[0].clamp(0.0, 1.0) * 65535.0).round() as u16, (t[1].clamp(0.0, 1.0) * 65535.0).round() as u16])
.collect();
let view = b.add(q.len() * 4, 4, vcount, "ATTRIBUTES", &enc(&q)?);
b.accessors.push(json!({
"bufferView": view, "componentType": USHORT, "count": vcount,
"type": "VEC2", "normalized": true,
}));
} else {
let view = b.add(vcount * 8, 8, vcount, "ATTRIBUTES", &enc(uvs)?);
b.accessors.push(json!({
"bufferView": view, "componentType": FLOAT, "count": vcount, "type": "VEC2",
}));
}
attributes["TEXCOORD_0"] = json!(uv_acc);
}
let comp = meshopt::encode_index_buffer(&p.indices, vcount).map_err(|e| format!("encode indices: {e:?}"))?;
let iv = b.add(p.indices.len() * 4, 4, p.indices.len(), "TRIANGLES", &comp);
let idx_acc = b.accessors.len();
b.accessors.push(json!({
"bufferView": iv, "componentType": UINT, "count": p.indices.len(), "type": "SCALAR",
}));
let mut prim = json!({ "attributes": attributes, "indices": idx_acc });
if let Some(jpeg) = &p.jpeg {
while image_bin.len() % 4 != 0 {
image_bin.push(0);
}
let img_off = image_bin.len();
image_bin.extend_from_slice(jpeg);
let view_idx = b.views.len();
b.views.push(json!({ "buffer": 0, "byteOffset": img_off, "byteLength": jpeg.len(), "__image": true }));
let img = images.len();
images.push(json!({ "bufferView": view_idx, "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 img_base = Builder::align4(b.comp.len());
for v in b.views.iter_mut() {
if v.get("__image").is_some() {
let off = v["byteOffset"].as_u64().unwrap() as usize + img_base;
v["byteOffset"] = json!(off);
v.as_object_mut().unwrap().remove("__image");
}
}
while b.comp.len() < img_base {
b.comp.push(0);
}
b.comp.extend_from_slice(&image_bin);
let node = if quantize {
let s = scale / 65535.0;
json!({ "mesh": 0, "matrix": [s, 0.0, 0.0, 0.0, 0.0, s, 0.0, 0.0, 0.0, 0.0, s, 0.0, offset[0], offset[1], offset[2], 1.0] })
} else {
json!({ "mesh": 0 })
};
let mut ext_used = vec!["EXT_meshopt_compression"];
let mut ext_req = vec!["EXT_meshopt_compression"];
if quantize {
ext_used.push("KHR_mesh_quantization");
ext_req.push("KHR_mesh_quantization");
}
if !images.is_empty() {
ext_used.push("KHR_materials_unlit");
}
let mut gltf = json!({
"asset": { "version": "2.0", "generator": "mxmxmx2tiles" },
"extensionsUsed": ext_used,
"extensionsRequired": ext_req,
"scene": 0,
"scenes": [ { "nodes": [0] } ],
"nodes": [ node ],
"meshes": [ { "primitives": gltf_prims } ],
"accessors": b.accessors,
"bufferViews": b.views,
"buffers": [
{ "byteLength": b.comp.len() },
{ "byteLength": b.fb_len, "extensions": { "EXT_meshopt_compression": { "fallback": true } } },
],
});
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 } ]);
}
Ok(crate::glb::pack_glb(&gltf, &b.comp))
}
#[cfg(test)]
mod tests {
use crate::glb::Prim;
fn sample() -> Prim {
Prim {
positions: (0..500).map(|i| [i as f32 * 0.1, (i % 7) as f32 * 2.0, (i % 13) as f32 * -1.5]).collect(),
normals: None,
uvs: Some(vec![[0.25, 0.75]; 500]),
indices: (0..(498 * 3)).map(|i| (i % 500) as u32).collect(),
jpeg: None,
}
}
#[test]
fn lossless_roundtrip_is_exact() {
let p = sample();
let glb = super::write(&[p.clone()], false).unwrap();
let jl = u32::from_le_bytes(glb[12..16].try_into().unwrap()) as usize;
let g: serde_json::Value = serde_json::from_slice(&glb[20..20 + jl]).unwrap();
let bin = &glb[20 + jl + 8..];
let acc = &g["accessors"][0];
assert_eq!(acc["componentType"], 5126);
assert!(g["nodes"][0].get("matrix").is_none());
let bv = &g["bufferViews"][acc["bufferView"].as_u64().unwrap() as usize];
let e = &bv["extensions"]["EXT_meshopt_compression"];
let o = e["byteOffset"].as_u64().unwrap() as usize;
let l = e["byteLength"].as_u64().unwrap() as usize;
let dec: Vec<[f32; 3]> = meshopt::decode_vertex_buffer(&bin[o..o + l], p.positions.len()).unwrap();
assert_eq!(dec, p.positions);
}
#[test]
fn quantized_roundtrip_within_step() {
let p = sample();
let glb = super::write(&[p.clone()], true).unwrap();
let jl = u32::from_le_bytes(glb[12..16].try_into().unwrap()) as usize;
let g: serde_json::Value = serde_json::from_slice(&glb[20..20 + jl]).unwrap();
let bin = &glb[20 + jl + 8..];
let m = g["nodes"][0]["matrix"].as_array().unwrap();
let s = m[0].as_f64().unwrap() as f32;
let off = [m[12].as_f64().unwrap() as f32, m[13].as_f64().unwrap() as f32, m[14].as_f64().unwrap() as f32];
let acc = &g["accessors"][0];
let bv = &g["bufferViews"][acc["bufferView"].as_u64().unwrap() as usize];
let e = &bv["extensions"]["EXT_meshopt_compression"];
let o = e["byteOffset"].as_u64().unwrap() as usize;
let l = e["byteLength"].as_u64().unwrap() as usize;
let dec: Vec<[u16; 4]> = meshopt::decode_vertex_buffer(&bin[o..o + l], p.positions.len()).unwrap();
for (orig, q) in p.positions.iter().zip(dec.iter()) {
for k in 0..3 {
assert!((q[k] as f32 * s + off[k] - orig[k]).abs() <= s * 2.0);
}
}
}
}