1use pfx_core::anim::{Interpolation, Property, Trs};
2
3#[derive(Clone, Debug, PartialEq)]
4pub struct Mesh {
5 pub nodes: Vec<Node>,
6 pub groups: Vec<Group>,
7 pub materials: Vec<Material>,
8 pub roots: Vec<u32>,
9 pub scenes: Vec<Scene>,
10 pub skins: Vec<Skin>,
11 pub animations: Vec<Animation>,
12}
13
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct Scene {
16 pub name: Option<String>,
17 pub roots: Vec<u32>,
18}
19
20#[derive(Clone, Debug, PartialEq)]
21pub struct Node {
22 pub name: String,
23 pub parent: Option<u32>,
24 pub children: Vec<u32>,
25 pub transform: [[f32; 4]; 4],
26 pub trs: Trs,
27 pub group: Option<u32>,
28 pub skin: Option<u32>,
29 pub extras: Option<serde_json::Value>,
30}
31
32#[derive(Clone, Debug, PartialEq)]
33pub struct Skin {
34 pub name: String,
35 pub joints: Vec<u32>,
36 pub inverse_binds: Vec<[[f32; 4]; 4]>,
37}
38
39#[derive(Clone, Debug, PartialEq)]
40pub struct Animation {
41 pub name: String,
42 pub channels: Vec<AnimationChannel>,
43}
44
45#[derive(Clone, Debug, PartialEq)]
46pub struct AnimationChannel {
47 pub node: u32,
48 pub property: Property,
49 pub interpolation: Interpolation,
50 pub times: Vec<f32>,
51 pub values: Vec<f32>,
52}
53
54#[derive(Clone, Debug, PartialEq)]
55pub struct Group {
56 pub name: String,
57 pub primitives: Vec<Primitive>,
58 pub extras: Option<serde_json::Value>,
59}
60
61#[derive(Clone, Debug, PartialEq)]
62pub struct Primitive {
63 pub positions: Vec<[f32; 3]>,
64 pub normals: Vec<[f32; 3]>,
65 pub tangents: Vec<[f32; 4]>,
66 pub uvs: Vec<[f32; 2]>,
67 pub uvs1: Option<Vec<[f32; 2]>>,
68 pub indices: Vec<u32>,
69 pub material: Option<u32>,
70 pub joints: Option<Vec<[u16; 4]>>,
71 pub weights: Option<Vec<[f32; 4]>>,
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct Material {
76 pub name: String,
77 pub extras: Option<serde_json::Value>,
78}
79
80macro_rules! extras_helpers {
81 ($($kind:ty),*) => {$(
82 impl $kind {
83 pub fn extra(&self, key: &str) -> Option<&serde_json::Value> {
84 self.extras.as_ref()?.get(key)
85 }
86
87 pub fn extra_str(&self, key: &str) -> Option<&str> {
88 self.extra(key)?.as_str()
89 }
90
91 pub fn extra_f64(&self, key: &str) -> Option<f64> {
92 self.extra(key)?.as_f64()
93 }
94
95 pub fn extra_bool(&self, key: &str) -> Option<bool> {
96 self.extra(key)?.as_bool()
97 }
98
99 pub fn extra_array(&self, key: &str) -> Option<&[serde_json::Value]> {
100 self.extra(key)?.as_array().map(Vec::as_slice)
101 }
102
103 pub fn extra_object(
104 &self,
105 key: &str,
106 ) -> Option<&serde_json::Map<String, serde_json::Value>> {
107 self.extra(key)?.as_object()
108 }
109
110 pub fn extra_f64s(&self, key: &str) -> Option<Vec<f64>> {
111 self.extra_array(key)?
112 .iter()
113 .map(serde_json::Value::as_f64)
114 .collect()
115 }
116 }
117 )*};
118}
119
120extras_helpers!(Node, Group, Material);
121
122fn read_extras(extras: &gltf::json::Extras) -> Result<Option<serde_json::Value>, String> {
123 match extras {
124 Some(raw) => serde_json::from_str(raw.get())
125 .map(Some)
126 .map_err(|e| format!("mesh: bad extras: {e}")),
127 None => Ok(None),
128 }
129}
130
131const IDENTITY: [[f32; 4]; 4] = [
132 [1.0, 0.0, 0.0, 0.0],
133 [0.0, 1.0, 0.0, 0.0],
134 [0.0, 0.0, 1.0, 0.0],
135 [0.0, 0.0, 0.0, 1.0],
136];
137
138impl Mesh {
139 pub fn parse(bytes: &[u8]) -> Result<Self, String> {
140 Self::parse_with(bytes, |uri| Err(format!("mesh: external buffer {uri}")))
141 }
142
143 pub fn parse_with<F>(bytes: &[u8], read: F) -> Result<Self, String>
144 where
145 F: Fn(&str) -> Result<Vec<u8>, String>,
146 {
147 let gltf = gltf::Gltf::from_slice(bytes).map_err(|e| format!("mesh: {e}"))?;
148 let buffers = buffers(&gltf, &read)?;
149 let nodes = nodes(&gltf)?;
150 let groups = groups(&gltf, &buffers)?;
151 let materials = materials(&gltf)?;
152 let skins = skins(&gltf, &buffers)?;
153 let animations = animations(&gltf, &buffers)?;
154 for node in &nodes {
155 if let Some(group) = node.group
156 && groups.get(group as usize).is_none()
157 {
158 return Err("mesh: a node names a missing mesh".into());
159 }
160 }
161 for group in &groups {
162 for primitive in &group.primitives {
163 if let Some(material) = primitive.material
164 && materials.get(material as usize).is_none()
165 {
166 return Err("mesh: a triangle names a missing material".into());
167 }
168 }
169 }
170 let roots = gltf
171 .default_scene()
172 .or_else(|| gltf.scenes().next())
173 .map(|scene| scene.nodes().map(|node| node.index() as u32).collect())
174 .unwrap_or_default();
175 let scenes = gltf
176 .scenes()
177 .map(|scene| Scene {
178 name: scene.name().map(str::to_string),
179 roots: scene.nodes().map(|node| node.index() as u32).collect(),
180 })
181 .collect();
182 Ok(Mesh {
183 nodes,
184 groups,
185 materials,
186 roots,
187 scenes,
188 skins,
189 animations,
190 })
191 }
192
193 pub fn all_roots(&self) -> Vec<u32> {
194 let mut seen = std::collections::HashSet::new();
195 self.scenes
196 .iter()
197 .flat_map(|scene| scene.roots.iter().copied())
198 .filter(|root| seen.insert(*root))
199 .collect()
200 }
201
202 pub fn world(&self, node: u32) -> Option<[[f32; 4]; 4]> {
203 let mut chain = Vec::new();
204 let mut at = node;
205 for _ in 0..=self.nodes.len() {
206 chain.push(at);
207 match self.nodes.get(at as usize)?.parent {
208 Some(parent) => at = parent,
209 None => {
210 let mut matrix = IDENTITY;
211 for id in chain.iter().rev() {
212 matrix = crate::scene::multiply(matrix, self.nodes[*id as usize].transform);
213 }
214 return Some(matrix);
215 }
216 }
217 }
218 None
219 }
220}
221
222fn buffers<F>(gltf: &gltf::Gltf, read: &F) -> Result<Vec<Vec<u8>>, String>
223where
224 F: Fn(&str) -> Result<Vec<u8>, String>,
225{
226 let mut blob = gltf.blob.clone();
227 let mut out = Vec::new();
228 for buffer in gltf.buffers() {
229 let mut data = match buffer.source() {
230 gltf::buffer::Source::Bin => blob
231 .take()
232 .ok_or_else(|| "mesh: missing BIN chunk".to_string())?,
233 gltf::buffer::Source::Uri(uri) => load_uri(uri, read)?,
234 };
235 if data.len() < buffer.length() {
236 return Err(format!(
237 "mesh: buffer {} is {} bytes, the file says {}",
238 buffer.index(),
239 data.len(),
240 buffer.length()
241 ));
242 }
243 while !data.len().is_multiple_of(4) {
244 data.push(0);
245 }
246 out.push(data);
247 }
248 Ok(out)
249}
250
251fn load_uri<F>(uri: &str, read: &F) -> Result<Vec<u8>, String>
252where
253 F: Fn(&str) -> Result<Vec<u8>, String>,
254{
255 if let Some(rest) = uri.strip_prefix("data:") {
256 let (meta, payload) = rest
257 .split_once(',')
258 .ok_or_else(|| "mesh: bad data uri".to_string())?;
259 if meta.split(';').any(|part| part == "base64") {
260 return decode_base64(payload).map_err(|e| format!("mesh: {e}"));
261 }
262 return percent_decode(payload).map(|text| text.into_bytes());
263 }
264 if uri.contains(':') {
265 return Err(format!("mesh: unsupported buffer uri {uri}"));
266 }
267 read(&percent_decode(uri)?)
268}
269
270fn nodes(gltf: &gltf::Gltf) -> Result<Vec<Node>, String> {
271 let mut nodes = Vec::new();
272 for (index, node) in gltf.nodes().enumerate() {
273 if node.index() != index {
274 return Err("mesh: node order is not packed".into());
275 }
276 nodes.push(Node {
277 name: node.name().unwrap_or("").to_string(),
278 parent: None,
279 children: node.children().map(|child| child.index() as u32).collect(),
280 transform: node.transform().matrix(),
281 trs: {
282 let (translation, rotation, scale) = node.transform().decomposed();
283 Trs {
284 translation,
285 rotation,
286 scale,
287 }
288 },
289 group: node.mesh().map(|mesh| mesh.index() as u32),
290 skin: node.skin().map(|skin| skin.index() as u32),
291 extras: read_extras(node.extras())?,
292 });
293 }
294 let children: Vec<Vec<u32>> = nodes.iter().map(|node| node.children.clone()).collect();
295 for (index, child_list) in children.iter().enumerate() {
296 for &child in child_list {
297 let node = nodes
298 .get_mut(child as usize)
299 .ok_or("mesh: a node names a missing child")?;
300 if node.parent.is_some() {
301 return Err("mesh: a node has two parents".into());
302 }
303 node.parent = Some(index as u32);
304 }
305 }
306 Ok(nodes)
307}
308
309fn groups(gltf: &gltf::Gltf, buffers: &[Vec<u8>]) -> Result<Vec<Group>, String> {
310 let mut groups = Vec::new();
311 for (index, mesh) in gltf.meshes().enumerate() {
312 if mesh.index() != index {
313 return Err("mesh: mesh order is not packed".into());
314 }
315 let mut primitives = Vec::new();
316 for primitive in mesh.primitives() {
317 if let Some(primitive) = read_primitive(&primitive, buffers)? {
318 primitives.push(primitive);
319 }
320 }
321 groups.push(Group {
322 name: mesh.name().unwrap_or("").to_string(),
323 primitives,
324 extras: read_extras(mesh.extras())?,
325 });
326 }
327 Ok(groups)
328}
329
330fn materials(gltf: &gltf::Gltf) -> Result<Vec<Material>, String> {
331 let mut materials = Vec::new();
332 for (index, material) in gltf.materials().enumerate() {
333 if material.index() != Some(index) {
334 return Err("mesh: material order is not packed".into());
335 }
336 materials.push(Material {
337 name: material.name().unwrap_or("").to_string(),
338 extras: read_extras(material.extras())?,
339 });
340 }
341 Ok(materials)
342}
343
344fn skins(gltf: &gltf::Gltf, buffers: &[Vec<u8>]) -> Result<Vec<Skin>, String> {
345 let mut skins = Vec::new();
346 for (index, skin) in gltf.skins().enumerate() {
347 if skin.index() != index {
348 return Err("mesh: skin order is not packed".into());
349 }
350 let joints: Vec<u32> = skin.joints().map(|joint| joint.index() as u32).collect();
351 let reader = skin.reader(|buffer| buffers.get(buffer.index()).map(Vec::as_slice));
352 let inverse_binds: Vec<[[f32; 4]; 4]> = match reader.read_inverse_bind_matrices() {
353 Some(matrices) => matrices.collect(),
354 None => vec![IDENTITY; joints.len()],
355 };
356 if inverse_binds.len() != joints.len() {
357 return Err(format!(
358 "mesh: skin {index} has {} inverse bind matrices for {} joints",
359 inverse_binds.len(),
360 joints.len()
361 ));
362 }
363 skins.push(Skin {
364 name: skin.name().unwrap_or("").to_string(),
365 joints,
366 inverse_binds,
367 });
368 }
369 Ok(skins)
370}
371
372fn animations(gltf: &gltf::Gltf, buffers: &[Vec<u8>]) -> Result<Vec<Animation>, String> {
373 use gltf::animation::util::ReadOutputs;
374 let mut out = Vec::new();
375 for (index, animation) in gltf.animations().enumerate() {
376 let mut channels = Vec::new();
377 for channel in animation.channels() {
378 let property = match channel.target().property() {
379 gltf::animation::Property::Translation => Property::Translation,
380 gltf::animation::Property::Rotation => Property::Rotation,
381 gltf::animation::Property::Scale => Property::Scale,
382 gltf::animation::Property::MorphTargetWeights => continue,
383 };
384 let interpolation = match channel.sampler().interpolation() {
385 gltf::animation::Interpolation::Linear => Interpolation::Linear,
386 gltf::animation::Interpolation::Step => Interpolation::Step,
387 gltf::animation::Interpolation::CubicSpline => Interpolation::CubicSpline,
388 };
389 let reader = channel.reader(|buffer| buffers.get(buffer.index()).map(Vec::as_slice));
390 let times: Vec<f32> = reader
391 .read_inputs()
392 .ok_or_else(|| format!("mesh: animation {index} has a channel without key times"))?
393 .collect();
394 let values: Vec<f32> = match reader.read_outputs() {
395 Some(ReadOutputs::Translations(values)) => values.flatten().collect(),
396 Some(ReadOutputs::Scales(values)) => values.flatten().collect(),
397 Some(ReadOutputs::Rotations(values)) => values.into_f32().flatten().collect(),
398 Some(ReadOutputs::MorphTargetWeights(_)) | None => {
399 return Err(format!(
400 "mesh: animation {index} has a channel without key values"
401 ));
402 }
403 };
404 channels.push(AnimationChannel {
405 node: channel.target().node().index() as u32,
406 property,
407 interpolation,
408 times,
409 values,
410 });
411 }
412 out.push(Animation {
413 name: animation
414 .name()
415 .map_or_else(|| format!("animation{index}"), str::to_string),
416 channels,
417 });
418 }
419 Ok(out)
420}
421
422fn read_primitive(
423 primitive: &gltf::Primitive<'_>,
424 buffers: &[Vec<u8>],
425) -> Result<Option<Primitive>, String> {
426 if primitive.mode() != gltf::mesh::Mode::Triangles {
427 return Err("mesh: only triangles are supported".into());
428 }
429 let reader = primitive.reader(|buffer| buffers.get(buffer.index()).map(Vec::as_slice));
430 let Some(positions) = reader.read_positions() else {
431 return Ok(None);
432 };
433 let positions: Vec<[f32; 3]> = positions.collect();
434 let normals = fit(
435 reader
436 .read_normals()
437 .map(|values| values.collect())
438 .unwrap_or_default(),
439 positions.len(),
440 "normals",
441 )?;
442 let tangents = fit(
443 reader
444 .read_tangents()
445 .map(|values| values.collect())
446 .unwrap_or_default(),
447 positions.len(),
448 "tangents",
449 )?;
450 let uvs = fit(
451 reader
452 .read_tex_coords(0)
453 .map(|values| values.into_f32().collect())
454 .unwrap_or_default(),
455 positions.len(),
456 "uvs",
457 )?;
458 let uvs1 = reader
459 .read_tex_coords(1)
460 .map(|values| fit(values.into_f32().collect(), positions.len(), "uvs1"))
461 .transpose()?
462 .filter(|values| !values.is_empty());
463 let indices: Vec<u32> = match reader.read_indices() {
464 Some(indices) => indices.into_u32().collect(),
465 None => (0..positions.len() as u32).collect(),
466 };
467 if !indices.len().is_multiple_of(3) {
468 return Err("mesh: indices are not a list of triangles".into());
469 }
470 if indices
471 .iter()
472 .any(|index| *index as usize >= positions.len())
473 {
474 return Err("mesh: an index is outside the vertex list".into());
475 }
476 let joints = reader
477 .read_joints(0)
478 .map(|values| fit(values.into_u16().collect(), positions.len(), "joints"))
479 .transpose()?
480 .filter(|values| !values.is_empty());
481 let weights = reader
482 .read_weights(0)
483 .map(|values| fit(values.into_f32().collect(), positions.len(), "weights"))
484 .transpose()?
485 .filter(|values| !values.is_empty());
486 if joints.is_some() != weights.is_some() {
487 return Err(
488 "mesh: a primitive has joints without weights or weights without joints".into(),
489 );
490 }
491 Ok(Some(Primitive {
492 positions,
493 normals,
494 tangents,
495 uvs,
496 uvs1,
497 indices,
498 material: primitive.material().index().map(|index| index as u32),
499 joints,
500 weights,
501 }))
502}
503
504fn fit<T>(values: Vec<T>, len: usize, what: &str) -> Result<Vec<T>, String> {
505 if values.is_empty() || values.len() == len {
506 Ok(values)
507 } else {
508 Err(format!(
509 "mesh: {what} has {} values for {len} vertices",
510 values.len()
511 ))
512 }
513}
514
515fn percent_decode(input: &str) -> Result<String, String> {
516 let bytes = input.as_bytes();
517 let mut out = Vec::with_capacity(bytes.len());
518 let mut index = 0;
519 while index < bytes.len() {
520 if bytes[index] == b'%' {
521 if index + 2 >= bytes.len() {
522 return Err("mesh: bad percent escape".into());
523 }
524 let hex = std::str::from_utf8(&bytes[index + 1..index + 3])
525 .map_err(|_| "mesh: bad percent escape")?;
526 let value = u8::from_str_radix(hex, 16).map_err(|_| "mesh: bad percent escape")?;
527 out.push(value);
528 index += 3;
529 } else {
530 out.push(bytes[index]);
531 index += 1;
532 }
533 }
534 String::from_utf8(out).map_err(|_| "mesh: buffer uri is not utf-8".into())
535}
536
537fn decode_base64(input: &str) -> Result<Vec<u8>, String> {
538 fn value(byte: u8) -> Option<u8> {
539 match byte {
540 b'A'..=b'Z' => Some(byte - b'A'),
541 b'a'..=b'z' => Some(byte - b'a' + 26),
542 b'0'..=b'9' => Some(byte - b'0' + 52),
543 b'+' => Some(62),
544 b'/' => Some(63),
545 _ => None,
546 }
547 }
548 let bytes: Vec<u8> = input
549 .bytes()
550 .filter(|byte| !byte.is_ascii_whitespace())
551 .collect();
552 if !bytes.len().is_multiple_of(4) {
553 return Err("bad base64".into());
554 }
555 let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
556 for chunk in bytes.chunks_exact(4) {
557 let pad = chunk.iter().rev().take_while(|byte| **byte == b'=').count();
558 if pad > 2 {
559 return Err("bad base64".into());
560 }
561 let mut packed = 0u32;
562 for (index, &byte) in chunk.iter().enumerate() {
563 if byte == b'=' {
564 if index < 4 - pad {
565 return Err("bad base64".into());
566 }
567 continue;
568 }
569 let value = value(byte).ok_or("bad base64")?;
570 packed |= u32::from(value) << (18 - 6 * index);
571 }
572 out.push((packed >> 16) as u8);
573 if pad < 2 {
574 out.push((packed >> 8) as u8);
575 }
576 if pad < 1 {
577 out.push(packed as u8);
578 }
579 }
580 Ok(out)
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586 use crate::Source;
587 use std::path::Path;
588
589 fn triangle(mesh: &Mesh) {
590 assert_eq!(mesh.roots, vec![0]);
591 assert_eq!(mesh.nodes[0].name, "root");
592 assert_eq!(mesh.nodes[0].children, vec![1]);
593 assert_eq!(mesh.nodes[0].parent, None);
594 assert_eq!(mesh.nodes[1].name, "triangle");
595 assert_eq!(mesh.nodes[1].parent, Some(0));
596 assert_eq!(mesh.nodes[1].group, Some(0));
597 assert_eq!(mesh.groups[0].name, "sheet");
598 assert_eq!(
599 mesh.materials,
600 vec![Material {
601 name: "ink".into(),
602 extras: None
603 }]
604 );
605 let primitive = &mesh.groups[0].primitives[0];
606 assert_eq!(
607 primitive.positions,
608 vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
609 );
610 assert_eq!(primitive.normals, vec![[0.0, 0.0, 1.0]; 3]);
611 assert_eq!(primitive.tangents, vec![[1.0, 0.0, 0.0, 1.0]; 3]);
612 assert_eq!(primitive.uvs, vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]);
613 assert_eq!(primitive.indices, vec![0, 1, 2]);
614 assert_eq!(primitive.material, Some(0));
615 let world = mesh.world(1).unwrap();
616 assert_eq!(world[3], [1.0, 2.0, 3.0, 1.0]);
617 assert_eq!(mesh.world(0).unwrap()[0], [1.0, 0.0, 0.0, 0.0]);
618 }
619
620 #[test]
621 fn a_hand_made_glb_triangle_keeps_its_names_and_local_vertices() {
622 let glb = Mesh::parse(include_bytes!("../tests/data/triangle.glb")).unwrap();
623 triangle(&glb);
624 let folder = Source::folder(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data"));
625 let json = String::from_utf8(folder.read("triangle.gltf").unwrap()).unwrap();
626 let gltf = Mesh::parse_with(json.as_bytes(), |uri| {
627 folder.read_beside("triangle.gltf", uri)
628 })
629 .unwrap();
630 assert_eq!(gltf, glb);
631 let encoded = base64_encode(include_bytes!("../tests/data/triangle.bin"));
632 let inline = json.replace(
633 "\"uri\":\"triangle.bin\"",
634 &format!("\"uri\":\"data:application/octet-stream;base64,{encoded}\""),
635 );
636 assert_eq!(Mesh::parse(inline.as_bytes()).unwrap(), glb);
637 assert_eq!(decode_base64("TQ==").unwrap(), b"M");
638 assert_eq!(decode_base64("TWE=").unwrap(), b"Ma");
639 assert_eq!(decode_base64("TWFu").unwrap(), b"Man");
640 assert_eq!(percent_decode("a%2Fb.bin").unwrap(), "a/b.bin");
641 }
642
643 fn scenes_json(scenes: &str, default: &str) -> String {
644 format!(
645 "{{\"asset\":{{\"version\":\"2.0\"}}{scenes}{default},\"nodes\":[\
646 {{\"name\":\"a\"}},{{\"name\":\"b\"}},{{\"name\":\"c\"}},{{\"name\":\"d\"}}]}}"
647 )
648 }
649
650 #[test]
651 fn every_scene_keeps_its_roots_and_all_roots_lists_each_node_once() {
652 let json = scenes_json(
653 ",\"scenes\":[{\"name\":\"first\",\"nodes\":[0,1]},\
654 {\"name\":\"second\",\"nodes\":[1,2]},{\"nodes\":[3]}]",
655 "",
656 );
657 let mesh = Mesh::parse(json.as_bytes()).unwrap();
658 assert_eq!(
659 mesh.scenes,
660 vec![
661 Scene {
662 name: Some("first".into()),
663 roots: vec![0, 1]
664 },
665 Scene {
666 name: Some("second".into()),
667 roots: vec![1, 2]
668 },
669 Scene {
670 name: None,
671 roots: vec![3]
672 },
673 ]
674 );
675 assert_eq!(mesh.roots, vec![0, 1]);
676 assert_eq!(mesh.all_roots(), vec![0, 1, 2, 3]);
677
678 let json = scenes_json(
679 ",\"scenes\":[{\"nodes\":[0,1]},{\"nodes\":[1,2]},{\"nodes\":[3,0]}]",
680 ",\"scene\":1",
681 );
682 let mesh = Mesh::parse(json.as_bytes()).unwrap();
683 assert_eq!(mesh.roots, vec![1, 2]);
684 assert_eq!(mesh.scenes.len(), 3);
685 assert_eq!(mesh.all_roots(), vec![0, 1, 2, 3]);
686 }
687
688 #[test]
689 fn a_file_without_scenes_has_no_roots() {
690 let json = scenes_json("", "");
691 let mesh = Mesh::parse(json.as_bytes()).unwrap();
692 assert_eq!(mesh.nodes.len(), 4);
693 assert!(mesh.roots.is_empty());
694 assert!(mesh.scenes.is_empty());
695 assert!(mesh.all_roots().is_empty());
696 }
697
698 const EXTRAS_NODE_A: &str = "\"extras\":{\"mover\":true,\"kind\":\"door\",\"speed\":2.5,\"axis\":[0,1,0],\"limits\":{\"min\":-1,\"max\":1.5,\"tags\":[\"a\",\"b\"]},\"none\":null}";
699
700 fn with_extras(json: &str) -> String {
701 json.replace(
702 "{\"name\":\"root\",\"children\":[1]}",
703 &format!("{{\"name\":\"root\",\"children\":[1],{EXTRAS_NODE_A}}}"),
704 )
705 .replace(
706 "\"translation\":[1,2,3]}",
707 "\"translation\":[1,2,3],\"extras\":{\"flag\":false,\"id\":7}}",
708 )
709 .replace(
710 "\"name\":\"sheet\",",
711 "\"name\":\"sheet\",\"extras\":{\"shadow\":\"only\"},",
712 )
713 .replace(
714 "{\"name\":\"ink\"}",
715 "{\"name\":\"ink\",\"extras\":{\"wet\":1.5,\"layers\":[1,2,3]}}",
716 )
717 }
718
719 fn glb(json: &str, bin: &[u8]) -> Vec<u8> {
720 let mut json = json.as_bytes().to_vec();
721 while !json.len().is_multiple_of(4) {
722 json.push(b' ');
723 }
724 let mut bin = bin.to_vec();
725 while !bin.len().is_multiple_of(4) {
726 bin.push(0);
727 }
728 let total = 12 + 8 + json.len() + 8 + bin.len();
729 let mut out = Vec::new();
730 out.extend_from_slice(b"glTF");
731 out.extend_from_slice(&2u32.to_le_bytes());
732 out.extend_from_slice(&(total as u32).to_le_bytes());
733 out.extend_from_slice(&(json.len() as u32).to_le_bytes());
734 out.extend_from_slice(b"JSON");
735 out.extend_from_slice(&json);
736 out.extend_from_slice(&(bin.len() as u32).to_le_bytes());
737 out.extend_from_slice(b"BIN\0");
738 out.extend_from_slice(&bin);
739 out
740 }
741
742 #[test]
743 fn extras_of_every_json_kind_are_read_from_nodes_a_mesh_and_a_material() {
744 let json = include_str!("../tests/data/triangle.gltf");
745 let bin = include_bytes!("../tests/data/triangle.bin");
746 let embedded = with_extras(json).replace("{\"uri\":\"triangle.bin\",", "{");
747 let mesh = Mesh::parse(&glb(&embedded, bin)).unwrap();
748
749 let root = &mesh.nodes[0];
750 assert_eq!(root.extra_bool("mover"), Some(true));
751 assert_eq!(root.extra_str("kind"), Some("door"));
752 assert_eq!(root.extra_f64("speed"), Some(2.5));
753 assert_eq!(root.extra_f64s("axis"), Some(vec![0.0, 1.0, 0.0]));
754 assert_eq!(root.extra_array("axis").unwrap().len(), 3);
755 let limits = root.extra_object("limits").unwrap();
756 assert_eq!(limits["min"], serde_json::json!(-1));
757 assert_eq!(limits["tags"], serde_json::json!(["a", "b"]));
758 assert_eq!(root.extra("none"), Some(&serde_json::Value::Null));
759 assert_eq!(root.extra_str("none"), None);
760
761 let triangle = &mesh.nodes[1];
762 assert_eq!(triangle.extra_bool("flag"), Some(false));
763 assert_eq!(triangle.extra_f64("id"), Some(7.0));
764
765 assert_eq!(mesh.groups[0].extra_str("shadow"), Some("only"));
766 assert_eq!(mesh.materials[0].extra_f64("wet"), Some(1.5));
767 assert_eq!(
768 mesh.materials[0].extra_f64s("layers"),
769 Some(vec![1.0, 2.0, 3.0])
770 );
771
772 let text = Mesh::parse_with(with_extras(json).as_bytes(), |uri| {
773 if uri == "triangle.bin" {
774 Ok(include_bytes!("../tests/data/triangle.bin").to_vec())
775 } else {
776 Err(format!("unexpected {uri}"))
777 }
778 })
779 .unwrap();
780 assert_eq!(text, mesh);
781 }
782
783 #[test]
784 fn extra_helpers_never_panic_on_missing_or_mistyped_keys() {
785 let json = include_str!("../tests/data/triangle.gltf");
786 let bin = include_bytes!("../tests/data/triangle.bin");
787 let embedded = with_extras(json).replace("{\"uri\":\"triangle.bin\",", "{");
788 let mesh = Mesh::parse(&glb(&embedded, bin)).unwrap();
789 let root = &mesh.nodes[0];
790 assert_eq!(root.extra("missing"), None);
791 assert_eq!(root.extra_str("mover"), None);
792 assert_eq!(root.extra_bool("kind"), None);
793 assert_eq!(root.extra_f64("kind"), None);
794 assert_eq!(root.extra_array("kind"), None);
795 assert_eq!(root.extra_object("axis"), None);
796 assert_eq!(root.extra_f64s("limits"), None);
797 let mixed = Node {
798 extras: Some(serde_json::json!({"mixed": [1, "x"], "list": 4})),
799 ..mesh.nodes[0].clone()
800 };
801 assert_eq!(mixed.extra_f64s("mixed"), None);
802 let scalar = Node {
803 extras: Some(serde_json::json!(5)),
804 ..mesh.nodes[0].clone()
805 };
806 assert_eq!(scalar.extra("anything"), None);
807 let none = Node {
808 extras: None,
809 ..mesh.nodes[0].clone()
810 };
811 assert_eq!(none.extra_str("kind"), None);
812 }
813
814 #[test]
815 fn a_file_without_extras_loads_with_none_and_the_same_geometry() {
816 let plain = Mesh::parse(include_bytes!("../tests/data/triangle.glb")).unwrap();
817 assert!(plain.nodes.iter().all(|node| node.extras.is_none()));
818 assert!(plain.groups.iter().all(|group| group.extras.is_none()));
819 assert!(
820 plain
821 .materials
822 .iter()
823 .all(|material| material.extras.is_none())
824 );
825 let json = include_str!("../tests/data/triangle.gltf");
826 let bin = include_bytes!("../tests/data/triangle.bin");
827 let embedded = with_extras(json).replace("{\"uri\":\"triangle.bin\",", "{");
828 let extra = Mesh::parse(&glb(&embedded, bin)).unwrap();
829 assert_eq!(extra.groups[0].primitives, plain.groups[0].primitives);
830 assert_eq!(extra.nodes[1].transform, plain.nodes[1].transform);
831 assert_eq!(extra.roots, plain.roots);
832 }
833
834 fn with_second_uvs(uvs1: &[[f32; 2]]) -> Vec<u8> {
835 let json = include_str!("../tests/data/triangle.gltf");
836 let mut bin = include_bytes!("../tests/data/triangle.bin").to_vec();
837 while !bin.len().is_multiple_of(4) {
838 bin.push(0);
839 }
840 let offset = bin.len();
841 for uv in uvs1 {
842 for value in uv {
843 bin.extend_from_slice(&value.to_le_bytes());
844 }
845 }
846 let json = json
847 .replace("{\"uri\":\"triangle.bin\",\"byteLength\":150}", &format!("{{\"byteLength\":{}}}", bin.len()))
848 .replace(
849 "\"TEXCOORD_0\":3}",
850 "\"TEXCOORD_0\":3,\"TEXCOORD_1\":5}",
851 )
852 .replace(
853 "\"type\":\"SCALAR\"}]",
854 &format!(
855 "\"type\":\"SCALAR\"}},{{\"bufferView\":5,\"componentType\":5126,\"count\":{},\"type\":\"VEC2\"}}]",
856 uvs1.len()
857 ),
858 )
859 .replace(
860 "\"byteLength\":6}]",
861 &format!(
862 "\"byteLength\":6}},{{\"buffer\":0,\"byteOffset\":{offset},\"byteLength\":{}}}]",
863 uvs1.len() * 8
864 ),
865 );
866 glb(&json, &bin)
867 }
868
869 #[test]
870 fn a_second_uv_set_loads_beside_the_first() {
871 let second = [[0.25, 0.5], [0.75, 0.5], [0.25, 1.0]];
872 let mesh = Mesh::parse(&with_second_uvs(&second)).unwrap();
873 let primitive = &mesh.groups[0].primitives[0];
874 assert_eq!(primitive.uvs1.as_deref(), Some(&second[..]));
875 assert_eq!(primitive.uvs, vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]);
876 let plain = Mesh::parse(include_bytes!("../tests/data/triangle.glb")).unwrap();
877 assert_eq!(plain.groups[0].primitives[0].uvs1, None);
878 let without = Primitive {
879 uvs1: None,
880 ..primitive.clone()
881 };
882 assert_eq!(without, plain.groups[0].primitives[0]);
883 assert!(
884 Mesh::parse(&with_second_uvs(&second[..2]))
885 .unwrap_err()
886 .contains("uvs1 has 2 values for 3 vertices")
887 );
888 }
889
890 fn base64_encode(bytes: &[u8]) -> String {
891 const ALPHA: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
892 let mut out = String::new();
893 let mut index = 0;
894 while index + 3 <= bytes.len() {
895 let packed = (u32::from(bytes[index]) << 16)
896 | (u32::from(bytes[index + 1]) << 8)
897 | u32::from(bytes[index + 2]);
898 for shift in [18, 12, 6, 0] {
899 out.push(ALPHA[((packed >> shift) & 63) as usize] as char);
900 }
901 index += 3;
902 }
903 out
904 }
905}