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