Skip to main content

falcon_library/model3d/
dae.rs

1//! COLLADA (`.dae`), versions 1.4.1 and 1.5: XML read with `roxmltree`.
2//!
3//! Read: the meshes of `library_geometries` (`triangles`, `polylist`,
4//! `polygons`, `tristrips` and `trifans`, polygons fan-triangulated, holes
5//! of `ph` ignored), their sources through accessors with offset, stride
6//! and named params, positions, normals and vertex colours; the node tree
7//! of the visual scene with `matrix`, `translate`, `rotate`, `scale` and
8//! `lookat` composed into world transforms, `instance_geometry`,
9//! `instance_node` and `instance_controller` (the skin's or morph's base
10//! mesh in its bind pose, after the skin's bind shape matrix); the
11//! `up_axis` and `unit` of the root `asset`, turned into falcon's Y-up
12//! metres the way three.js's loader (used by Eagle's viewer) does;
13//! material base colours from the `library_effects` diffuse (emission for
14//! `constant`), or, when the diffuse is a texture whose image sits next to
15//! the file (or is embedded as `hex`), that image's average colour. The
16//! texture is also kept for drawing: the image through the profile's
17//! sampler and surface (1.4.1) or `instance_image` (1.5) (the sampler's
18//! `wrap_s` and `wrap_t` are ignored, textures always repeat), and the `TEXCOORD` set the
19//! texture's `texcoord` is bound to by the instance's `bind_vertex_input`
20//! (else the first set), with v flipped to falcon's picture space.
21//!
22//! A `bump` texture (the shader's own child, or in an `extra` technique of
23//! the shader's technique, the profile or the effect, the latter winning) is
24//! drawn as a normal map, as Eagle's viewer does: its picture goes through
25//! the same sampler route and takes the mesh's one set of coordinates.
26//! Transparency follows `transparent` (its `opaque` mode, colour) and
27//! `transparency`: the opacity of the material, and, with a texture in
28//! `transparent`, blending by the diffuse picture's alpha alone.
29//!
30//! Not read: animation, skinning weights and joints, morph targets,
31//! cameras, lights, `skew`, lines, texture transforms (`extra` techniques)
32//! and emission (colour or texture, apart from `constant` shaders using it
33//! as the base colour): Eagle's viewer shows none, so `emissive` stays
34//! black. A missing normal
35//! input leaves `normals` empty; the viewer then shades flat or generates
36//! smooth normals as for any other format.
37
38use std::collections::HashMap;
39use std::rc::Rc;
40
41use roxmltree::{Document, Node};
42
43use super::math::{self, M4, V3};
44use super::texture::{Loader, Texture};
45use super::{DEFAULT_COLOR, Material, Mesh, Model, ParseError, Result, Surface};
46
47/// Deepest node nesting followed (also stops `instance_node` cycles).
48const MAX_DEPTH: usize = 32;
49/// Most nodes visited, against `instance_node` fan-out.
50const MAX_VISITS: usize = 100_000;
51/// Most triangles produced over all instances.
52const MAX_TRIANGLES: usize = 50_000_000;
53/// Most XML nodes parsed.
54const MAX_XML_NODES: u32 = 20_000_000;
55
56type Side<'s> = &'s dyn Fn(&str) -> Option<Vec<u8>>;
57
58pub fn parse(bytes: &[u8], side: Side) -> Result<Model> {
59    let text = String::from_utf8_lossy(bytes);
60    let opts = roxmltree::ParsingOptions { allow_dtd: true, nodes_limit: MAX_XML_NODES, ..Default::default() };
61    let doc = Document::parse_with_options(&text, opts).map_err(|e| ParseError(format!("not a COLLADA file: {e}")))?;
62    let root = doc.root_element();
63    if root.tag_name().name() != "COLLADA" {
64        return Err(ParseError::new("not a COLLADA file"));
65    }
66    let mut r = Reader::new(root, Loader::new(side));
67    r.read()?;
68    Model { meshes: r.meshes }.finish()
69}
70
71// ---- XML helpers -----------------------------------------------------------
72
73fn child<'a, 'i>(n: Node<'a, 'i>, name: &str) -> Option<Node<'a, 'i>> {
74    n.children().find(|c| c.is_element() && c.tag_name().name() == name)
75}
76
77fn children<'a, 'i: 'a>(n: Node<'a, 'i>, name: &'a str) -> impl Iterator<Item = Node<'a, 'i>> + 'a {
78    n.children().filter(move |c| c.is_element() && c.tag_name().name() == name)
79}
80
81/// `#id` (or a bare id) to the id.
82fn url_id(s: &str) -> &str {
83    s.trim().trim_start_matches('#')
84}
85
86fn floats(n: Node) -> Vec<f32> {
87    n.text().unwrap_or("").split_ascii_whitespace().map(|t| t.parse::<f32>().ok().filter(|v| v.is_finite()).unwrap_or(0.0)).collect()
88}
89
90/// Integers of a `p` or `vcount`; anything that is not a valid index
91/// becomes `u32::MAX`, which no source is long enough to reach.
92fn indices(n: Node) -> Vec<u32> {
93    n.text().unwrap_or("").split_ascii_whitespace().map(|t| t.parse::<u32>().unwrap_or(u32::MAX)).collect()
94}
95
96// ---- transforms --------------------------------------------------------------
97
98const IDENTITY: M4 = [[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]];
99
100/// A COLLADA `matrix` (16 values, row by row) as falcon's column-major
101/// matrix.
102fn row_major(v: &[f32]) -> M4 {
103    let mut m = IDENTITY;
104    if v.len() >= 16 {
105        for (r, row) in v.chunks(4).take(4).enumerate() {
106            for (c, &x) in row.iter().enumerate() {
107                m[c][r] = x;
108            }
109        }
110    }
111    m
112}
113
114fn translation(t: V3) -> M4 {
115    let mut m = IDENTITY;
116    m[3] = [t[0], t[1], t[2], 1.0];
117    m
118}
119
120fn scaling(s: V3) -> M4 {
121    let mut m = IDENTITY;
122    for (k, &v) in s.iter().enumerate() {
123        m[k][k] = v;
124    }
125    m
126}
127
128/// Rotation by `deg` degrees about `axis`, counter-clockwise looking down
129/// the axis.
130fn rotation(axis: V3, deg: f32) -> M4 {
131    let a = math::normalize(axis);
132    if a == [0.0; 3] {
133        return IDENTITY;
134    }
135    let mut m = IDENTITY;
136    for (k, col) in m.iter_mut().take(3).enumerate() {
137        let mut e = [0.0; 3];
138        e[k] = 1.0;
139        let r = math::rotate_about(e, [0.0; 3], a, deg.to_radians());
140        *col = [r[0], r[1], r[2], 0.0];
141    }
142    m
143}
144
145/// `lookat`: eye, target, up; places the node at the eye looking at the
146/// target (the inverse of a view matrix).
147fn lookat(v: &[f32]) -> M4 {
148    let [ex, ey, ez, tx, ty, tz, ux, uy, uz] = v[..] else { return IDENTITY };
149    let eye = [ex, ey, ez];
150    let f = math::normalize(math::sub([tx, ty, tz], eye));
151    let s = math::normalize(math::cross(f, [ux, uy, uz]));
152    let u = math::cross(s, f);
153    if s == [0.0; 3] || f == [0.0; 3] {
154        return translation(eye);
155    }
156    [[s[0], s[1], s[2], 0.0], [u[0], u[1], u[2], 0.0], [-f[0], -f[1], -f[2], 0.0], [ex, ey, ez, 1.0]]
157}
158
159/// The node's own transform: its transform elements in document order.
160fn local_transform(node: Node) -> M4 {
161    let mut m = IDENTITY;
162    for c in node.children().filter(|c| c.is_element()) {
163        let v = floats(c);
164        let t = match c.tag_name().name() {
165            "matrix" => row_major(&v),
166            "translate" if v.len() >= 3 => translation([v[0], v[1], v[2]]),
167            "scale" if v.len() >= 3 => scaling([v[0], v[1], v[2]]),
168            "rotate" if v.len() >= 4 => rotation([v[0], v[1], v[2]], v[3]),
169            "lookat" if v.len() >= 9 => lookat(&v[..9]),
170            _ => continue,
171        };
172        m = math::mul(&m, &t);
173    }
174    m
175}
176
177fn det3(m: &M4) -> f32 {
178    let c = |i: usize, j: usize| m[j][i];
179    c(0, 0) * (c(1, 1) * c(2, 2) - c(1, 2) * c(2, 1)) - c(0, 1) * (c(1, 0) * c(2, 2) - c(1, 2) * c(2, 0)) + c(0, 2) * (c(1, 0) * c(2, 1) - c(1, 1) * c(2, 0))
180}
181
182/// The matrix that carries normals: the inverse transpose of the upper
183/// 3×3, up to scale (the cofactor matrix), applied as `n' = N n`.
184fn normal_matrix(m: &M4) -> [[f32; 3]; 3] {
185    let a = |r: usize, c: usize| m[c][r];
186    let mut n = [[0.0; 3]; 3];
187    for (r, row) in n.iter_mut().enumerate() {
188        for (c, cell) in row.iter_mut().enumerate() {
189            let (r1, r2) = ((r + 1) % 3, (r + 2) % 3);
190            let (c1, c2) = ((c + 1) % 3, (c + 2) % 3);
191            *cell = a(r1, c1) * a(r2, c2) - a(r1, c2) * a(r2, c1);
192        }
193    }
194    n
195}
196
197fn apply3(n: &[[f32; 3]; 3], v: V3) -> V3 {
198    let k = |r: usize| n[r][0] * v[0] + n[r][1] * v[1] + n[r][2] * v[2];
199    math::normalize([k(0), k(1), k(2)])
200}
201
202/// The root `asset`'s up axis and unit as one matrix into Y-up metres.
203fn asset_matrix(root: Node) -> M4 {
204    let Some(asset) = child(root, "asset") else { return IDENTITY };
205    let up = child(asset, "up_axis").and_then(|n| n.text()).unwrap_or("Y_UP").trim().to_ascii_uppercase();
206    let axes = match up.as_str() {
207        // (x, y, z) -> (x, z, -y): three.js turns Z-up scenes -90° about X.
208        "Z_UP" => [[1.0, 0.0, 0.0, 0.0], [0.0, 0.0, -1.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]],
209        // (x, y, z) -> (-y, x, z): +X becomes up.
210        "X_UP" => [[0.0, 1.0, 0.0, 0.0], [-1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]],
211        _ => IDENTITY,
212    };
213    let meter = child(asset, "unit").and_then(|n| n.attribute("meter")).and_then(|s| s.trim().parse::<f32>().ok()).filter(|m| m.is_finite() && *m > 0.0).unwrap_or(1.0);
214    math::mul(&scaling([meter; 3]), &axes)
215}
216
217// ---- meshes ----------------------------------------------------------------
218
219/// One primitive of a geometry, in the geometry's own space.
220struct Prim {
221    positions: Vec<V3>,
222    normals: Option<Vec<V3>>,
223    colors: Option<Vec<V3>>,
224    /// Texture coordinates (v down) of each `TEXCOORD` set, by its `set`.
225    uvs: Vec<(Option<u32>, Vec<[f32; 2]>)>,
226    indices: Vec<u32>,
227    /// The `material` symbol, bound per instance.
228    material: Option<String>,
229}
230
231/// A source's values through its accessor, `k` components per element
232/// (missing ones are 0).
233fn read_source(src: Node, k: usize) -> Vec<V3> {
234    let Some(arr) = child(src, "float_array") else { return Vec::new() };
235    let data = floats(arr);
236    let acc = child(src, "technique_common").and_then(|t| child(t, "accessor"));
237    let attr = |name: &str, default: usize| acc.and_then(|a| a.attribute(name)).and_then(|s| s.trim().parse::<usize>().ok()).unwrap_or(default);
238    let stride = attr("stride", 1).max(1);
239    let offset = attr("offset", 0);
240    let avail = data.len().saturating_sub(offset) / stride;
241    let count = attr("count", avail).min(avail);
242    // The components to read: the named params, else the first ones.
243    let mut cols: Vec<usize> = acc.map(|a| children(a, "param").enumerate().filter(|(_, p)| p.attribute("name").is_some_and(|n| !n.is_empty())).map(|(i, _)| i).collect()).unwrap_or_default();
244    if cols.is_empty() {
245        cols = (0..stride.min(k)).collect();
246    }
247    cols.retain(|&c| c < stride);
248    cols.truncate(k);
249    (0..count)
250        .map(|i| {
251            let base = offset + i * stride;
252            let mut v = [0.0; 3];
253            for (slot, &c) in v.iter_mut().zip(&cols) {
254                *slot = data[base + c];
255            }
256            v
257        })
258        .collect()
259}
260
261/// Most `TEXCOORD` sets kept per primitive.
262const MAX_UV_SETS: usize = 4;
263
264/// An input's `set` attribute.
265fn set_of(input: Node) -> Option<u32> {
266    input.attribute("set").and_then(|s| s.trim().parse().ok())
267}
268
269struct Inputs {
270    position: Option<(Vec<V3>, usize)>,
271    normal: Option<(Vec<V3>, usize)>,
272    color: Option<(Vec<V3>, usize)>,
273    /// The `TEXCOORD` inputs (at most [`MAX_UV_SETS`]): set, values, offset.
274    texcoords: Vec<(Option<u32>, Vec<V3>, usize)>,
275    /// Indices per corner in `p`.
276    stride: usize,
277}
278
279fn read_inputs(prim: Node, mesh: Node, ids: &HashMap<&str, Node>) -> Inputs {
280    let mut out = Inputs { position: None, normal: None, color: None, texcoords: Vec::new(), stride: 1 };
281    let source = |id: &str, k: usize| ids.get(url_id(id)).map(|n| read_source(*n, k)).unwrap_or_default();
282    for input in children(prim, "input") {
283        let offset = input.attribute("offset").and_then(|s| s.trim().parse::<usize>().ok()).unwrap_or(0);
284        // A corner never has more than a few dozen indices; a silly offset
285        // is ignored rather than allowed to stretch every read.
286        if offset > 64 {
287            continue;
288        }
289        out.stride = out.stride.max(offset + 1);
290        let src = input.attribute("source").unwrap_or("");
291        match input.attribute("semantic").unwrap_or("") {
292            "VERTEX" => {
293                // The `vertices` element of this mesh: POSITION and friends,
294                // all indexed at this offset.
295                let Some(v) = children(mesh, "vertices").find(|v| v.attribute("id") == Some(url_id(src))) else { continue };
296                for vi in children(v, "input") {
297                    let s = vi.attribute("source").unwrap_or("");
298                    match vi.attribute("semantic").unwrap_or("") {
299                        "TEXCOORD" if out.texcoords.len() < MAX_UV_SETS => out.texcoords.push((set_of(vi), source(s, 2), offset)),
300                        "POSITION" => out.position = Some((source(s, 3), offset)),
301                        "NORMAL" if out.normal.is_none() => out.normal = Some((source(s, 3), offset)),
302                        "COLOR" if out.color.is_none() => out.color = Some((source(s, 3), offset)),
303                        _ => {}
304                    }
305                }
306            }
307            "NORMAL" => out.normal = Some((source(src, 3), offset)),
308            "TEXCOORD" if out.texcoords.len() < MAX_UV_SETS => out.texcoords.push((set_of(input), source(src, 2), offset)),
309            "COLOR" if out.color.is_none() => out.color = Some((source(src, 3), offset)),
310            _ => {}
311        }
312    }
313    out
314}
315
316/// Builds a primitive from polygons given as lists of corners, each corner
317/// being the start of its indices in `p`.
318struct Builder<'a> {
319    inputs: &'a Inputs,
320    p: &'a [u32],
321    prim: Prim,
322    seen: HashMap<(u32, u32, u32, [u32; MAX_UV_SETS]), u32>,
323}
324
325impl<'a> Builder<'a> {
326    fn new(inputs: &'a Inputs, p: &'a [u32], material: Option<String>) -> Self {
327        let prim = Prim { positions: Vec::new(), normals: inputs.normal.as_ref().map(|_| Vec::new()), colors: inputs.color.as_ref().map(|_| Vec::new()), uvs: inputs.texcoords.iter().map(|t| (t.0, Vec::new())).collect(), indices: Vec::new(), material };
328        Builder { inputs, p, prim, seen: HashMap::new() }
329    }
330
331    /// The output vertex of corner number `c` of `p`, if its indices are
332    /// all in range.
333    fn corner(&mut self, c: usize) -> Option<u32> {
334        let base = c.checked_mul(self.inputs.stride)?;
335        let at = |src: &Option<(Vec<V3>, usize)>| -> Option<Option<u32>> {
336            match src {
337                None => Some(None),
338                Some((vals, off)) => {
339                    let i = *self.p.get(base + off)?;
340                    ((i as usize) < vals.len()).then_some(Some(i))
341                }
342            }
343        };
344        let (pos, nrm, col) = (at(&self.inputs.position)??, at(&self.inputs.normal)?, at(&self.inputs.color)?);
345        let mut tcs = [0u32; MAX_UV_SETS];
346        for (slot, (_, vals, off)) in tcs.iter_mut().zip(&self.inputs.texcoords) {
347            let i = *self.p.get(base + off)?;
348            if i as usize >= vals.len() {
349                return None;
350            }
351            *slot = i;
352        }
353        let key = (pos, nrm.unwrap_or(0), col.unwrap_or(0), tcs);
354        if let Some(&v) = self.seen.get(&key) {
355            return Some(v);
356        }
357        let v = self.prim.positions.len() as u32;
358        self.prim.positions.push(self.inputs.position.as_ref()?.0[pos as usize]);
359        if let (Some(out), Some((vals, _)), Some(i)) = (self.prim.normals.as_mut(), &self.inputs.normal, nrm) {
360            out.push(vals[i as usize]);
361        }
362        if let (Some(out), Some((vals, _)), Some(i)) = (self.prim.colors.as_mut(), &self.inputs.color, col) {
363            out.push(vals[i as usize]);
364        }
365        for (k, (_, vals, _)) in self.inputs.texcoords.iter().enumerate() {
366            let uv = vals[tcs[k] as usize];
367            self.prim.uvs[k].1.push([uv[0], 1.0 - uv[1]]);
368        }
369        self.seen.insert(key, v);
370        Some(v)
371    }
372
373    /// A polygon of `n` corners starting at corner `first`, fanned.
374    fn polygon(&mut self, first: usize, n: usize) {
375        let Some(vs) = (first..first + n).map(|c| self.corner(c)).collect::<Option<Vec<u32>>>() else { return };
376        for k in 1..n.saturating_sub(1) {
377            self.prim.indices.extend([vs[0], vs[k], vs[k + 1]]);
378        }
379    }
380
381    fn strip(&mut self, n: usize) {
382        let Some(vs) = (0..n).map(|c| self.corner(c)).collect::<Option<Vec<u32>>>() else { return };
383        for k in 0..n.saturating_sub(2) {
384            let t = if k % 2 == 0 { [vs[k], vs[k + 1], vs[k + 2]] } else { [vs[k + 1], vs[k], vs[k + 2]] };
385            self.prim.indices.extend(t);
386        }
387    }
388
389    fn corners(&self) -> usize {
390        self.p.len() / self.inputs.stride
391    }
392}
393
394/// The primitives of a `mesh` element.
395fn read_mesh(mesh: Node, ids: &HashMap<&str, Node>) -> Vec<Prim> {
396    let mut out = Vec::new();
397    for prim in mesh.children().filter(|c| c.is_element()) {
398        let kind = prim.tag_name().name();
399        if !matches!(kind, "triangles" | "polylist" | "polygons" | "tristrips" | "trifans") {
400            continue;
401        }
402        let inputs = read_inputs(prim, mesh, ids);
403        if inputs.position.is_none() {
404            continue;
405        }
406        let material = prim.attribute("material").map(str::to_string);
407        // Each `p` (or the `p` of each `ph`) as its own polygon or strip.
408        let lists: Vec<Vec<u32>> = match kind {
409            "triangles" | "polylist" => child(prim, "p").map(indices).into_iter().collect(),
410            _ => prim.children().filter(|c| c.is_element()).filter_map(|c| match c.tag_name().name() {
411                "p" => Some(indices(c)),
412                "ph" => child(c, "p").map(indices),
413                _ => None,
414            }).collect(),
415        };
416        let mut b = Builder::new(&inputs, &[], material);
417        for p in &lists {
418            b.p = p;
419            match kind {
420                "triangles" => {
421                    for t in 0..b.corners() / 3 {
422                        b.polygon(t * 3, 3);
423                    }
424                }
425                "polylist" => {
426                    let counts = child(prim, "vcount").map(indices).unwrap_or_default();
427                    let mut at = 0usize;
428                    for &n in &counts {
429                        let n = n as usize;
430                        if n > b.corners().saturating_sub(at) {
431                            break;
432                        }
433                        b.polygon(at, n);
434                        at += n;
435                    }
436                }
437                "tristrips" => {
438                    let n = b.corners();
439                    b.strip(n);
440                }
441                // polygons and trifans: one polygon per `p`.
442                _ => {
443                    let n = b.corners();
444                    b.polygon(0, n);
445                }
446            }
447        }
448        out.push(b.prim);
449    }
450    out
451}
452
453// ---- materials -------------------------------------------------------------
454
455/// Undoes `%xx` escapes of a URI path.
456fn percent_decode(s: &str) -> String {
457    let b = s.as_bytes();
458    let mut out = Vec::with_capacity(b.len());
459    let mut i = 0;
460    while i < b.len() {
461        if b[i] == b'%'
462            && let Some(v) = s.get(i + 1..i + 3).and_then(|h| u8::from_str_radix(h, 16).ok())
463        {
464            out.push(v);
465            i += 3;
466            continue;
467        }
468        out.push(b[i]);
469        i += 1;
470    }
471    String::from_utf8_lossy(&out).into_owned()
472}
473
474// ---- the scene ---------------------------------------------------------------
475
476/// What a material adds to its colour and picture: the normal map (from
477/// `bump`) with the `texcoord` name it names, and whether it blends.
478#[derive(Clone)]
479struct Extra {
480    normal: Option<(Texture, String)>,
481    blend: bool,
482}
483
484/// A material as bound: its colour (with the opacity) and, when its diffuse
485/// is a picture that could be read, the texture and the `texcoord` name it
486/// uses, then the rest.
487type Resolved = (Material, Option<(Texture, String)>, Extra);
488
489struct Reader<'a, 'i, 's> {
490    root: Node<'a, 'i>,
491    ids: HashMap<&'a str, Node<'a, 'i>>,
492    loader: Loader<'s>,
493    up: M4,
494    prims: HashMap<String, Rc<Vec<Prim>>>,
495    materials: HashMap<String, Option<Resolved>>,
496    visits: usize,
497    triangles: usize,
498    meshes: Vec<Mesh>,
499}
500
501impl<'a, 'i, 's> Reader<'a, 'i, 's> {
502    fn new(root: Node<'a, 'i>, loader: Loader<'s>) -> Self {
503        let mut ids = HashMap::new();
504        for n in root.descendants().filter(|n| n.is_element()) {
505            if let Some(id) = n.attribute("id") {
506                ids.entry(id).or_insert(n);
507            }
508        }
509        Reader { root, ids, loader, up: asset_matrix(root), prims: HashMap::new(), materials: HashMap::new(), visits: 0, triangles: 0, meshes: Vec::new() }
510    }
511
512    fn read(&mut self) -> Result<()> {
513        let scene = child(self.root, "scene")
514            .and_then(|s| child(s, "instance_visual_scene"))
515            .and_then(|i| i.attribute("url"))
516            .and_then(|u| self.ids.get(url_id(u)).copied())
517            .or_else(|| child(self.root, "library_visual_scenes").and_then(|l| child(l, "visual_scene")));
518        match scene {
519            Some(vs) => {
520                let up = self.up;
521                for node in children(vs, "node") {
522                    self.visit(node, up, 0)?;
523                }
524            }
525            None => {
526                // No scene: every geometry once, untransformed.
527                let geoms: Vec<Node> = child(self.root, "library_geometries").map(|l| children(l, "geometry").collect()).unwrap_or_default();
528                for g in geoms {
529                    let up = self.up;
530                    self.emit(g, &up, None, "")?;
531                }
532            }
533        }
534        Ok(())
535    }
536
537    fn visit(&mut self, node: Node<'a, 'i>, parent: M4, depth: usize) -> Result<()> {
538        self.visits += 1;
539        if depth > MAX_DEPTH || self.visits > MAX_VISITS {
540            return Ok(());
541        }
542        let world = math::mul(&parent, &local_transform(node));
543        let name = node.attribute("name").or(node.attribute("id")).unwrap_or("");
544        for c in node.children().filter(|c| c.is_element()) {
545            let target = || c.attribute("url").and_then(|u| self.ids.get(url_id(u)).copied());
546            match c.tag_name().name() {
547                "node" => self.visit(c, world, depth + 1)?,
548                "instance_node" => {
549                    if let Some(n) = target().filter(|n| n.tag_name().name() == "node") {
550                        self.visit(n, world, depth + 1)?;
551                    }
552                }
553                "instance_geometry" => {
554                    if let Some(g) = target().filter(|n| n.tag_name().name() == "geometry") {
555                        self.emit(g, &world, child(c, "bind_material"), name)?;
556                    }
557                }
558                "instance_controller" => {
559                    if let Some((g, bind)) = target().and_then(|ctl| self.controller_base(ctl)) {
560                        let m = math::mul(&world, &bind);
561                        self.emit(g, &m, child(c, "bind_material"), name)?;
562                    }
563                }
564                _ => {}
565            }
566        }
567        Ok(())
568    }
569
570    /// The geometry under a controller (following skins of morphs) and the
571    /// skin's bind shape matrix.
572    fn controller_base(&self, mut ctl: Node<'a, 'i>) -> Option<(Node<'a, 'i>, M4)> {
573        let mut bind = IDENTITY;
574        for _ in 0..8 {
575            match ctl.tag_name().name() {
576                "geometry" => return Some((ctl, bind)),
577                "controller" => {
578                    let inner = ctl.children().find(|c| c.is_element() && matches!(c.tag_name().name(), "skin" | "morph"))?;
579                    if inner.tag_name().name() == "skin"
580                        && let Some(b) = child(inner, "bind_shape_matrix")
581                    {
582                        bind = math::mul(&bind, &row_major(&floats(b)));
583                    }
584                    ctl = *self.ids.get(url_id(inner.attribute("source")?))?;
585                }
586                _ => return None,
587            }
588        }
589        None
590    }
591
592    fn geometry_prims(&mut self, g: Node<'a, 'i>) -> Rc<Vec<Prim>> {
593        let key = g.attribute("id").map(str::to_string).unwrap_or_else(|| format!("@{}", g.range().start));
594        if let Some(p) = self.prims.get(&key) {
595            return p.clone();
596        }
597        let prims = Rc::new(child(g, "mesh").map(|m| read_mesh(m, &self.ids)).unwrap_or_default());
598        self.prims.insert(key, prims.clone());
599        prims
600    }
601
602    fn emit(&mut self, g: Node<'a, 'i>, m: &M4, bind: Option<Node<'a, 'i>>, node_name: &str) -> Result<()> {
603        let prims = self.geometry_prims(g);
604        let name = if node_name.is_empty() { g.attribute("name").or(g.attribute("id")).unwrap_or("") } else { node_name };
605        let nm = normal_matrix(m);
606        let flip = det3(m) < 0.0;
607        for prim in prims.iter() {
608            self.triangles += prim.indices.len() / 3;
609            if self.triangles > MAX_TRIANGLES {
610                return Err(ParseError::new("the COLLADA file has too many triangles"));
611            }
612            let resolved = prim.material.as_deref().and_then(|sym| self.bound_material(bind, sym));
613            let (material, texture, extra) = match resolved {
614                Some((m, t, e)) => (Some(m), t, Some(e)),
615                None => (None, None, None),
616            };
617            // The UV set the texture's `texcoord` is bound to, else the first;
618            // the normal map's own name chooses when there is no texture.
619            let tc = texture.as_ref().map(|(_, tc)| tc.as_str()).or(extra.as_ref().and_then(|e| e.normal.as_ref()).map(|(_, tc)| tc.as_str()));
620            let want = tc.and_then(|tc| bound_set(bind, prim.material.as_deref().unwrap_or(""), tc));
621            let uvs = prim.uvs.iter().find(|(set, _)| want.is_some() && *set == want).or(prim.uvs.first()).map(|(_, v)| v.clone());
622            let mut indices = prim.indices.clone();
623            if flip {
624                for t in indices.as_chunks_mut::<3>().0 {
625                    t.swap(1, 2);
626                }
627            }
628            self.meshes.push(Mesh {
629                name: name.to_string(),
630                positions: prim.positions.iter().map(|&p| math::transform_point(m, p)).collect(),
631                normals: prim.normals.as_ref().map(|ns| ns.iter().map(|&n| apply3(&nm, n)).collect()),
632                colors: prim.colors.clone(),
633                indices,
634                material,
635                texture: texture.filter(|_| uvs.is_some()).map(|(t, _)| t),
636                surface: match extra {
637                    Some(e) => Surface { normal_map: e.normal.map(|(t, _)| t).filter(|_| uvs.is_some()), blend: e.blend, ..Default::default() },
638                    None => Surface::default(),
639                },
640                uvs,
641            });
642        }
643        Ok(())
644    }
645
646    /// The material an instance binds to the primitive symbol `sym`; files
647    /// without a binding often name the material itself.
648    fn bound_material(&mut self, bind: Option<Node<'a, 'i>>, sym: &str) -> Option<Resolved> {
649        let target = bind
650            .and_then(|b| child(b, "technique_common"))
651            .and_then(|t| children(t, "instance_material").find(|im| im.attribute("symbol") == Some(sym)))
652            .and_then(|im| im.attribute("target"))
653            .map(url_id)
654            .unwrap_or(sym)
655            .to_string();
656        if let Some(m) = self.materials.get(&target) {
657            return m.clone();
658        }
659        let m = self.material(&target);
660        self.materials.insert(target, m.clone());
661        m
662    }
663
664    fn material(&mut self, id: &str) -> Option<Resolved> {
665        let mat = self.ids.get(id).copied().filter(|n| n.tag_name().name() == "material")?;
666        let name = mat.attribute("name").unwrap_or(id).to_string();
667        let effect = child(mat, "instance_effect").and_then(|i| i.attribute("url")).and_then(|u| self.ids.get(url_id(u)).copied());
668        let (color, texture) = effect.map(|e| self.effect_color(e)).unwrap_or((None, None));
669        let (extra, opacity) = effect.map(|e| self.effect_extra(e)).unwrap_or((Extra { normal: None, blend: false }, 1.0));
670        Some((Material { name, color: color.unwrap_or(DEFAULT_COLOR), opacity }, texture, extra))
671    }
672
673    /// The effect's normal map (a `bump` texture), its opacity and whether
674    /// it blends.
675    fn effect_extra(&mut self, effect: Node<'a, 'i>) -> (Extra, f32) {
676        let mut extra = Extra { normal: None, blend: false };
677        let Some(profile) = child(effect, "profile_COMMON") else { return (extra, 1.0) };
678        let technique = child(profile, "technique");
679        let shader = technique.and_then(shader_of);
680        // `extra` techniques first, then the shader's own child.
681        let bump = [technique, Some(profile), Some(effect)]
682            .into_iter()
683            .flatten()
684            .flat_map(|h| children(h, "extra"))
685            .flat_map(|e| children(e, "technique"))
686            .chain(shader)
687            .flat_map(|t| children(t, "bump"))
688            .find_map(|b| child(b, "texture"));
689        if let Some(tex) = bump {
690            let name = tex.attribute("texture").unwrap_or("");
691            if let Some(source) = self.texture_source(profile, name) {
692                let image = self.loader.for_map(|l| match source {
693                    Source::Uri(uri) => l.file(&percent_decode(uri.trim())),
694                    Source::Hex(bytes) => l.embedded("embedded", &bytes),
695                });
696                if let Some(image) = image {
697                    let texture = Texture { image, tint: [1.0; 3], alpha_cutoff: None };
698                    extra.normal = Some((texture, tex.attribute("texcoord").unwrap_or("").to_string()));
699                }
700            }
701        }
702        let Some(shader) = shader else { return (extra, 1.0) };
703        let transparent = child(shader, "transparent");
704        let transparency = child(shader, "transparency");
705        if transparent.is_none() && transparency.is_none() {
706            return (extra, 1.0);
707        }
708        if transparent.is_some_and(|t| child(t, "texture").is_some()) {
709            extra.blend = true;
710            return (extra, 1.0);
711        }
712        let factor = transparency.and_then(|t| child(t, "float")).and_then(|f| f.text()).and_then(|t| t.trim().parse::<f32>().ok()).unwrap_or(1.0);
713        let mut c = [1.0f32; 4];
714        if let Some(v) = transparent.and_then(|t| child(t, "color")).map(floats) {
715            for (slot, x) in c.iter_mut().zip(v) {
716                *slot = x;
717            }
718        }
719        let mode = transparent.and_then(|t| t.attribute("opaque")).map(str::trim).unwrap_or("A_ONE");
720        let opacity = match mode {
721            "A_ONE" => c[3] * factor,
722            "RGB_ZERO" => 1.0 - c[0] * factor,
723            "A_ZERO" => 1.0 - c[3] * factor,
724            "RGB_ONE" => c[0] * factor,
725            _ => 1.0,
726        };
727        let opacity = if opacity.is_finite() { opacity.clamp(0.0, 1.0) } else { 1.0 };
728        extra.blend = opacity < 1.0;
729        (extra, opacity)
730    }
731
732    /// The effect's colour (the diffuse's, the emission's for `constant`,
733    /// the picture's average for a texture) and the texture to draw.
734    fn effect_color(&mut self, effect: Node<'a, 'i>) -> (Option<V3>, Option<(Texture, String)>) {
735        let Some(profile) = child(effect, "profile_COMMON") else { return (None, None) };
736        let Some(technique) = child(profile, "technique") else { return (None, None) };
737        let Some(shader) = shader_of(technique) else { return (None, None) };
738        let slot = if shader.tag_name().name() == "constant" { "emission" } else { "diffuse" };
739        let Some(value) = child(shader, slot) else { return (None, None) };
740        if let Some(c) = child(value, "color") {
741            let v = floats(c);
742            return ((v.len() >= 3).then(|| [v[0], v[1], v[2]].map(|x| x.clamp(0.0, 1.0))), None);
743        }
744        let Some(tex) = child(value, "texture") else { return (None, None) };
745        let Some(name) = tex.attribute("texture") else { return (None, None) };
746        let Some(source) = self.texture_source(profile, name) else { return (None, None) };
747        let image = match source {
748            Source::Uri(uri) => self.loader.file(&percent_decode(uri.trim())),
749            Source::Hex(bytes) => self.loader.embedded("embedded", &bytes),
750        };
751        let Some(image) = image else { return (None, None) };
752        let color = image.average();
753        let texture = Texture { image, tint: [1.0; 3], alpha_cutoff: None };
754        (color, Some((texture, tex.attribute("texcoord").unwrap_or("").to_string())))
755    }
756
757    /// The image a `texture` attribute names: through the profile's sampler and surface
758    /// params (1.4.1), the sampler's `instance_image` (1.5), or an image
759    /// id used directly (wrapping around).
760    fn texture_source(&self, profile: Node<'a, 'i>, tex: &str) -> Option<Source> {
761        let param = |sid: &str| children(profile, "newparam").find(|p| p.attribute("sid") == Some(sid));
762        let mut image = None;
763        if let Some(sampler) = param(tex).and_then(|p| child(p, "sampler2D")) {
764            if let Some(url) = child(sampler, "instance_image").and_then(|i| i.attribute("url")) {
765                image = self.ids.get(url_id(url)).copied();
766            } else if let Some(surface_sid) = child(sampler, "source").and_then(|s| s.text()) {
767                let init = param(surface_sid.trim()).and_then(|p| child(p, "surface")).and_then(|s| child(s, "init_from")).and_then(|i| i.text());
768                image = init.and_then(|id| self.ids.get(id.trim()).copied());
769            }
770        }
771        let image = image.or_else(|| self.ids.get(tex).copied()).filter(|n| n.tag_name().name() == "image")?;
772        let init = child(image, "init_from")?;
773        if let Some(hex) = child(init, "hex") {
774            return Some(Source::Hex(hex_bytes(hex.text().unwrap_or(""))?));
775        }
776        // 1.5 wraps the path in `ref`.
777        let text = child(init, "ref").and_then(|r| r.text()).or(init.text())?;
778        Some(Source::Uri(text.trim().to_string())).filter(|s| matches!(s, Source::Uri(u) if !u.is_empty()))
779    }
780}
781
782/// The shading model element of a profile's technique.
783fn shader_of<'a, 'i>(technique: Node<'a, 'i>) -> Option<Node<'a, 'i>> {
784    technique.children().find(|c| c.is_element() && matches!(c.tag_name().name(), "phong" | "lambert" | "blinn" | "constant"))
785}
786
787/// Where a texture's picture is.
788enum Source {
789    Uri(String),
790    Hex(Vec<u8>),
791}
792
793/// The bytes of a `hex` element (whitespace between digits allowed).
794fn hex_bytes(s: &str) -> Option<Vec<u8>> {
795    let digits: Vec<u8> = s.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
796    if !digits.len().is_multiple_of(2) {
797        return None;
798    }
799    digits.chunks(2).map(|p| u8::from_str_radix(std::str::from_utf8(p).ok()?, 16).ok()).collect()
800}
801
802/// The `input_set` the instance's `bind_vertex_input` gives the `texcoord`
803/// name for the material symbol `sym`.
804fn bound_set(bind: Option<Node>, sym: &str, texcoord: &str) -> Option<u32> {
805    let im = children(child(bind?, "technique_common")?, "instance_material").find(|im| im.attribute("symbol") == Some(sym))?;
806    let b = children(im, "bind_vertex_input").find(|b| b.attribute("semantic") == Some(texcoord) && b.attribute("input_semantic") == Some("TEXCOORD"))?;
807    b.attribute("input_set").and_then(|s| s.trim().parse().ok())
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813    use crate::model3d::texture;
814
815    fn none(_: &str) -> Option<Vec<u8>> {
816        None
817    }
818
819    const CUBE_POS: &str = "-1 -1 -1 1 -1 -1 1 1 -1 -1 1 -1 -1 -1 1 1 -1 1 1 1 1 -1 1 1";
820    /// Six outward quads over the corners above.
821    const QUADS: [[u32; 4]; 6] = [[0, 3, 2, 1], [4, 5, 6, 7], [0, 1, 5, 4], [2, 3, 7, 6], [1, 2, 6, 5], [0, 4, 7, 3]];
822    const NORMALS: [V3; 6] = [[0.0, 0.0, -1.0], [0.0, 0.0, 1.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
823
824    /// A cube document: `prim` is the primitive element, `node` the inner
825    /// part of the scene node, `asset` the asset's contents.
826    fn doc(asset: &str, prim: &str, node: &str, extra: &str) -> String {
827        let normals: Vec<String> = NORMALS.iter().map(|n| format!("{} {} {}", n[0], n[1], n[2])).collect();
828        format!(
829            r##"<?xml version="1.0" encoding="utf-8"?>
830<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
831  <asset>{asset}</asset>
832  <library_effects>
833    <effect id="red-fx"><profile_COMMON><technique sid="common"><phong>
834      <diffuse><color>1 0 0 1</color></diffuse></phong></technique></profile_COMMON></effect>
835    <effect id="tex-fx"><profile_COMMON>
836      <newparam sid="surf"><surface type="2D"><init_from>green-img</init_from></surface></newparam>
837      <newparam sid="samp"><sampler2D><source>surf</source></sampler2D></newparam>
838      <technique sid="common"><lambert><diffuse><texture texture="samp" texcoord="UV"/></diffuse></lambert></technique>
839    </profile_COMMON></effect>
840  </library_effects>
841  <library_images><image id="green-img"><init_from>textures/green%20tex.png</init_from></image></library_images>
842  <library_materials>
843    <material id="red-mat" name="Red"><instance_effect url="#red-fx"/></material>
844    <material id="tex-mat" name="Tex"><instance_effect url="#tex-fx"/></material>
845  </library_materials>
846  <library_geometries>
847    <geometry id="cube" name="Cube"><mesh>
848      <source id="cube-pos"><float_array id="cube-pos-arr" count="24">{CUBE_POS}</float_array>
849        <technique_common><accessor source="#cube-pos-arr" count="8" stride="3">
850          <param name="X" type="float"/><param name="Y" type="float"/><param name="Z" type="float"/></accessor></technique_common></source>
851      <source id="cube-nrm"><float_array id="cube-nrm-arr" count="18">{normals}</float_array>
852        <technique_common><accessor source="#cube-nrm-arr" count="6" stride="3">
853          <param name="X" type="float"/><param name="Y" type="float"/><param name="Z" type="float"/></accessor></technique_common></source>
854      <vertices id="cube-vtx"><input semantic="POSITION" source="#cube-pos"/></vertices>
855      {prim}
856    </mesh></geometry>
857  </library_geometries>
858  {extra}
859  <library_visual_scenes><visual_scene id="scene">
860    <node id="n" name="CubeNode">{node}</node>
861  </visual_scene></library_visual_scenes>
862  <scene><instance_visual_scene url="#scene"/></scene>
863</COLLADA>"##,
864            normals = normals.join(" ")
865        )
866    }
867
868    const BIND_RED: &str = r##"<instance_geometry url="#cube"><bind_material><technique_common><instance_material symbol="m" target="#red-mat"/></technique_common></bind_material></instance_geometry>"##;
869
870    /// A polylist of the six quads with normals at offset 1.
871    fn polylist() -> String {
872        let p: Vec<String> = QUADS.iter().enumerate().flat_map(|(f, q)| q.iter().map(move |v| format!("{v} {f}"))).collect();
873        format!(
874            r##"<polylist material="m" count="6"><input semantic="VERTEX" source="#cube-vtx" offset="0"/><input semantic="NORMAL" source="#cube-nrm" offset="1"/><vcount>4 4 4 4 4 4</vcount><p>{}</p></polylist>"##,
875            p.join(" ")
876        )
877    }
878
879    fn triangles() -> String {
880        let p: Vec<String> = QUADS.iter().flat_map(|q| [q[0], q[1], q[2], q[0], q[2], q[3]]).map(|v| v.to_string()).collect();
881        format!(r##"<triangles material="m" count="12"><input semantic="VERTEX" source="#cube-vtx" offset="0"/><p>{}</p></triangles>"##, p.join(" "))
882    }
883
884    fn polygons() -> String {
885        let ps: Vec<String> = QUADS.iter().map(|q| format!("<p>{} {} {} {}</p>", q[0], q[1], q[2], q[3])).collect();
886        format!(r##"<polygons material="m" count="6"><input semantic="VERTEX" source="#cube-vtx" offset="0"/>{}</polygons>"##, ps.concat())
887    }
888
889    fn read(s: &str) -> Model {
890        parse(s.as_bytes(), &none).unwrap()
891    }
892
893    fn close(a: V3, b: V3) -> bool {
894        (0..3).all(|k| (a[k] - b[k]).abs() < 1e-4)
895    }
896
897    /// Every triangle faces away from the model's centre.
898    fn outward(m: &Model) -> bool {
899        let (lo, hi) = m.bounds().unwrap();
900        let c = math::scale(math::add(lo, hi), 0.5);
901        m.meshes.iter().all(|mesh| {
902            mesh.indices.as_chunks::<3>().0.iter().all(|t| {
903                let [a, b, d] = t.map(|i| mesh.positions[i as usize]);
904                let n = super::super::geom::face_normal(a, b, d);
905                math::dot(n, math::sub(math::scale(math::add(math::add(a, b), d), 1.0 / 3.0), c)) > 0.0
906            })
907        })
908    }
909
910    #[test]
911    fn polylist_cube_with_normals_and_colour() {
912        let m = read(&doc("", &polylist(), BIND_RED, ""));
913        assert_eq!(m.meshes.len(), 1);
914        assert_eq!(m.triangle_count(), 12);
915        assert_eq!(m.bounds(), Some(([-1.0; 3], [1.0; 3])));
916        let mesh = &m.meshes[0];
917        assert_eq!(mesh.name, "CubeNode");
918        let mat = mesh.material.as_ref().unwrap();
919        assert_eq!((mat.name.as_str(), mat.color), ("Red", [1.0, 0.0, 0.0]));
920        // One vertex per (corner, face normal) pair: 24.
921        assert_eq!(mesh.positions.len(), 24);
922        let normals = mesh.normals.as_ref().unwrap();
923        assert!(normals.iter().all(|n| NORMALS.iter().any(|&f| close(*n, f))));
924        assert!(outward(&m));
925    }
926
927    #[test]
928    fn triangles_and_polygons_read_the_same_cube() {
929        for prim in [triangles(), polygons()] {
930            let m = read(&doc("", &prim, BIND_RED, ""));
931            assert_eq!(m.triangle_count(), 12);
932            assert_eq!(m.vertex_count(), 8);
933            assert!(m.meshes[0].normals.is_none());
934            assert_eq!(m.meshes[0].material.as_ref().unwrap().color, [1.0, 0.0, 0.0]);
935            assert!(outward(&m));
936        }
937    }
938
939    #[test]
940    fn strips_and_fans() {
941        let strip = r##"<tristrips material="m" count="1"><input semantic="VERTEX" source="#cube-vtx" offset="0"/><p>0 1 3 2</p></tristrips>"##;
942        let fan = r##"<trifans material="m" count="1"><input semantic="VERTEX" source="#cube-vtx" offset="0"/><p>0 3 2 1</p></trifans>"##;
943        for prim in [strip, fan] {
944            let m = read(&doc("", prim, BIND_RED, ""));
945            assert_eq!(m.triangle_count(), 2);
946            let mesh = &m.meshes[0];
947            for t in mesh.indices.as_chunks::<3>().0 {
948                let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
949                let n = super::super::geom::face_normal(a, b, c);
950                assert_eq!(n[2].signum(), if prim == strip { 1.0 } else { -1.0 }, "{prim}");
951            }
952        }
953    }
954
955    #[test]
956    fn node_transforms_compose() {
957        // translate then scale: points are scaled first, then moved.
958        let node = format!("<translate>10 0 0</translate><scale>2 2 2</scale>{BIND_RED}");
959        let m = read(&doc("", &triangles(), &node, ""));
960        assert_eq!(m.bounds(), Some(([8.0, -2.0, -2.0], [12.0, 2.0, 2.0])));
961        // The same as a row-major matrix.
962        let node = format!("<matrix>2 0 0 10 0 2 0 0 0 0 2 0 0 0 0 1</matrix>{BIND_RED}");
963        assert_eq!(read(&doc("", &triangles(), &node, "")).bounds(), Some(([8.0, -2.0, -2.0], [12.0, 2.0, 2.0])));
964        // Rotating 90° about Z carries a stretched X onto Y.
965        let node = format!("<rotate>0 0 1 90</rotate><scale>3 1 1</scale>{BIND_RED}");
966        let (lo, hi) = read(&doc("", &triangles(), &node, "")).bounds().unwrap();
967        assert!(close(lo, [-1.0, -3.0, -1.0]) && close(hi, [1.0, 3.0, 1.0]), "{lo:?} {hi:?}");
968        // Normals turn with the node.
969        let node = format!("<rotate>0 0 1 90</rotate>{BIND_RED}");
970        let m = read(&doc("", &polylist(), &node, ""));
971        assert!(outward(&m));
972        let mesh = &m.meshes[0];
973        for t in mesh.indices.as_chunks::<3>().0 {
974            let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
975            let f = super::super::geom::face_normal(a, b, c);
976            assert!(t.iter().all(|&i| close(mesh.normals.as_ref().unwrap()[i as usize], f)));
977        }
978    }
979
980    #[test]
981    fn nested_and_instanced_nodes() {
982        let lib = format!(r##"<library_nodes><node id="part"><translate>0 5 0</translate>{BIND_RED}</node></library_nodes>"##);
983        let node = r##"<translate>10 0 0</translate><node><instance_node url="#part"/></node><instance_node url="#part"/>"##;
984        let m = read(&doc("", &triangles(), node, &lib));
985        assert_eq!(m.meshes.len(), 2);
986        assert_eq!(m.bounds(), Some(([9.0, 4.0, -1.0], [11.0, 6.0, 1.0])));
987    }
988
989    #[test]
990    fn mirrored_instances_keep_faces_outward() {
991        let node = format!("<scale>-1 1 1</scale>{BIND_RED}");
992        let m = read(&doc("", &polylist(), &node, ""));
993        assert!(outward(&m));
994        assert!(m.meshes[0].normals.as_ref().unwrap().iter().all(|n| NORMALS.iter().any(|&f| close(*n, f))));
995    }
996
997    #[test]
998    fn z_up_and_units_become_y_up_metres() {
999        let node = format!("<translate>0 0 10</translate>{BIND_RED}");
1000        let asset = r#"<unit name="centimeter" meter="0.01"/><up_axis>Z_UP</up_axis>"#;
1001        let (lo, hi) = read(&doc(asset, &triangles(), &node, "")).bounds().unwrap();
1002        // File z (up) 9..11 cm becomes y 0.09..0.11 m.
1003        assert!(close(lo, [-0.01, 0.09, -0.01]) && close(hi, [0.01, 0.11, 0.01]), "{lo:?} {hi:?}");
1004        let (lo, hi) = read(&doc("<up_axis>X_UP</up_axis>", &triangles(), &format!("<translate>10 0 0</translate>{BIND_RED}"), "")).bounds().unwrap();
1005        assert!(close(lo, [-1.0, 9.0, -1.0]) && close(hi, [1.0, 11.0, 1.0]), "{lo:?} {hi:?}");
1006        // Converting keeps faces outward and normals unit length.
1007        let m = read(&doc(asset, &polylist(), BIND_RED, ""));
1008        assert!(outward(&m));
1009        assert!(m.meshes[0].normals.as_ref().unwrap().iter().all(|n| (math::length(*n) - 1.0).abs() < 1e-4));
1010    }
1011
1012    #[test]
1013    fn diffuse_texture_gives_its_average_colour() {
1014        let mut png = Vec::new();
1015        image::RgbImage::from_pixel(4, 4, image::Rgb([0, 255, 0])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
1016        let side = |name: &str| (name == "textures/green tex.png").then(|| png.clone());
1017        let node = r##"<instance_geometry url="#cube"><bind_material><technique_common><instance_material symbol="m" target="#tex-mat"/></technique_common></bind_material></instance_geometry>"##;
1018        let m = parse(doc("", &triangles(), node, "").as_bytes(), &side).unwrap();
1019        let mat = m.meshes[0].material.as_ref().unwrap();
1020        assert_eq!(mat.name, "Tex");
1021        assert!(close(mat.color, [0.0, 1.0, 0.0]));
1022        // Without the image the default colour stands in.
1023        let m = read(&doc("", &triangles(), node, ""));
1024        assert_eq!(m.meshes[0].material.as_ref().unwrap().color, DEFAULT_COLOR);
1025        // 1.5's instance_image and init_from/ref, found by file name alone.
1026        let side = |name: &str| (name == "g.png").then(|| png.clone());
1027        let extra = r##"<library_images><image id="img15"><init_from><ref>file:///C:/work/g.png</ref></init_from></image></library_images>
1028            <library_effects><effect id="fx15"><profile_COMMON><newparam sid="s"><sampler2D><instance_image url="#img15"/></sampler2D></newparam>
1029            <technique sid="t"><blinn><diffuse><texture texture="s" texcoord="UV"/></diffuse></blinn></technique></profile_COMMON></effect></library_effects>
1030            <library_materials><material id="m15"><instance_effect url="#fx15"/></material></library_materials>"##;
1031        let node = r##"<instance_geometry url="#cube"><bind_material><technique_common><instance_material symbol="m" target="#m15"/></technique_common></bind_material></instance_geometry>"##;
1032        let m = parse(doc("", &triangles(), node, extra).as_bytes(), &side).unwrap();
1033        assert!(close(m.meshes[0].material.as_ref().unwrap().color, [0.0, 1.0, 0.0]));
1034    }
1035
1036    /// A document with a UV source (three corners) and a triangle using it
1037    /// at offset 1, and `effects` and `images` added to the libraries.
1038    fn textured(effect: &str, images: &str, bind: &str) -> String {
1039        let uv = r##"<source id="cube-uv"><float_array id="cube-uv-arr" count="6">0 0 1 0 1 0.25</float_array>
1040            <technique_common><accessor source="#cube-uv-arr" count="3" stride="2"><param name="S" type="float"/><param name="T" type="float"/></accessor></technique_common></source>
1041            <vertices id="cube-vtx">"##;
1042        let prim = r##"<triangles material="m" count="1"><input semantic="VERTEX" source="#cube-vtx" offset="0"/><input semantic="TEXCOORD" source="#cube-uv" offset="1" set="0"/><p>0 0 1 1 2 2</p></triangles>"##;
1043        let extra = format!(r##"{images}<library_effects>{effect}</library_effects><library_materials><material id="m141"><instance_effect url="#fx"/></material></library_materials>"##);
1044        let node = format!(r##"<instance_geometry url="#cube"><bind_material><technique_common><instance_material symbol="m" target="#m141">{bind}</instance_material></technique_common></bind_material></instance_geometry>"##);
1045        doc("", prim, &node, &extra).replace(r#"<vertices id="cube-vtx">"#, uv)
1046    }
1047
1048    const SAMPLER_141: &str = r##"<effect id="fx"><profile_COMMON>
1049        <newparam sid="surf"><surface type="2D"><init_from>img</init_from></surface></newparam>
1050        <newparam sid="samp"><sampler2D><source>surf</source><wrap_s>CLAMP</wrap_s><wrap_t>MIRROR</wrap_t></sampler2D></newparam>
1051        <technique sid="common"><lambert><diffuse><texture texture="samp" texcoord="UVSET0"/></diffuse></lambert></technique></profile_COMMON></effect>"##;
1052
1053    #[test]
1054    fn texture_coordinates_are_read() {
1055        let png = texture::tests::png_2x2();
1056        let side = |name: &str| (name == "pics/a.png").then(|| png.clone());
1057        let images = r#"<library_images><image id="img"><init_from>pics/a.png</init_from></image></library_images>"#;
1058        let m = parse(textured(SAMPLER_141, images, "").as_bytes(), &side).unwrap();
1059        let mesh = &m.meshes[0];
1060        // v is flipped into picture space.
1061        assert_eq!(mesh.uvs.as_deref(), Some(&[[0.0, 1.0], [1.0, 1.0], [1.0, 0.75]][..]));
1062        let t = mesh.texture.as_ref().unwrap();
1063        assert_eq!((t.image.width, t.image.height), (2, 2));
1064        assert_eq!((t.tint, t.alpha_cutoff), ([1.0; 3], None));
1065        assert!(mesh.textured().is_some());
1066        // The colour is the picture's average, as before: red, green, blue, white.
1067        assert!(close(mesh.material.as_ref().unwrap().color, [0.5, 0.5, 0.5]));
1068    }
1069
1070    #[test]
1071    fn bound_texcoord_set_is_chosen() {
1072        let png = texture::tests::png_2x2();
1073        let side = |name: &str| (name == "a.png").then(|| png.clone());
1074        let images = r#"<library_images><image id="img"><init_from>a.png</init_from></image></library_images>"#;
1075        let s = textured(SAMPLER_141, images, r#"<bind_vertex_input semantic="UVSET0" input_semantic="TEXCOORD" input_set="1"/>"#);
1076        // A second set, 1, with other values, in front of set 0.
1077        let s = s.replace(
1078            r##"<input semantic="TEXCOORD" source="#cube-uv" offset="1" set="0"/>"##,
1079            r##"<input semantic="TEXCOORD" source="#cube-uv" offset="1" set="0"/><input semantic="TEXCOORD" source="#cube-uv2" offset="2" set="1"/>"##,
1080        )
1081        .replace("<p>0 0 1 1 2 2</p>", "<p>0 0 2 1 1 1 2 2 0</p>")
1082        .replace(
1083            r#"<vertices id="cube-vtx">"#,
1084            r##"<source id="cube-uv2"><float_array id="cube-uv2-arr" count="6">0.5 0.5 0 1 1 1</float_array>
1085            <technique_common><accessor source="#cube-uv2-arr" count="3" stride="2"><param name="S" type="float"/><param name="T" type="float"/></accessor></technique_common></source>
1086            <vertices id="cube-vtx">"##,
1087        );
1088        let m = parse(s.as_bytes(), &side).unwrap();
1089        // Set 1 through offset 2: indices 2, 1, 0 of its source.
1090        assert_eq!(m.meshes[0].uvs.as_deref(), Some(&[[1.0, 0.0], [0.0, 0.0], [0.5, 0.5]][..]));
1091    }
1092
1093    #[test]
1094    fn collada_15_instance_image_and_embedded_hex() {
1095        let png = texture::tests::png_2x2();
1096        let side = |name: &str| (name == "g.png").then(|| png.clone());
1097        let fx = r##"<effect id="fx"><profile_COMMON><newparam sid="s"><sampler2D><instance_image url="#img"/><wrap_s>WRAP</wrap_s><wrap_t>MIRROR_ONCE</wrap_t></sampler2D></newparam>
1098            <technique sid="t"><blinn><diffuse><texture texture="s" texcoord="UV"/></diffuse></blinn></technique></profile_COMMON></effect>"##;
1099        let images = r#"<library_images><image id="img"><init_from><ref>file:///C:/work/g.png</ref></init_from></image></library_images>"#;
1100        let m = parse(textured(fx, images, "").as_bytes(), &side).unwrap();
1101        let t = m.meshes[0].texture.as_ref().unwrap();
1102        assert_eq!(t.image.width, 2);
1103        // The same picture inline.
1104        let hex: String = png.iter().map(|b| format!("{b:02x}")).collect();
1105        let images = format!(r#"<library_images><image id="img"><init_from><hex format="PNG">{hex}</hex></init_from></image></library_images>"#);
1106        let m = parse(textured(fx, &images, "").as_bytes(), &none).unwrap();
1107        assert_eq!(m.meshes[0].texture.as_ref().unwrap().image.height, 2);
1108        assert_ne!(m.meshes[0].material.as_ref().unwrap().color, DEFAULT_COLOR);
1109    }
1110
1111    const IMG: &str = r#"<library_images><image id="img"><init_from>a.png</init_from></image></library_images>"#;
1112
1113    /// An effect with a sampler `samp` for `a.png` and `shader` as its shader.
1114    fn fx_with(shader: &str, extra: &str) -> String {
1115        format!(
1116            r##"<effect id="fx"><profile_COMMON><newparam sid="surf"><surface type="2D"><init_from>img</init_from></surface></newparam>
1117            <newparam sid="samp"><sampler2D><source>surf</source></sampler2D></newparam>
1118            <technique sid="common">{shader}{extra}</technique></profile_COMMON></effect>"##
1119        )
1120    }
1121
1122    fn opacity_of(shader: &str) -> (f32, bool) {
1123        let m = read(&textured(&fx_with(shader, ""), "", ""));
1124        (m.meshes[0].material.as_ref().unwrap().opacity, m.meshes[0].surface.blend)
1125    }
1126
1127    #[test]
1128    fn transparency_modes() {
1129        let near = |a: f32, b: f32| (a - b).abs() < 1e-5;
1130        let sh = |opaque: &str, c: &str, f: &str| {
1131            let op = if opaque.is_empty() { String::new() } else { format!(r#" opaque="{opaque}""#) };
1132            format!("<phong><transparent{op}><color>{c}</color></transparent><transparency><float>{f}</float></transparency></phong>")
1133        };
1134        let (o, b) = opacity_of(&sh("A_ONE", "1 1 1 0.8", "0.5"));
1135        assert!(near(o, 0.4) && b);
1136        let (o, b) = opacity_of(&sh("RGB_ZERO", "0.5 0 0 1", "0.5"));
1137        assert!(near(o, 0.75) && b);
1138        let (o, b) = opacity_of(&sh("A_ZERO", "0 0 0 0.5", "1"));
1139        assert!(near(o, 0.5) && b);
1140        let (o, b) = opacity_of(&sh("RGB_ONE", "0.25 0 0 1", "2"));
1141        assert!(near(o, 0.5) && b);
1142        // No attribute: A_ONE. Unknown mode: opaque. Opaque result: no blend.
1143        let (o, b) = opacity_of(&sh("", "1 1 1 0.25", "1"));
1144        assert!(near(o, 0.25) && b);
1145        assert_eq!(opacity_of(&sh("SOMETHING", "1 1 1 0.25", "1")), (1.0, false));
1146        assert_eq!(opacity_of(&sh("A_ONE", "1 1 1 1", "1")), (1.0, false));
1147        // Clamped; fewer than four numbers count as ones.
1148        assert_eq!(opacity_of(&sh("RGB_ONE", "1 1 1 1", "5")), (1.0, false));
1149        assert_eq!(opacity_of(&sh("RGB_ZERO", "1 1 1 1", "5")), (0.0, true));
1150        let (o, b) = opacity_of(&sh("A_ONE", "1 1", "0.5"));
1151        assert!(near(o, 0.5) && b);
1152    }
1153
1154    #[test]
1155    fn transparency_defaults_and_garbage() {
1156        // Neither element.
1157        assert_eq!(opacity_of("<phong><diffuse><color>1 0 0 1</color></diffuse></phong>"), (1.0, false));
1158        // `transparent` alone: factor 1.
1159        let (o, b) = opacity_of(r#"<phong><transparent opaque="A_ZERO"><color>0 0 0 0.25</color></transparent></phong>"#);
1160        assert!((o - 0.75).abs() < 1e-5 && b);
1161        // `transparency` alone: A_ONE with white, so the factor is the opacity.
1162        let (o, b) = opacity_of("<phong><transparency><float>0.3</float></transparency></phong>");
1163        assert!((o - 0.3).abs() < 1e-5 && b);
1164        // Garbage and non-finite factors.
1165        assert_eq!(opacity_of("<phong><transparency><float>abc</float></transparency></phong>"), (1.0, false));
1166        assert_eq!(opacity_of("<phong><transparency><float>NaN</float></transparency></phong>"), (1.0, false));
1167        assert_eq!(opacity_of("<phong><transparency><float>inf</float></transparency></phong>"), (1.0, false));
1168        assert_eq!(opacity_of(r#"<phong><transparent opaque="A_ONE"><color>x y z w</color></transparent></phong>"#), (0.0, true));
1169    }
1170
1171    #[test]
1172    fn transparent_texture_blends_at_full_opacity() {
1173        let sh = r#"<phong><transparent opaque="A_ONE"><texture texture="samp" texcoord="UV"/></transparent><transparency><float>0.5</float></transparency></phong>"#;
1174        assert_eq!(opacity_of(sh), (1.0, true));
1175    }
1176
1177    #[test]
1178    fn bump_is_a_normal_map() {
1179        let png = texture::tests::png_2x2();
1180        let side = |name: &str| (name == "a.png").then(|| png.clone());
1181        let direct = r#"<phong><bump><texture texture="samp" texcoord="UV"/></bump></phong>"#;
1182        let m = parse(textured(&fx_with(direct, ""), IMG, "").as_bytes(), &side).unwrap();
1183        let s = &m.meshes[0].surface;
1184        assert_eq!(s.normal_map.as_ref().map(|t| t.image.width), Some(2));
1185        assert!(s.bump_map.is_none() && s.emissive_map.is_none() && s.emissive == [0.0; 3]);
1186        // In an extra technique of the shader's technique, the profile or the effect.
1187        let ex = r#"<extra><technique profile="X"><bump><texture texture="samp" texcoord="UV"/></bump></technique></extra>"#;
1188        let plain = r#"<phong><diffuse><color>1 0 0 1</color></diffuse></phong>"#;
1189        for fx in [fx_with(plain, ex), fx_with(plain, "").replace("</technique></profile_COMMON>", &format!("</technique>{ex}</profile_COMMON>")), fx_with(plain, "").replace("</profile_COMMON>", &format!("</profile_COMMON>{ex}"))] {
1190            let m = parse(textured(&fx, IMG, "").as_bytes(), &side).unwrap();
1191            assert!(m.meshes[0].surface.normal_map.is_some(), "{fx}");
1192            assert!(m.meshes[0].texture.is_none());
1193        }
1194        // The extra one wins when both exist (here only it has a picture).
1195        let both = r#"<phong><bump><texture texture="none" texcoord="UV"/></bump></phong>"#;
1196        let m = parse(textured(&fx_with(both, ex), IMG, "").as_bytes(), &side).unwrap();
1197        assert!(m.meshes[0].surface.normal_map.is_some());
1198    }
1199
1200    #[test]
1201    fn broken_bump_leaves_the_mesh() {
1202        let direct = r#"<phong><bump><texture texture="samp" texcoord="UV"/></bump></phong>"#;
1203        // Picture missing.
1204        let m = read(&textured(&fx_with(direct, ""), IMG, ""));
1205        assert_eq!(m.meshes.len(), 1);
1206        assert!(m.meshes[0].surface.normal_map.is_none());
1207        // Unknown sampler, and undecodable hex.
1208        let m = read(&textured(&fx_with(&direct.replace("samp", "nope"), ""), IMG, ""));
1209        assert!(m.meshes[0].surface.normal_map.is_none());
1210        let images = r#"<library_images><image id="img"><init_from><hex>zz</hex></init_from></image></library_images>"#;
1211        let m = read(&textured(&fx_with(direct, ""), images, ""));
1212        assert!(m.meshes[0].surface.normal_map.is_none() && !m.meshes[0].positions.is_empty());
1213    }
1214
1215    #[test]
1216    fn normal_map_needs_uvs() {
1217        let png = texture::tests::png_2x2();
1218        let side = |name: &str| (name == "a.png").then(|| png.clone());
1219        let direct = r#"<phong><bump><texture texture="samp" texcoord="UV"/></bump></phong>"#;
1220        let s = textured(&fx_with(direct, ""), IMG, "").replace(r##"<input semantic="TEXCOORD" source="#cube-uv" offset="1" set="0"/>"##, "").replace("<p>0 0 1 1 2 2</p>", "<p>0 1 2</p>");
1221        let m = parse(s.as_bytes(), &side).unwrap();
1222        assert!(m.meshes[0].uvs.is_none());
1223        assert!(m.meshes[0].surface.normal_map.is_none());
1224    }
1225
1226    #[test]
1227    fn missing_image_keeps_the_fallback_colour() {
1228        let images = r#"<library_images><image id="img"><init_from>nope.png</init_from></image></library_images>"#;
1229        let m = read(&textured(SAMPLER_141, images, ""));
1230        let mesh = &m.meshes[0];
1231        assert!(mesh.texture.is_none());
1232        assert_eq!(mesh.material.as_ref().unwrap().color, DEFAULT_COLOR);
1233        // The coordinates are still read.
1234        assert!(mesh.uvs.is_some());
1235    }
1236
1237    #[test]
1238    fn images_above_the_model_folder_are_not_read() {
1239        let dir = tempfile::tempdir().unwrap();
1240        std::fs::create_dir(dir.path().join("sub")).unwrap();
1241        std::fs::write(dir.path().join("x.png"), texture::tests::png_2x2()).unwrap();
1242        let images = r#"<library_images><image id="img"><init_from>../x.png</init_from></image></library_images>"#;
1243        let path = dir.path().join("sub").join("m.dae");
1244        std::fs::write(&path, textured(SAMPLER_141, images, "")).unwrap();
1245        let m = super::super::load(&path, "dae").unwrap();
1246        assert!(m.meshes[0].texture.is_none());
1247        // Only the file name next to the model is asked for.
1248        let asked = std::cell::RefCell::new(Vec::new());
1249        let side = |name: &str| {
1250            asked.borrow_mut().push(name.to_string());
1251            None
1252        };
1253        parse(textured(SAMPLER_141, images, "").as_bytes(), &side).unwrap();
1254        assert_eq!(*asked.borrow(), ["x.png"]);
1255        // The same picture inside the folder is found.
1256        std::fs::write(dir.path().join("sub").join("x.png"), texture::tests::png_2x2()).unwrap();
1257        assert!(super::super::load(&path, "dae").unwrap().meshes[0].texture.is_some());
1258    }
1259
1260    #[test]
1261    fn controllers_show_the_bind_pose() {
1262        let extra = r##"<library_controllers><controller id="skin"><skin source="#cube">
1263            <bind_shape_matrix>1 0 0 0 0 1 0 3 0 0 1 0 0 0 0 1</bind_shape_matrix></skin></controller></library_controllers>"##;
1264        let node = r##"<instance_controller url="#skin"><bind_material><technique_common><instance_material symbol="m" target="#red-mat"/></technique_common></bind_material></instance_controller>"##;
1265        let m = read(&doc("", &triangles(), node, extra));
1266        assert_eq!(m.bounds(), Some(([-1.0, 2.0, -1.0], [1.0, 4.0, 1.0])));
1267        assert_eq!(m.meshes[0].material.as_ref().unwrap().color, [1.0, 0.0, 0.0]);
1268    }
1269
1270    #[test]
1271    fn without_a_scene_every_geometry_is_shown() {
1272        let s = doc("", &triangles(), BIND_RED, "");
1273        let start = s.find("<library_visual_scenes>").unwrap();
1274        let end = s.find("</COLLADA>").unwrap();
1275        let s = format!("{}{}", &s[..start], &s[end..]);
1276        let m = read(&s);
1277        assert_eq!(m.triangle_count(), 12);
1278        // The primitive's symbol names no material, so none is applied.
1279        assert!(m.meshes[0].material.is_none());
1280    }
1281
1282    #[test]
1283    fn bad_indices_and_accessors_are_dropped() {
1284        // An index past the positions drops its triangle only.
1285        let prim = r##"<triangles material="m" count="2"><input semantic="VERTEX" source="#cube-vtx" offset="0"/><p>0 1 2 0 1 99 0 -1 2</p></triangles>"##;
1286        assert_eq!(read(&doc("", prim, BIND_RED, "")).triangle_count(), 1);
1287        // A vcount longer than the indices stops at the end.
1288        let prim = r##"<polylist material="m"><input semantic="VERTEX" source="#cube-vtx" offset="0"/><vcount>3 4000000000 3</vcount><p>0 1 2 0 2 3</p></polylist>"##;
1289        assert_eq!(read(&doc("", prim, BIND_RED, "")).triangle_count(), 1);
1290        // A huge accessor count and a zero stride do not stretch the source.
1291        let s = doc("", &triangles(), BIND_RED, "").replace(r#"count="8" stride="3""#, r#"count="4000000000" stride="0""#);
1292        assert!(parse(s.as_bytes(), &none).is_ok_and(|m| m.vertex_count() <= 24));
1293        // A huge offset is ignored.
1294        let prim = r##"<triangles material="m"><input semantic="VERTEX" source="#cube-vtx" offset="0"/><input semantic="NORMAL" source="#cube-nrm" offset="4000000000"/><p>0 1 2</p></triangles>"##;
1295        assert_eq!(read(&doc("", prim, BIND_RED, "")).triangle_count(), 1);
1296    }
1297
1298    #[test]
1299    fn instance_cycles_end() {
1300        let lib = format!(r##"<library_nodes><node id="a">{BIND_RED}<instance_node url="#b"/><instance_node url="#b"/></node><node id="b"><instance_node url="#a"/><instance_node url="#a"/></node></library_nodes>"##);
1301        let m = read(&doc("", &triangles(), r##"<instance_node url="#a"/>"##, &lib));
1302        assert!(m.meshes.len() <= MAX_VISITS);
1303    }
1304
1305    #[test]
1306    fn import_thumbnail_draws_the_red_cube() {
1307        let dir = tempfile::tempdir().unwrap();
1308        let path = dir.path().join("Cube.DAE");
1309        std::fs::write(&path, doc("<up_axis>Z_UP</up_axis>", &polylist(), BIND_RED, "")).unwrap();
1310        let img = super::super::raster::thumbnail(&path, "DAE").unwrap();
1311        assert!(img.pixels().any(|p| p[3] == 255 && p[0] > 100 && p[1] < 40 && p[2] < 40));
1312    }
1313
1314    #[test]
1315    fn truncated_and_corrupt_files_fail_cleanly() {
1316        let full = doc("", &polylist(), BIND_RED, "");
1317        let b = full.as_bytes();
1318        for n in 0..b.len() {
1319            let _ = parse(&b[..n], &none);
1320        }
1321        // Damage one byte at a time across the geometry and scene.
1322        let start = full.find("<library_geometries>").unwrap();
1323        for i in (start..b.len()).step_by(3) {
1324            let mut d = b.to_vec();
1325            d[i] = b"<>\"/0 9x"[i % 8];
1326            let _ = parse(&d, &none);
1327        }
1328        assert!(parse(b"", &none).is_err());
1329        assert!(parse(b"<html/>", &none).is_err());
1330        assert!(parse(&[0xff; 64], &none).is_err());
1331        let empty = r#"<COLLADA version="1.5.0"><library_geometries/></COLLADA>"#;
1332        assert!(parse(empty.as_bytes(), &none).is_err());
1333    }
1334}