1use 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
47const MAX_DEPTH: usize = 32;
49const MAX_VISITS: usize = 100_000;
51const MAX_TRIANGLES: usize = 50_000_000;
53const 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
71fn 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
81fn 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
90fn 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
96const 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
100fn 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
128fn 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
145fn 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
159fn 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
182fn 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
202fn 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 "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_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
217struct Prim {
221 positions: Vec<V3>,
222 normals: Option<Vec<V3>>,
223 colors: Option<Vec<V3>>,
224 uvs: Vec<(Option<u32>, Vec<[f32; 2]>)>,
226 indices: Vec<u32>,
227 material: Option<String>,
229}
230
231fn 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 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
261const MAX_UV_SETS: usize = 4;
263
264fn 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 texcoords: Vec<(Option<u32>, Vec<V3>, usize)>,
275 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 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 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
316struct 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 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 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
394fn 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 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 _ => {
443 let n = b.corners();
444 b.polygon(0, n);
445 }
446 }
447 }
448 out.push(b.prim);
449 }
450 out
451}
452
453fn 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#[derive(Clone)]
479struct Extra {
480 normal: Option<(Texture, String)>,
481 blend: bool,
482}
483
484type 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 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 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 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 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 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 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 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 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 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
782fn 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
787enum Source {
789 Uri(String),
790 Hex(Vec<u8>),
791}
792
793fn 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
802fn 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 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 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 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 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 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 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 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 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 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 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 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 let m = read(&doc("", &triangles(), node, ""));
1024 assert_eq!(m.meshes[0].material.as_ref().unwrap().color, DEFAULT_COLOR);
1025 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 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 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 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 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 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 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 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 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 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 assert_eq!(opacity_of("<phong><diffuse><color>1 0 0 1</color></diffuse></phong>"), (1.0, false));
1158 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 let (o, b) = opacity_of("<phong><transparency><float>0.3</float></transparency></phong>");
1163 assert!((o - 0.3).abs() < 1e-5 && b);
1164 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 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 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 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 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 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 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 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 assert!(m.meshes[0].material.is_none());
1280 }
1281
1282 #[test]
1283 fn bad_indices_and_accessors_are_dropped() {
1284 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 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 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 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 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}