mxmxmx2tiles 0.1.0

Convert Bentley ContextCapture 3mx photogrammetry data to Cesium 3D Tiles
Documentation
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)); // ELEMENT_ARRAY_BUFFER
        let idx_acc = accessors.len();
        accessors.push(json!({
            "bufferView": idx_view, "componentType": 5125, // UNSIGNED_INT
            "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)); // ARRAY_BUFFER
        let pos_acc = accessors.len();
        accessors.push(json!({
            "bufferView": pos_view, "componentType": 5126, // FLOAT
            "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);
        // CLAMP_TO_EDGE, LINEAR_MIPMAP_LINEAR, LINEAR
        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()); // "glTF"
    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()); // "JSON"
    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()); // "BIN\0"
    out.extend_from_slice(&bin_chunk);
    out
}