Skip to main content

falcon_library/model3d/
gltf.rs

1//! glTF 2.0: binary `.glb` and JSON `.gltf` with its buffers.
2//!
3//! Geometry, the base colour, the base colour, normal and emissive textures,
4//! the emissive factor and the alpha mode are read (no other textures,
5//! animation, skins or sparse accessors). The default
6//! scene is walked, node transforms are baked into positions and normals,
7//! and every primitive of every mesh instance becomes one [`Mesh`].
8//!
9//! A texture comes from the GLB's binary chunk, a data URI or a file next
10//! to the model (sampler wrap modes are ignored, textures always repeat; see
11//! [`super::texture`]). The base colour texture has
12//! the base colour factor as its tint (glTF multiplies the two) and, for the
13//! `MASK` alpha mode, its alpha cutoff. A mesh has one set of texture
14//! coordinates, those of the base colour texture, else of the normal
15//! texture, else of the emissive one, with that texture's
16//! `KHR_texture_transform` baked in. The `BLEND` and `MASK` alpha modes draw
17//! see-through. A material with `KHR_materials_pbrSpecularGlossiness` has no
18//! normal, emissive or see-through part, as in Eagle's viewer. A texture or
19//! factor that is out of range, undecodable or malformed is left out; it
20//! never fails the file.
21
22use std::collections::HashMap;
23
24use base64::Engine;
25use serde::Deserialize;
26
27use super::math::{self, M4, V3};
28use super::texture::{Loader, Texture};
29use super::{Material, Mesh, Model, ParseError, Result, Surface};
30
31const GLB_MAGIC: u32 = 0x4654_6C67;
32const CHUNK_JSON: u32 = 0x4E4F_534A;
33const CHUNK_BIN: u32 = 0x004E_4942;
34
35/// Deepest node nesting followed; deeper (or cyclic) branches are dropped.
36const MAX_DEPTH: usize = 64;
37/// Most mesh instances and output vertices, against files that instance a
38/// big mesh exponentially often.
39const MAX_INSTANCES: usize = 100_000;
40const MAX_VERTICES: usize = 50_000_000;
41
42type Side<'a> = &'a dyn Fn(&str) -> Option<Vec<u8>>;
43
44#[derive(Deserialize, Default)]
45#[serde(default)]
46struct Doc {
47    buffers: Vec<BufferDef>,
48    #[serde(rename = "bufferViews")]
49    buffer_views: Vec<ViewDef>,
50    accessors: Vec<AccessorDef>,
51    meshes: Vec<MeshDef>,
52    materials: Vec<MaterialDef>,
53    nodes: Vec<NodeDef>,
54    scenes: Vec<SceneDef>,
55    scene: Option<usize>,
56    textures: Vec<TextureDef>,
57    images: Vec<ImageDef>,
58}
59
60#[derive(Deserialize, Default)]
61#[serde(default)]
62struct TextureDef {
63    source: Option<usize>,
64}
65
66#[derive(Deserialize, Default)]
67#[serde(default)]
68struct ImageDef {
69    name: String,
70    uri: Option<String>,
71    #[serde(rename = "bufferView")]
72    buffer_view: Option<usize>,
73}
74
75/// A material's reference to a texture.
76#[derive(Deserialize, Default, Clone)]
77#[serde(default)]
78struct TexInfo {
79    index: usize,
80    #[serde(rename = "texCoord")]
81    tex_coord: usize,
82    extensions: TexExtensions,
83}
84
85#[derive(Deserialize, Default, Clone)]
86#[serde(default)]
87struct TexExtensions {
88    #[serde(rename = "KHR_texture_transform")]
89    transform: Option<TexTransform>,
90}
91
92#[derive(Deserialize, Clone)]
93#[serde(default)]
94struct TexTransform {
95    offset: [f32; 2],
96    rotation: f32,
97    scale: [f32; 2],
98    #[serde(rename = "texCoord")]
99    tex_coord: Option<usize>,
100}
101
102impl Default for TexTransform {
103    fn default() -> Self {
104        TexTransform { offset: [0.0; 2], rotation: 0.0, scale: [1.0; 2], tex_coord: None }
105    }
106}
107
108#[derive(Deserialize, Default)]
109#[serde(default)]
110struct BufferDef {
111    uri: Option<String>,
112}
113
114#[derive(Deserialize, Default)]
115#[serde(default)]
116struct ViewDef {
117    buffer: usize,
118    #[serde(rename = "byteOffset")]
119    byte_offset: usize,
120    #[serde(rename = "byteLength")]
121    byte_length: usize,
122    #[serde(rename = "byteStride")]
123    byte_stride: Option<usize>,
124}
125
126#[derive(Deserialize, Default)]
127#[serde(default)]
128struct AccessorDef {
129    #[serde(rename = "bufferView")]
130    buffer_view: Option<usize>,
131    #[serde(rename = "byteOffset")]
132    byte_offset: usize,
133    #[serde(rename = "componentType")]
134    component_type: u32,
135    normalized: bool,
136    count: usize,
137    #[serde(rename = "type")]
138    kind: String,
139}
140
141#[derive(Deserialize, Default)]
142#[serde(default)]
143struct MeshDef {
144    name: String,
145    primitives: Vec<PrimDef>,
146}
147
148#[derive(Deserialize, Default)]
149#[serde(default)]
150struct PrimDef {
151    attributes: HashMap<String, usize>,
152    indices: Option<usize>,
153    material: Option<usize>,
154    mode: Option<u32>,
155}
156
157#[derive(Deserialize, Default)]
158#[serde(default)]
159struct MaterialDef {
160    name: String,
161    #[serde(rename = "pbrMetallicRoughness")]
162    pbr: PbrDef,
163    #[serde(rename = "alphaMode")]
164    alpha_mode: String,
165    #[serde(rename = "alphaCutoff")]
166    alpha_cutoff: Option<f32>,
167    // Kept as raw JSON so that a wrongly shaped value only loses itself.
168    #[serde(rename = "normalTexture")]
169    normal_texture: Option<serde_json::Value>,
170    #[serde(rename = "emissiveTexture")]
171    emissive_texture: Option<serde_json::Value>,
172    #[serde(rename = "emissiveFactor")]
173    emissive_factor: Option<serde_json::Value>,
174    extensions: serde_json::Value,
175}
176
177/// A texture reference from raw JSON; anything but an object is none (a
178/// sequence would otherwise be read as a struct's fields).
179fn tex_info(v: &serde_json::Value) -> Option<TexInfo> {
180    if !v.is_object() {
181        return None;
182    }
183    serde_json::from_value(v.clone()).ok()
184}
185
186impl MaterialDef {
187    fn normal(&self) -> Option<TexInfo> {
188        tex_info(self.normal_texture.as_ref()?)
189    }
190
191    fn emissive_texture(&self) -> Option<TexInfo> {
192        tex_info(self.emissive_texture.as_ref()?)
193    }
194
195    /// The emissive factor in display values; black unless it is exactly
196    /// three finite numbers.
197    fn emissive(&self) -> V3 {
198        let Some(a) = self.emissive_factor.as_ref().and_then(|v| v.as_array()).filter(|a| a.len() == 3) else { return [0.0; 3] };
199        let mut out = [0.0; 3];
200        for (o, v) in out.iter_mut().zip(a) {
201            match v.as_f64().map(|x| x as f32).filter(|x| x.is_finite()) {
202                Some(x) => *o = srgb(x),
203                None => return [0.0; 3],
204            }
205        }
206        out
207    }
208
209    /// Eagle's viewer replaces a specular-glossiness material, dropping
210    /// everything of the surface.
211    fn spec_gloss(&self) -> bool {
212        self.extensions.get("KHR_materials_pbrSpecularGlossiness").is_some()
213    }
214
215    /// The surface without its maps.
216    fn flat_surface(&self) -> Surface {
217        if self.spec_gloss() {
218            return Surface::default();
219        }
220        Surface { emissive: self.emissive(), blend: matches!(self.alpha_mode.as_str(), "BLEND" | "MASK"), ..Surface::default() }
221    }
222}
223
224#[derive(Deserialize, Default)]
225#[serde(default)]
226struct PbrDef {
227    #[serde(rename = "baseColorFactor")]
228    base_color_factor: Option<[f32; 4]>,
229    #[serde(rename = "baseColorTexture")]
230    base_color_texture: Option<TexInfo>,
231}
232
233#[derive(Deserialize, Default)]
234#[serde(default)]
235struct NodeDef {
236    mesh: Option<usize>,
237    children: Vec<usize>,
238    matrix: Option<Vec<f32>>,
239    translation: Option<[f32; 3]>,
240    rotation: Option<[f32; 4]>,
241    scale: Option<[f32; 3]>,
242}
243
244#[derive(Deserialize, Default)]
245#[serde(default)]
246struct SceneDef {
247    nodes: Vec<usize>,
248}
249
250/// Reads a `.glb` file.
251pub fn parse_glb(bytes: &[u8], side: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<Model> {
252    let u32_at = |o: usize| -> Result<u32> {
253        let b = bytes.get(o..o + 4).ok_or_else(|| ParseError::new("truncated GLB"))?;
254        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
255    };
256    if bytes.len() < 12 || u32_at(0)? != GLB_MAGIC {
257        return Err(ParseError::new("not a GLB file"));
258    }
259    let version = u32_at(4)?;
260    if version != 2 {
261        return Err(ParseError(format!("unsupported GLB version {version}")));
262    }
263    let total = u32_at(8)? as usize;
264    if total < 12 || total > bytes.len() {
265        return Err(ParseError::new("GLB length does not match the file"));
266    }
267    let mut json: Option<&[u8]> = None;
268    let mut bin: Option<&[u8]> = None;
269    let mut pos = 12;
270    let mut first = true;
271    while pos + 8 <= total {
272        let len = u32_at(pos)? as usize;
273        let kind = u32_at(pos + 4)?;
274        let start = pos + 8;
275        let end = start.checked_add(len).filter(|&e| e <= total).ok_or_else(|| ParseError::new("GLB chunk runs past the file"))?;
276        if first && kind != CHUNK_JSON {
277            return Err(ParseError::new("GLB does not start with a JSON chunk"));
278        }
279        first = false;
280        match kind {
281            CHUNK_JSON if json.is_none() => json = Some(&bytes[start..end]),
282            CHUNK_BIN if bin.is_none() => bin = Some(&bytes[start..end]),
283            _ => {}
284        }
285        // Chunks are 4-byte aligned.
286        pos = end.next_multiple_of(4);
287    }
288    let json = json.ok_or_else(|| ParseError::new("GLB has no JSON chunk"))?;
289    build(json, bin, side)
290}
291
292/// Reads a `.gltf` file; its buffers are data URIs or fetched with `side`.
293pub fn parse_gltf(bytes: &[u8], side: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<Model> {
294    build(bytes, None, side)
295}
296
297fn build(json: &[u8], bin: Option<&[u8]>, side: Side) -> Result<Model> {
298    let doc: Doc = serde_json::from_slice(json).map_err(|e| ParseError(format!("bad glTF JSON: {e}")))?;
299    let mut buffers = Vec::with_capacity(doc.buffers.len());
300    for (i, b) in doc.buffers.iter().enumerate() {
301        let data = match &b.uri {
302            None if i == 0 => bin.map(<[u8]>::to_vec),
303            None => None,
304            Some(uri) => load_uri(uri, side),
305        };
306        buffers.push(data.ok_or_else(|| ParseError(format!("glTF buffer {i} is missing")))?);
307    }
308
309    let mut out = Vec::new();
310    let mut cache: HashMap<(usize, usize), Option<Prim>> = HashMap::new();
311    let mut walker = Walker {
312        doc: &doc,
313        buffers: &buffers,
314        cache: &mut cache,
315        out: &mut out,
316        instances: 0,
317        vertices: 0,
318        loader: Loader::new(side),
319        textures: HashMap::new(),
320    };
321    let identity = identity();
322    for root in roots(&doc) {
323        walker.visit(root, &identity, 0);
324    }
325    if doc.nodes.is_empty() {
326        for m in 0..doc.meshes.len() {
327            walker.emit(m, &identity);
328        }
329    }
330    Model { meshes: out }.finish()
331}
332
333fn identity() -> M4 {
334    [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]
335}
336
337/// The nodes to start from: the default scene, else the first scene, else
338/// every node that is nobody's child.
339fn roots(doc: &Doc) -> Vec<usize> {
340    if let Some(s) = doc.scene.and_then(|i| doc.scenes.get(i)).or(doc.scenes.first()) {
341        return s.nodes.clone();
342    }
343    let mut child = vec![false; doc.nodes.len()];
344    for n in &doc.nodes {
345        for &c in &n.children {
346            if let Some(f) = child.get_mut(c) {
347                *f = true;
348            }
349        }
350    }
351    (0..doc.nodes.len()).filter(|&i| !child[i]).collect()
352}
353
354fn load_uri(uri: &str, side: Side) -> Option<Vec<u8>> {
355    if let Some(rest) = uri.strip_prefix("data:") {
356        let (head, data) = rest.split_once(',')?;
357        return if head.split(';').any(|p| p.eq_ignore_ascii_case("base64")) {
358            let clean: String = data.chars().filter(|c| !c.is_whitespace()).collect();
359            base64::engine::general_purpose::STANDARD.decode(clean.trim_end_matches('=')).or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(clean.trim_end_matches('='))).ok()
360        } else {
361            Some(percent_decode(data))
362        };
363    }
364    side(&String::from_utf8_lossy(&percent_decode(uri)))
365}
366
367fn percent_decode(s: &str) -> Vec<u8> {
368    let b = s.as_bytes();
369    let mut out = Vec::with_capacity(b.len());
370    let mut i = 0;
371    while i < b.len() {
372        if b[i] == b'%'
373            && i + 2 < b.len()
374            && let (Some(h), Some(l)) = (hex(b[i + 1]), hex(b[i + 2]))
375        {
376            out.push(h * 16 + l);
377            i += 3;
378            continue;
379        }
380        out.push(b[i]);
381        i += 1;
382    }
383    out
384}
385
386fn hex(c: u8) -> Option<u8> {
387    (c as char).to_digit(16).map(|d| d as u8)
388}
389
390/// Linear to sRGB for one channel.
391fn srgb(c: f32) -> f32 {
392    let c = c.clamp(0.0, 1.0);
393    if c <= 0.003_130_8 { 12.92 * c } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 }
394}
395
396/// Reads accessor `idx` as `count * components` numbers (integers widened,
397/// normalised ones mapped to 0..1 / -1..1). Only scalar and vector types.
398fn read_accessor(doc: &Doc, buffers: &[Vec<u8>], idx: usize) -> Result<(Vec<f64>, usize)> {
399    let bad = |m: &str| ParseError(format!("glTF accessor {idx}: {m}"));
400    let acc = doc.accessors.get(idx).ok_or_else(|| bad("does not exist"))?;
401    let ncomp = match acc.kind.as_str() {
402        "SCALAR" => 1,
403        "VEC2" => 2,
404        "VEC3" => 3,
405        "VEC4" => 4,
406        _ => return Err(bad("unsupported type")),
407    };
408    let csize = match acc.component_type {
409        5120 | 5121 => 1,
410        5122 | 5123 => 2,
411        5125 | 5126 => 4,
412        _ => return Err(bad("unsupported component type")),
413    };
414    let esize = ncomp * csize;
415    let total = acc.count.checked_mul(ncomp).filter(|&t| t <= 1 << 28).ok_or_else(|| bad("too many elements"))?;
416    let Some(vi) = acc.buffer_view else {
417        return Ok((vec![0.0; total], ncomp));
418    };
419    let view = doc.buffer_views.get(vi).ok_or_else(|| bad("buffer view does not exist"))?;
420    let data = buffers.get(view.buffer).ok_or_else(|| bad("buffer does not exist"))?;
421    let stride = view.byte_stride.filter(|&s| s > 0).unwrap_or(esize);
422    let view_end = view.byte_offset.checked_add(view.byte_length).ok_or_else(|| bad("view overflows"))?.min(data.len());
423    let start = view.byte_offset.checked_add(acc.byte_offset).ok_or_else(|| bad("offset overflows"))?;
424    if acc.count > 0 {
425        let last = (acc.count - 1).checked_mul(stride).and_then(|o| o.checked_add(start)).and_then(|o| o.checked_add(esize));
426        if last.is_none_or(|l| l > view_end) {
427            return Err(bad("reads past its buffer"));
428        }
429    }
430    let mut out = Vec::with_capacity(total);
431    for i in 0..acc.count {
432        let base = start + i * stride;
433        for c in 0..ncomp {
434            let o = base + c * csize;
435            out.push(component(&data[o..o + csize], acc.component_type, acc.normalized));
436        }
437    }
438    Ok((out, ncomp))
439}
440
441fn component(b: &[u8], ct: u32, norm: bool) -> f64 {
442    match ct {
443        5120 => {
444            let v = f64::from(b[0] as i8);
445            if norm { (v / 127.0).max(-1.0) } else { v }
446        }
447        5121 => {
448            let v = f64::from(b[0]);
449            if norm { v / 255.0 } else { v }
450        }
451        5122 => {
452            let v = f64::from(i16::from_le_bytes([b[0], b[1]]));
453            if norm { (v / 32767.0).max(-1.0) } else { v }
454        }
455        5123 => {
456            let v = f64::from(u16::from_le_bytes([b[0], b[1]]));
457            if norm { v / 65535.0 } else { v }
458        }
459        5125 => {
460            let v = f64::from(u32::from_le_bytes([b[0], b[1], b[2], b[3]]));
461            if norm { v / 4_294_967_295.0 } else { v }
462        }
463        _ => f64::from(f32::from_le_bytes([b[0], b[1], b[2], b[3]])),
464    }
465}
466
467/// A primitive's geometry in its own node space, as triangles.
468struct Prim {
469    positions: Vec<V3>,
470    normals: Option<Vec<V3>>,
471    colors: Option<Vec<V3>>,
472    indices: Vec<u32>,
473    material: Option<Material>,
474    uvs: Option<Vec<[f32; 2]>>,
475}
476
477fn read_prim(doc: &Doc, buffers: &[Vec<u8>], p: &PrimDef) -> Result<Option<Prim>> {
478    let mode = p.mode.unwrap_or(4);
479    if !matches!(mode, 4..=6) {
480        return Ok(None);
481    }
482    let Some(&pos_idx) = p.attributes.get("POSITION") else {
483        return Ok(None);
484    };
485    let (pos, nc) = read_accessor(doc, buffers, pos_idx)?;
486    if nc != 3 {
487        return Err(ParseError::new("glTF POSITION is not a VEC3"));
488    }
489    let positions: Vec<V3> = pos.as_chunks::<3>().0.iter().map(|c| c.map(|v| v as f32)).collect();
490    let n = positions.len();
491
492    // Optional attributes that are broken or do not match are left out.
493    let normals = p.attributes.get("NORMAL").and_then(|&i| read_accessor(doc, buffers, i).ok()).filter(|(v, nc)| *nc == 3 && v.len() == n * 3).map(|(v, _)| v.as_chunks::<3>().0.iter().map(|c| c.map(|x| x as f32)).collect::<Vec<V3>>());
494    let colors = p.attributes.get("COLOR_0").and_then(|&i| read_accessor(doc, buffers, i).ok()).filter(|(v, nc)| (*nc == 3 || *nc == 4) && v.len() == n * *nc).map(|(v, nc)| v.chunks(nc).map(|c| [srgb(c[0] as f32), srgb(c[1] as f32), srgb(c[2] as f32)]).collect::<Vec<V3>>());
495
496    let seq: Vec<u32> = match p.indices {
497        Some(i) => {
498            let (v, nc) = read_accessor(doc, buffers, i)?;
499            if nc != 1 {
500                return Err(ParseError::new("glTF indices are not scalar"));
501            }
502            v.into_iter().map(|x| x as u32).collect()
503        }
504        None => (0..n as u32).collect(),
505    };
506    let indices = match mode {
507        4 => seq[..seq.len() / 3 * 3].to_vec(),
508        5 => (0..seq.len().saturating_sub(2)).flat_map(|i| if i % 2 == 0 { [seq[i], seq[i + 1], seq[i + 2]] } else { [seq[i + 1], seq[i], seq[i + 2]] }).collect(),
509        _ => (1..seq.len().saturating_sub(1)).flat_map(|i| [seq[0], seq[i], seq[i + 1]]).collect(),
510    };
511
512    let material = p.material.and_then(|m| doc.materials.get(m)).map(|m| {
513        let f = m.pbr.base_color_factor.unwrap_or([1.0; 4]);
514        Material { name: m.name.clone(), color: [srgb(f[0]), srgb(f[1]), srgb(f[2])], opacity: f[3] }
515    });
516    // The coordinates of the first texture the material has, in the order
517    // base colour, normal, emissive, through its transform.
518    let info = p.material.and_then(|m| doc.materials.get(m)).and_then(|m| m.pbr.base_color_texture.clone().or_else(|| m.normal()).or_else(|| m.emissive_texture()));
519    let info = info.as_ref();
520    let uvs = info.and_then(|t| {
521        let set = t.extensions.transform.as_ref().and_then(|x| x.tex_coord).unwrap_or(t.tex_coord);
522        let (v, nc) = read_accessor(doc, buffers, *p.attributes.get(&format!("TEXCOORD_{set}"))?).ok()?;
523        (nc == 2 && v.len() == n * 2).then(|| v.as_chunks::<2>().0.iter().map(|c| transform_uv(t.extensions.transform.as_ref(), [c[0] as f32, c[1] as f32])).collect())
524    });
525    Ok(Some(Prim { positions, normals, colors, indices, material, uvs }))
526}
527
528/// `uv` through a `KHR_texture_transform`: scaled, rotated, then offset,
529/// rotating as three.js does for these files.
530fn transform_uv(t: Option<&TexTransform>, uv: [f32; 2]) -> [f32; 2] {
531    let Some(t) = t else { return uv };
532    let (s, c) = t.rotation.sin_cos();
533    let (u, v) = (uv[0] * t.scale[0], uv[1] * t.scale[1]);
534    [c * u + s * v + t.offset[0], -s * u + c * v + t.offset[1]]
535}
536
537/// The picture a texture reference names, decoded through `loader`.
538fn texture_for(doc: &Doc, buffers: &[Vec<u8>], loader: &mut Loader, info: &TexInfo) -> Option<Texture> {
539    let tex = doc.textures.get(info.index)?;
540    let img = doc.images.get(tex.source?)?;
541    let image = if let Some(view) = img.buffer_view.and_then(|v| doc.buffer_views.get(v)) {
542        let data = buffers.get(view.buffer)?;
543        let end = view.byte_offset.checked_add(view.byte_length)?;
544        let name = if img.name.is_empty() { format!("image {}", tex.source?) } else { img.name.clone() };
545        loader.embedded(&name, data.get(view.byte_offset..end)?)?
546    } else {
547        let uri = img.uri.as_deref()?;
548        if uri.starts_with("data:") {
549            loader.embedded(&img.name, &load_uri(uri, &|_| None)?)?
550        } else {
551            loader.file(&String::from_utf8_lossy(&percent_decode(uri)))?
552        }
553    };
554    Some(Texture::new(image))
555}
556
557/// What a material draws with: its base colour texture and its surface.
558#[derive(Clone)]
559struct Look {
560    texture: Option<Texture>,
561    surface: Surface,
562}
563
564/// Material `m`'s textures, decoded through `loader`: the base colour
565/// texture first, then the maps, which may take only half of what is left.
566fn material_look(doc: &Doc, buffers: &[Vec<u8>], loader: &mut Loader, m: usize) -> Look {
567    let Some(mat) = doc.materials.get(m) else { return Look { texture: None, surface: Surface::default() } };
568    let texture = mat.pbr.base_color_texture.as_ref().and_then(|info| texture_for(doc, buffers, loader, info)).map(|t| {
569        let f = mat.pbr.base_color_factor.unwrap_or([1.0; 4]);
570        Texture { tint: [srgb(f[0]), srgb(f[1]), srgb(f[2])], alpha_cutoff: (mat.alpha_mode == "MASK").then(|| mat.alpha_cutoff.unwrap_or(0.5)), ..t }
571    });
572    let mut surface = mat.flat_surface();
573    if !mat.spec_gloss() {
574        loader.for_map(|l| {
575            surface.normal_map = mat.normal().and_then(|i| texture_for(doc, buffers, l, &i));
576            surface.emissive_map = mat.emissive_texture().and_then(|i| texture_for(doc, buffers, l, &i));
577        });
578    }
579    Look { texture, surface }
580}
581
582struct Walker<'a> {
583    doc: &'a Doc,
584    buffers: &'a [Vec<u8>],
585    cache: &'a mut HashMap<(usize, usize), Option<Prim>>,
586    out: &'a mut Vec<Mesh>,
587    instances: usize,
588    vertices: usize,
589    loader: Loader<'a>,
590    /// Each material's textures, decoded when a primitive first uses it.
591    textures: HashMap<usize, Look>,
592}
593
594impl Walker<'_> {
595    fn visit(&mut self, node: usize, parent: &M4, depth: usize) {
596        let Some(n) = self.doc.nodes.get(node) else { return };
597        if depth >= MAX_DEPTH {
598            return;
599        }
600        let world = math::mul(parent, &local_matrix(n));
601        if let Some(m) = n.mesh {
602            self.emit(m, &world);
603        }
604        for &c in &n.children {
605            self.visit(c, &world, depth + 1);
606        }
607    }
608
609    fn emit(&mut self, mesh: usize, world: &M4) {
610        let Some(def) = self.doc.meshes.get(mesh) else { return };
611        for (pi, pdef) in def.primitives.iter().enumerate() {
612            if self.instances >= MAX_INSTANCES || self.vertices >= MAX_VERTICES {
613                return;
614            }
615            let (doc, buffers) = (self.doc, self.buffers);
616            let prim = self.cache.entry((mesh, pi)).or_insert_with(|| read_prim(doc, buffers, pdef).ok().flatten());
617            let Some(prim) = prim else { continue };
618            self.instances += 1;
619            self.vertices += prim.positions.len();
620            let mut mesh = instance(&def.name, prim, world);
621            if let Some(m) = pdef.material {
622                if mesh.uvs.is_some() {
623                    let loader = &mut self.loader;
624                    let look = self.textures.entry(m).or_insert_with(|| material_look(doc, buffers, loader, m));
625                    mesh.texture = look.texture.clone();
626                    mesh.surface = look.surface.clone();
627                } else if let Some(mat) = doc.materials.get(m) {
628                    mesh.surface = mat.flat_surface();
629                }
630            }
631            self.out.push(mesh);
632        }
633    }
634}
635
636/// `prim` placed by `world`: positions through the matrix, normals through
637/// its inverse transpose (renormalised), winding flipped for a mirroring
638/// matrix.
639fn instance(name: &str, prim: &Prim, world: &M4) -> Mesh {
640    let positions = prim.positions.iter().map(|&p| math::transform_point(world, p)).collect();
641    let (cof, det) = cofactor(world);
642    let normals = prim.normals.as_ref().map(|ns| {
643        ns.iter()
644            .map(|&n| {
645                let v = [0, 1, 2].map(|r| (0..3).map(|c| cof[r][c] * n[c]).sum::<f32>());
646                math::normalize(if det < 0.0 { math::scale(v, -1.0) } else { v })
647            })
648            .collect()
649    });
650    let mut indices = prim.indices.clone();
651    if det < 0.0 {
652        for t in indices.as_chunks_mut::<3>().0 {
653            t.swap(1, 2);
654        }
655    }
656    Mesh { name: name.to_string(), positions, normals, colors: prim.colors.clone(), indices, material: prim.material.clone(), uvs: prim.uvs.clone(), ..Mesh::default() }
657}
658
659/// The cofactor matrix of the upper 3x3 of `m` (`[row][col]`, which is the
660/// inverse transpose up to the determinant) and the determinant. A
661/// singular or non-finite matrix gives the matrix itself and a positive
662/// determinant.
663fn cofactor(m: &M4) -> ([[f32; 3]; 3], f32) {
664    let at = |r: usize, c: usize| m[c % 3][r % 3];
665    let mut cof = [[0.0f32; 3]; 3];
666    for (r, row) in cof.iter_mut().enumerate() {
667        for (c, cell) in row.iter_mut().enumerate() {
668            *cell = at(r + 1, c + 1) * at(r + 2, c + 2) - at(r + 1, c + 2) * at(r + 2, c + 1);
669        }
670    }
671    let det = (0..3).map(|c| at(0, c) * cof[0][c]).sum::<f32>();
672    if det == 0.0 || !det.is_finite() {
673        let mut plain = [[0.0f32; 3]; 3];
674        for (r, row) in plain.iter_mut().enumerate() {
675            for (c, cell) in row.iter_mut().enumerate() {
676                *cell = if m[c][r].is_finite() { m[c][r] } else { 0.0 };
677            }
678        }
679        return (plain, 1.0);
680    }
681    (cof, det)
682}
683
684/// A node's own transform: its `matrix`, or translation * rotation * scale.
685fn local_matrix(n: &NodeDef) -> M4 {
686    if let Some(v) = &n.matrix
687        && v.len() == 16
688    {
689        let mut m = [[0.0; 4]; 4];
690        for (c, col) in m.iter_mut().enumerate() {
691            col.copy_from_slice(&v[c * 4..c * 4 + 4]);
692        }
693        return m;
694    }
695    let t = n.translation.unwrap_or([0.0; 3]);
696    let s = n.scale.unwrap_or([1.0; 3]);
697    let [mut x, mut y, mut z, mut w] = n.rotation.unwrap_or([0.0, 0.0, 0.0, 1.0]);
698    let len = (x * x + y * y + z * z + w * w).sqrt();
699    if len > 0.0 && len.is_finite() {
700        (x, y, z, w) = (x / len, y / len, z / len, w / len);
701    } else {
702        (x, y, z, w) = (0.0, 0.0, 0.0, 1.0);
703    }
704    let c0 = [1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y + z * w), 2.0 * (x * z - y * w)];
705    let c1 = [2.0 * (x * y - z * w), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z + x * w)];
706    let c2 = [2.0 * (x * z + y * w), 2.0 * (y * z - x * w), 1.0 - 2.0 * (x * x + y * y)];
707    [
708        [c0[0] * s[0], c0[1] * s[0], c0[2] * s[0], 0.0],
709        [c1[0] * s[1], c1[1] * s[1], c1[2] * s[1], 0.0],
710        [c2[0] * s[2], c2[1] * s[2], c2[2] * s[2], 0.0],
711        [t[0], t[1], t[2], 1.0],
712    ]
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718
719    fn no_side(_: &str) -> Option<Vec<u8>> {
720        None
721    }
722
723    /// Wraps `json` and `bin` in a GLB container.
724    fn make_glb(json: &str, bin: &[u8]) -> Vec<u8> {
725        let mut j = json.as_bytes().to_vec();
726        while !j.len().is_multiple_of(4) {
727            j.push(b' ');
728        }
729        let mut b = bin.to_vec();
730        while !b.len().is_multiple_of(4) {
731            b.push(0);
732        }
733        let total = 12 + 8 + j.len() + if b.is_empty() { 0 } else { 8 + b.len() };
734        let mut out = Vec::new();
735        out.extend_from_slice(&GLB_MAGIC.to_le_bytes());
736        out.extend_from_slice(&2u32.to_le_bytes());
737        out.extend_from_slice(&(total as u32).to_le_bytes());
738        out.extend_from_slice(&(j.len() as u32).to_le_bytes());
739        out.extend_from_slice(&CHUNK_JSON.to_le_bytes());
740        out.extend_from_slice(&j);
741        if !b.is_empty() {
742            out.extend_from_slice(&(b.len() as u32).to_le_bytes());
743            out.extend_from_slice(&CHUNK_BIN.to_le_bytes());
744            out.extend_from_slice(&b);
745        }
746        out
747    }
748
749    /// Positions (0,0,0) (1,0,0) (0,1,0) then u16 indices 0 1 2, padded.
750    fn tri_bin() -> Vec<u8> {
751        let mut b = Vec::new();
752        for v in [0.0f32, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0] {
753            b.extend_from_slice(&v.to_le_bytes());
754        }
755        for i in [0u16, 1, 2, 0] {
756            b.extend_from_slice(&i.to_le_bytes());
757        }
758        b
759    }
760
761    /// JSON over `tri_bin`: `@B` is the buffer entry body, `@P` extra
762    /// primitive members, `@X` extra top-level members.
763    fn tri_json(buffer: &str, prim: &str, extra: &str) -> String {
764        r#"{"asset":{"version":"2.0"},"buffers":[{"byteLength":44@B}],
765        "bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":36},{"buffer":0,"byteOffset":36,"byteLength":6}],
766        "accessors":[{"bufferView":0,"componentType":5126,"count":3,"type":"VEC3"},{"bufferView":1,"componentType":5123,"count":3,"type":"SCALAR"}],
767        "meshes":[{"name":"tri","primitives":[{"attributes":{"POSITION":0},"indices":1@P}]}],
768        "materials":[{"name":"m","pbrMetallicRoughness":{"baseColorFactor":[0.5,0.5,0.5,0.5]}}]@X}"#
769            .replace("@B", buffer)
770            .replace("@P", prim)
771            .replace("@X", extra)
772    }
773
774    fn close(a: V3, b: V3) -> bool {
775        (0..3).all(|i| (a[i] - b[i]).abs() < 1e-4)
776    }
777
778    #[test]
779    fn single_triangle_glb() {
780        let glb = make_glb(&tri_json("", "", ""), &tri_bin());
781        let m = parse_glb(&glb, &no_side).unwrap();
782        assert_eq!(m.meshes.len(), 1);
783        let mesh = &m.meshes[0];
784        assert_eq!(mesh.name, "tri");
785        assert_eq!(mesh.indices, vec![0, 1, 2]);
786        assert_eq!(mesh.positions[1], [1.0, 0.0, 0.0]);
787        assert!(mesh.material.is_none());
788    }
789
790    #[test]
791    fn node_translation_is_baked() {
792        let extra = r#","nodes":[{"mesh":0,"translation":[1,2,3]}],"scene":0,"scenes":[{"nodes":[0]}]"#;
793        let glb = make_glb(&tri_json("", "", extra), &tri_bin());
794        let m = parse_glb(&glb, &no_side).unwrap();
795        assert!(close(m.meshes[0].positions[0], [1.0, 2.0, 3.0]));
796        assert!(close(m.meshes[0].positions[1], [2.0, 2.0, 3.0]));
797    }
798
799    #[test]
800    fn parent_and_child_compose() {
801        let extra = r#","nodes":[{"children":[1],"translation":[1,0,0],"scale":[2,2,2]},{"mesh":0,"translation":[0,1,0]}]"#;
802        let glb = make_glb(&tri_json("", "", extra), &tri_bin());
803        let m = parse_glb(&glb, &no_side).unwrap();
804        // Roots are derived: node 1 is a child, so only node 0 starts a walk.
805        assert_eq!(m.meshes.len(), 1);
806        assert!(close(m.meshes[0].positions[1], [3.0, 2.0, 0.0]));
807    }
808
809    #[test]
810    fn rotation_turns_positions_and_normals() {
811        let mut bin = tri_bin();
812        // Normals (0,0,1) x3 after the indices (offset 44).
813        for v in [0.0f32, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0] {
814            bin.extend_from_slice(&v.to_le_bytes());
815        }
816        let json = r#"{"buffers":[{"byteLength":80}],
817        "bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":36},{"buffer":0,"byteOffset":36,"byteLength":6},{"buffer":0,"byteOffset":44,"byteLength":36}],
818        "accessors":[{"bufferView":0,"componentType":5126,"count":3,"type":"VEC3"},{"bufferView":1,"componentType":5123,"count":3,"type":"SCALAR"},{"bufferView":2,"componentType":5126,"count":3,"type":"VEC3"}],
819        "meshes":[{"primitives":[{"attributes":{"POSITION":0,"NORMAL":2},"indices":1}]}],
820        "nodes":[{"mesh":0,"rotation":[0,0,0.70710678,0.70710678],"scale":[1,1,3]}]}"#;
821        let m = parse_glb(&make_glb(json, &bin), &no_side).unwrap();
822        let mesh = &m.meshes[0];
823        // 90 degrees about Z: (1,0,0) -> (0,1,0).
824        assert!(close(mesh.positions[1], [0.0, 1.0, 0.0]));
825        // The z stretch must not change a z normal's direction or length.
826        assert!(close(mesh.normals.as_ref().unwrap()[0], [0.0, 0.0, 1.0]));
827    }
828
829    #[test]
830    fn gltf_with_data_uri() {
831        let b64 = base64::engine::general_purpose::STANDARD.encode(tri_bin());
832        let buffer = format!(r#","uri":"data:application/octet-stream;base64,{b64}""#);
833        let m = parse_gltf(tri_json(&buffer, "", "").as_bytes(), &no_side).unwrap();
834        assert_eq!(m.triangle_count(), 1);
835    }
836
837    #[test]
838    fn gltf_with_external_buffer() {
839        let buffer = r#","uri":"my%20tri.bin""#;
840        let bin = tri_bin();
841        let side = |name: &str| (name == "my tri.bin").then(|| bin.clone());
842        let m = parse_gltf(tri_json(buffer, "", "").as_bytes(), &side).unwrap();
843        assert_eq!(m.triangle_count(), 1);
844        // Not served: an error.
845        assert!(parse_gltf(tri_json(buffer, "", "").as_bytes(), &no_side).is_err());
846    }
847
848    #[test]
849    fn base_color_factor_becomes_srgb() {
850        let glb = make_glb(&tri_json("", r#","material":0"#, ""), &tri_bin());
851        let mat = parse_glb(&glb, &no_side).unwrap().meshes[0].material.clone().unwrap();
852        assert_eq!(mat.name, "m");
853        // Linear 0.5 is about 0.7354 in sRGB.
854        assert!(close(mat.color, [0.7354; 3]));
855        assert!((mat.opacity - 0.5).abs() < 1e-6);
856        assert_eq!(srgb(0.0), 0.0);
857        assert!((srgb(1.0) - 1.0).abs() < 1e-6);
858    }
859
860    #[test]
861    fn triangle_strip_and_fan_become_lists() {
862        let mut bin = Vec::new();
863        for i in 0..4 {
864            for v in [i as f32, 0.0, 0.0] {
865                bin.extend_from_slice(&v.to_le_bytes());
866            }
867        }
868        let json = |mode: u32| {
869            format!(
870                r#"{{"buffers":[{{"byteLength":48}}],"bufferViews":[{{"buffer":0,"byteLength":48}}],
871                "accessors":[{{"bufferView":0,"componentType":5126,"count":4,"type":"VEC3"}}],
872                "meshes":[{{"primitives":[{{"attributes":{{"POSITION":0}},"mode":{mode}}}]}}]}}"#
873            )
874        };
875        let strip = parse_glb(&make_glb(&json(5), &bin), &no_side).unwrap();
876        assert_eq!(strip.meshes[0].indices, vec![0, 1, 2, 2, 1, 3]);
877        let fan = parse_glb(&make_glb(&json(6), &bin), &no_side).unwrap();
878        assert_eq!(fan.meshes[0].indices, vec![0, 1, 2, 0, 2, 3]);
879        // Points are skipped, leaving nothing.
880        assert!(parse_glb(&make_glb(&json(0), &bin), &no_side).is_err());
881    }
882
883    #[test]
884    fn bad_magic_is_an_error() {
885        let mut glb = make_glb(&tri_json("", "", ""), &tri_bin());
886        glb[0] = b'X';
887        assert!(parse_glb(&glb, &no_side).is_err());
888        assert!(parse_glb(&glb[..10], &no_side).is_err());
889        // A chunk longer than the file.
890        let mut glb = make_glb(&tri_json("", "", ""), &tri_bin());
891        glb[12] = 0xFF;
892        glb[13] = 0xFF;
893        assert!(parse_glb(&glb, &no_side).is_err());
894    }
895
896    #[test]
897    fn accessor_past_the_buffer_is_an_error() {
898        let json = tri_json("", "", "").replace(r#""count":3,"type":"VEC3""#, r#""count":1000,"type":"VEC3""#);
899        assert!(parse_glb(&make_glb(&json, &tri_bin()), &no_side).is_err());
900        // Short BIN chunk.
901        assert!(parse_glb(&make_glb(&tri_json("", "", ""), &tri_bin()[..20]), &no_side).is_err());
902    }
903
904    #[test]
905    fn mesh_used_by_two_nodes_appears_twice() {
906        let extra = r#","nodes":[{"mesh":0},{"mesh":0,"translation":[5,0,0]}],"scenes":[{"nodes":[0,1]}]"#;
907        let m = parse_glb(&make_glb(&tri_json("", "", extra), &tri_bin()), &no_side).unwrap();
908        assert_eq!(m.meshes.len(), 2);
909        assert!(close(m.meshes[1].positions[0], [5.0, 0.0, 0.0]));
910    }
911
912    #[test]
913    fn cycles_do_not_hang() {
914        let extra = r#","nodes":[{"mesh":0,"children":[1]},{"children":[0]}],"scenes":[{"nodes":[0]}]"#;
915        let m = parse_glb(&make_glb(&tri_json("", "", extra), &tri_bin()), &no_side).unwrap();
916        assert!(!m.meshes.is_empty());
917    }
918
919    /// A triangle with UVs (0,0) (1,0) (0,1) and a textured material:
920    /// `image` is the image entry's body, `bin_png` bytes go after the
921    /// geometry (as buffer view 3), `material` extra material members.
922    fn textured(image: &str, bin_png: &[u8], material: &str, texinfo: &str) -> Vec<u8> {
923        let mut bin = tri_bin();
924        for v in [0.0f32, 0.0, 1.0, 0.0, 0.0, 1.0] {
925            bin.extend_from_slice(&v.to_le_bytes());
926        }
927        let png_at = bin.len();
928        bin.extend_from_slice(bin_png);
929        let json = format!(
930            r#"{{"buffers":[{{"byteLength":{total}}}],
931            "bufferViews":[{{"buffer":0,"byteOffset":0,"byteLength":36}},{{"buffer":0,"byteOffset":36,"byteLength":6}},{{"buffer":0,"byteOffset":44,"byteLength":24}},{{"buffer":0,"byteOffset":{png_at},"byteLength":{png_len}}}],
932            "accessors":[{{"bufferView":0,"componentType":5126,"count":3,"type":"VEC3"}},{{"bufferView":1,"componentType":5123,"count":3,"type":"SCALAR"}},{{"bufferView":2,"componentType":5126,"count":3,"type":"VEC2"}}],
933            "meshes":[{{"primitives":[{{"attributes":{{"POSITION":0,"TEXCOORD_0":2}},"indices":1,"material":0}}]}}],
934            "materials":[{{"pbrMetallicRoughness":{{"baseColorFactor":[1,0.2140,1,1],"baseColorTexture":{{"index":0{texinfo}}}}}{material}}}],
935            "samplers":[{{"wrapS":33071,"wrapT":33648}}],
936            "textures":[{{"source":0,"sampler":0}}],
937            "images":[{{{image}}}]}}"#,
938            total = bin.len(),
939            png_len = bin_png.len(),
940        );
941        make_glb(&json, &bin)
942    }
943
944    #[test]
945    fn embedded_texture_is_read() {
946        let png = crate::model3d::texture::tests::png_2x2();
947        let glb = textured(r#""bufferView":3,"mimeType":"image/png""#, &png, r#","alphaMode":"MASK","alphaCutoff":0.25"#, "");
948        let m = parse_glb(&glb, &no_side).unwrap();
949        let mesh = &m.meshes[0];
950        assert_eq!(mesh.uvs.as_deref(), Some(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]][..]));
951        let t = mesh.texture.as_ref().expect("texture decoded");
952        assert_eq!((t.image.width, t.image.height), (2, 2));
953        assert_eq!(&t.image.rgba[..4], &[255, 0, 0, 255]);
954        assert_eq!(t.alpha_cutoff, Some(0.25));
955        // The factor tints, in display values (linear 0.214 is about 0.5).
956        assert!(close(t.tint, [1.0, 0.5, 1.0]), "{:?}", t.tint);
957        // The material colour is kept for when the texture can't be drawn.
958        assert!(close(mesh.material.as_ref().unwrap().color, [1.0, 0.5, 1.0]));
959        assert!(mesh.textured().is_some());
960    }
961
962    #[test]
963    fn data_uri_and_sidecar_textures() {
964        use base64::Engine;
965        let png = crate::model3d::texture::tests::png_2x2();
966        let uri = format!(r#""uri":"data:image/png;base64,{}""#, base64::engine::general_purpose::STANDARD.encode(&png));
967        let m = parse_glb(&textured(&uri, &[], "", ""), &no_side).unwrap();
968        assert_eq!(m.meshes[0].texture.as_ref().unwrap().image.width, 2);
969        assert_eq!(m.meshes[0].texture.as_ref().unwrap().alpha_cutoff, None, "opaque unless MASK");
970        // A file next to the model, its name percent-encoded.
971        let side = |n: &str| (n == "tex/my tex.png").then(|| png.clone());
972        let m = parse_glb(&textured(r#""uri":"tex/my%20tex.png""#, &[], "", ""), &side).unwrap();
973        assert!(m.meshes[0].texture.is_some());
974        // Missing: no texture, the material colour stays.
975        let m = parse_glb(&textured(r#""uri":"gone.png""#, &[], "", ""), &no_side).unwrap();
976        assert!(m.meshes[0].texture.is_none());
977        assert!(close(m.meshes[0].material.as_ref().unwrap().color, [1.0, 0.5, 1.0]));
978        // Outside the model's folder: only the bare name is asked for.
979        let asked = std::cell::RefCell::new(Vec::new());
980        let side = |n: &str| {
981            asked.borrow_mut().push(n.to_string());
982            None
983        };
984        parse_glb(&textured(r#""uri":"../up/x.png""#, &[], "", ""), &side).unwrap();
985        parse_glb(&textured(r#""uri":"/abs/x.png""#, &[], "", ""), &side).unwrap();
986        assert_eq!(*asked.borrow(), ["x.png", "x.png"]);
987    }
988
989    #[test]
990    fn texture_transform_is_baked_into_the_coordinates() {
991        let png = crate::model3d::texture::tests::png_2x2();
992        let ext = r#","extensions":{"KHR_texture_transform":{"offset":[0.5,0],"scale":[2,3]}}"#;
993        let m = parse_glb(&textured(r#""bufferView":3"#, &png, "", ext), &no_side).unwrap();
994        assert_eq!(m.meshes[0].uvs.as_deref(), Some(&[[0.5, 0.0], [2.5, 0.0], [0.5, 3.0]][..]));
995        let rot = r#","extensions":{"KHR_texture_transform":{"rotation":1.5707964}}"#;
996        let uv = parse_glb(&textured(r#""bufferView":3"#, &png, "", rot), &no_side).unwrap().meshes[0].uvs.clone().unwrap();
997        assert!((uv[1][0]).abs() < 1e-6 && (uv[1][1] + 1.0).abs() < 1e-6, "{uv:?}");
998    }
999
1000    /// A triangle with UVs and `material` (the whole material object) over
1001    /// one texture, index 0, that is `png` in buffer view 3.
1002    fn with_material(material: &str, png: &[u8]) -> Vec<u8> {
1003        let mut bin = tri_bin();
1004        for v in [0.0f32, 0.0, 1.0, 0.0, 0.0, 1.0] {
1005            bin.extend_from_slice(&v.to_le_bytes());
1006        }
1007        let png_at = bin.len();
1008        bin.extend_from_slice(png);
1009        let json = format!(
1010            r#"{{"buffers":[{{"byteLength":{total}}}],
1011            "bufferViews":[{{"buffer":0,"byteOffset":0,"byteLength":36}},{{"buffer":0,"byteOffset":36,"byteLength":6}},{{"buffer":0,"byteOffset":44,"byteLength":24}},{{"buffer":0,"byteOffset":{png_at},"byteLength":{png_len}}}],
1012            "accessors":[{{"bufferView":0,"componentType":5126,"count":3,"type":"VEC3"}},{{"bufferView":1,"componentType":5123,"count":3,"type":"SCALAR"}},{{"bufferView":2,"componentType":5126,"count":3,"type":"VEC2"}}],
1013            "meshes":[{{"primitives":[{{"attributes":{{"POSITION":0,"TEXCOORD_0":2}},"indices":1,"material":0}}]}}],
1014            "materials":[{material}],
1015            "samplers":[{{"wrapS":33071,"wrapT":33648}}],
1016            "textures":[{{"source":0,"sampler":0}}],
1017            "images":[{{"bufferView":3}}]}}"#,
1018            total = bin.len(),
1019            png_len = png.len(),
1020        );
1021        make_glb(&json, &bin)
1022    }
1023
1024    fn read_material(material: &str) -> Mesh {
1025        let png = crate::model3d::texture::tests::png_2x2();
1026        let m = parse_glb(&with_material(material, &png), &no_side).unwrap();
1027        assert_eq!(m.meshes.len(), 1);
1028        m.meshes.into_iter().next().unwrap()
1029    }
1030
1031    #[test]
1032    fn normal_and_emissive_maps_are_read() {
1033        let mesh = read_material(r#"{"normalTexture":{"index":0,"scale":3},"emissiveTexture":{"index":0},"emissiveFactor":[1,0.2140,0]}"#);
1034        let s = &mesh.surface;
1035        let n = s.normal_map.as_ref().expect("normal map");
1036        assert_eq!((n.image.width, n.image.height), (2, 2));
1037        assert!(s.emissive_map.is_some());
1038        assert!(close(s.emissive, [1.0, 0.5, 0.0]), "{:?}", s.emissive);
1039        assert!(!s.blend);
1040        assert!(mesh.texture.is_none());
1041        assert_eq!(mesh.uvs.as_deref(), Some(&[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]][..]));
1042    }
1043
1044    #[test]
1045    fn emissive_map_without_factor_still_loads() {
1046        let mesh = read_material(r#"{"emissiveTexture":{"index":0}}"#);
1047        assert!(mesh.surface.emissive_map.is_some());
1048        assert_eq!(mesh.surface.emissive, [0.0; 3]);
1049    }
1050
1051    #[test]
1052    fn alpha_modes_set_blend() {
1053        let png = crate::model3d::texture::tests::png_2x2();
1054        let base = r#""pbrMetallicRoughness":{"baseColorFactor":[1,1,1,0.5],"baseColorTexture":{"index":0}}"#;
1055        let get = |mode: &str| {
1056            let mat = if mode.is_empty() { format!("{{{base}}}") } else { format!(r#"{{{base},"alphaMode":"{mode}","alphaCutoff":0.3}}"#) };
1057            parse_glb(&with_material(&mat, &png), &no_side).unwrap().meshes.remove(0)
1058        };
1059        assert!(get("BLEND").surface.blend);
1060        assert_eq!(get("BLEND").texture.unwrap().alpha_cutoff, None);
1061        let mask = get("MASK");
1062        assert!(mask.surface.blend);
1063        assert_eq!(mask.texture.unwrap().alpha_cutoff, Some(0.3));
1064        for m in ["OPAQUE", ""] {
1065            let mesh = get(m);
1066            assert!(!mesh.surface.blend);
1067            assert!((mesh.material.unwrap().opacity - 0.5).abs() < 1e-6, "kept, though ignored");
1068        }
1069        // MASK with no base colour texture has no cutoff to keep.
1070        assert!(read_material(r#"{"alphaMode":"MASK"}"#).surface.blend);
1071    }
1072
1073    #[test]
1074    fn specular_glossiness_drops_the_surface() {
1075        let mesh = read_material(r#"{"normalTexture":{"index":0},"emissiveTexture":{"index":0},"emissiveFactor":[1,1,1],"alphaMode":"BLEND","extensions":{"KHR_materials_pbrSpecularGlossiness":{}}}"#);
1076        assert_eq!(mesh.surface, Surface::default());
1077    }
1078
1079    #[test]
1080    fn coordinates_follow_base_then_normal_then_emissive() {
1081        let ext = r#","extensions":{"KHR_texture_transform":{"offset":[0.5,0],"scale":[2,3]}}"#;
1082        let mesh = read_material(&format!(r#"{{"normalTexture":{{"index":0{ext}}},"emissiveTexture":{{"index":0}}}}"#));
1083        assert_eq!(mesh.uvs.as_deref(), Some(&[[0.5, 0.0], [2.5, 0.0], [0.5, 3.0]][..]));
1084        // Emissive alone, and a base colour texture ahead of both.
1085        let mesh = read_material(&format!(r#"{{"emissiveTexture":{{"index":0{ext}}}}}"#));
1086        assert_eq!(mesh.uvs.as_ref().unwrap()[1], [2.5, 0.0]);
1087        let mesh = read_material(&format!(r#"{{"pbrMetallicRoughness":{{"baseColorTexture":{{"index":0}}}},"normalTexture":{{"index":0{ext}}}}}"#));
1088        assert_eq!(mesh.uvs.as_ref().unwrap()[1], [1.0, 0.0]);
1089        // A set the mesh doesn't have: no coordinates, so no maps.
1090        let mesh = read_material(r#"{"normalTexture":{"index":0,"texCoord":1}}"#);
1091        assert!(mesh.uvs.is_none());
1092        assert!(mesh.surface.normal_map.is_none());
1093    }
1094
1095    #[test]
1096    fn bad_maps_and_factors_keep_the_mesh() {
1097        // Index out of range, texture without a usable image.
1098        let mesh = read_material(r#"{"normalTexture":{"index":9},"emissiveTexture":{"index":0},"emissiveFactor":[0.5,0.5,0.5]}"#);
1099        assert!(mesh.surface.normal_map.is_none());
1100        assert!(mesh.surface.emissive_map.is_some());
1101        assert!(mesh.surface.emissive[0] > 0.7);
1102        // Undecodable bytes.
1103        let m = parse_glb(&with_material(r#"{"normalTexture":{"index":0},"emissiveTexture":{"index":0}}"#, b"not a picture"), &no_side).unwrap();
1104        assert_eq!(m.meshes.len(), 1);
1105        assert!(m.meshes[0].surface.normal_map.is_none() && m.meshes[0].surface.emissive_map.is_none());
1106        // Wrongly shaped values lose only themselves.
1107        for f in ["[1,2]", "[1,2,3,4]", "[1,null,0]", r#"["a",0,0]"#, "5", "null"] {
1108            let mesh = read_material(&format!(r#"{{"emissiveFactor":{f},"alphaMode":"BLEND"}}"#));
1109            assert_eq!(mesh.surface.emissive, [0.0; 3], "{f}");
1110            assert!(mesh.surface.blend);
1111        }
1112        let mesh = read_material(r#"{"normalTexture":"x","emissiveTexture":[],"emissiveFactor":[2,-1,0]}"#);
1113        assert!(mesh.surface.normal_map.is_none() && mesh.surface.emissive_map.is_none());
1114        assert!(close(mesh.surface.emissive, [1.0, 0.0, 0.0]), "clamped");
1115    }
1116
1117    #[test]
1118    fn vertex_colors_normalized_u8() {
1119        let mut bin = tri_bin();
1120        bin.extend_from_slice(&[255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0]);
1121        let json = r#"{"buffers":[{"byteLength":56}],
1122        "bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":36},{"buffer":0,"byteOffset":36,"byteLength":6},{"buffer":0,"byteOffset":44,"byteLength":9}],
1123        "accessors":[{"bufferView":0,"componentType":5126,"count":3,"type":"VEC3"},{"bufferView":1,"componentType":5123,"count":3,"type":"SCALAR"},{"bufferView":2,"componentType":5121,"normalized":true,"count":3,"type":"VEC3"}],
1124        "meshes":[{"primitives":[{"attributes":{"POSITION":0,"COLOR_0":2},"indices":1}]}]}"#;
1125        let m = parse_glb(&make_glb(json, &bin), &no_side).unwrap();
1126        let c = m.meshes[0].colors.as_ref().unwrap();
1127        assert!(close(c[0], [1.0, 0.0, 0.0]));
1128        assert!(close(c[2], [0.0, 0.0, 1.0]));
1129    }
1130}