git.lucas.co / cce-mesh-io
mesh files in: STL, OBJ, glTF and PLY
git clone https://git.lucas.co/cce-mesh-io.git

src/gltf.rs (20K)

  1 //! glTF 2.0, both forms: `.gltf` (JSON, buffers beside it or inline as
  2 //! base64 data URIs) and `.glb` (one binary file).
  3 //!
  4 //! The scene's node tree is walked and every mesh placed where its node's
  5 //! transforms put it, one [`Part`] per node with a mesh, named by the node
  6 //! (else the mesh). Triangle lists, strips and fans are read; points and
  7 //! lines are skipped. Each primitive's material becomes a [`Material`]: its
  8 //! base colour factor, metallic and roughness factors, and its base-colour
  9 //! texture (decoded once per image, the first time a material uses it; the
 10 //! texture-coordinate set it names supplies the corners' uvs). A `COLOR_0`
 11 //! attribute becomes corner colours; the spec multiplies them by the factor,
 12 //! and the material carries the factor, so they are stored as they are.
 13 //! NORMAL is kept, carried through the node's inverse-transpose; a part where
 14 //! any primitive lacks it has no file normals. Metallic-roughness and normal
 15 //! TEXTURES are not read. A node whose transform mirrors (negative
 16 //! determinant) has its winding reversed, so its faces still point out. glTF
 17 //! is Y-up by its spec, in metres.
 18 //!
 19 //! Buffers and images are loaded here rather than by the crate's `import`
 20 //! feature, which would decode every image whether a material uses it or
 21 //! not, and drags in every image format.
 22 
 23 use std::path::Path;
 24 
 25 use base64::Engine as _;
 26 use glam::{Mat3, Mat4, Vec3};
 27 use gltf::buffer::Source;
 28 use gltf::mesh::Mode;
 29 
 30 use crate::mesh::{Material, Mesh};
 31 use crate::{Part, Scene, Texture, Unit, UpAxis};
 32 
 33 /// Deeper than any real node tree; a guard against a cycle the parser let by.
 34 const MAX_DEPTH: usize = 128;
 35 
 36 pub fn read(bytes: &[u8], dir: Option<&Path>) -> Result<Scene, String> {
 37     let file = gltf::Gltf::from_slice(bytes).map_err(|e| format!("not a readable glTF: {e}"))?;
 38     let buffers = load_buffers(&file, dir)?;
 39     let mut parts = Vec::new();
 40     let mut skipped_modes = 0usize;
 41     let mut textures: Vec<Texture> = Vec::new();
 42     // glTF image index -> index into `textures`, or None for one that would
 43     // not load (said once, then drawn without).
 44     let mut image_slot: std::collections::HashMap<usize, Option<usize>> = std::collections::HashMap::new();
 45 
 46     let roots: Vec<gltf::Node> = match file.default_scene().or_else(|| file.scenes().next()) {
 47         Some(scene) => scene.nodes().collect(),
 48         None => {
 49             // No scene: every node that is nobody's child is a root.
 50             let children: std::collections::HashSet<usize> =
 51                 file.nodes().flat_map(|n| n.children().map(|c| c.index())).collect();
 52             file.nodes().filter(|n| !children.contains(&n.index())).collect()
 53         }
 54     };
 55     // Pushed in reverse, so the stack pops in pre-order: parts come out in
 56     // the order the file lists them, parents before children.
 57     let mut stack: Vec<(gltf::Node, Mat4, usize)> = roots.into_iter().rev().map(|n| (n, Mat4::IDENTITY, 0)).collect();
 58     while let Some((node, parent, depth)) = stack.pop() {
 59         if depth > MAX_DEPTH {
 60             return Err("the node tree is too deep (a cycle?)".into());
 61         }
 62         let world = parent * Mat4::from_cols_array_2d(&node.transform().matrix());
 63         let children: Vec<gltf::Node> = node.children().collect();
 64         for child in children.into_iter().rev() {
 65             stack.push((child, world, depth + 1));
 66         }
 67         let Some(gmesh) = node.mesh() else { continue };
 68         let mirrored = world.determinant() < 0.0;
 69         let normal_matrix = Mat3::from_mat4(world).inverse().transpose();
 70         let mut mesh = Mesh::default();
 71         let mut corner_colors: Vec<[f32; 3]> = Vec::new();
 72         let mut any_colors = false;
 73         let mut corner_uvs: Vec<[f32; 2]> = Vec::new();
 74         let mut any_uvs = false;
 75         let mut corner_normals: Vec<Vec3> = Vec::new();
 76         let mut all_normals = true;
 77         for prim in gmesh.primitives() {
 78             let reader = prim.reader(|b| buffers.get(b.index()).map(Vec::as_slice));
 79             let Some(positions) = reader.read_positions() else { continue };
 80             let base = mesh.positions.len() as u32;
 81             mesh.positions.extend(positions.map(|p| world.transform_point3(Vec3::from(p))));
 82             let count = mesh.positions.len() as u32 - base;
 83             let indices: Vec<u32> = match reader.read_indices() {
 84                 Some(i) => i.into_u32().collect(),
 85                 None => (0..count).collect(),
 86             };
 87             if let Some(&bad) = indices.iter().find(|&&i| i >= count) {
 88                 return Err(format!("mesh '{}' indexes point {bad} of {count}", gmesh.name().unwrap_or("?")));
 89             }
 90             let tris = triangles(prim.mode(), &indices);
 91             let Some(tris) = tris else {
 92                 skipped_modes += 1;
 93                 continue;
 94             };
 95             let material = prim.material();
 96             let pbr = material.pbr_metallic_roughness();
 97             let [r, g, b, _] = pbr.base_color_factor();
 98             let base_texture = pbr.base_color_texture();
 99             let texture = base_texture.as_ref().and_then(|info| {
100                 let image = info.texture().source();
101                 *image_slot.entry(image.index()).or_insert_with(|| match load_image(&image, &buffers, dir) {
102                     Ok(t) => {
103                         textures.push(t);
104                         Some(textures.len() - 1)
105                     }
106                     Err(e) => {
107                         log::warn!("[mesh-io] glTF image {}: {e}", image.index());
108                         None
109                     }
110                 })
111             });
112             let slot = mesh.materials.len() as u32;
113             mesh.materials.push(Material {
114                 color: [r, g, b],
115                 texture,
116                 metallic: pbr.metallic_factor(),
117                 roughness: pbr.roughness_factor(),
118             });
119             let point_colors: Option<Vec<[f32; 3]>> = reader.read_colors(0).map(|c| c.into_rgb_f32().collect());
120             let set = base_texture.as_ref().map_or(0, |info| info.tex_coord());
121             let point_uvs: Option<Vec<[f32; 2]>> = reader.read_tex_coords(set).map(|t| t.into_f32().collect());
122             let point_normals: Option<Vec<Vec3>> = reader
123                 .read_normals()
124                 .map(|n| n.map(|n| (normal_matrix * Vec3::from(n)).normalize_or_zero()).collect());
125             all_normals &= point_normals.is_some();
126             for t in tris {
127                 let t = if mirrored { [t[0], t[2], t[1]] } else { t };
128                 for k in t {
129                     let k = k as usize;
130                     corner_colors.push(match &point_colors {
131                         Some(pc) => {
132                             any_colors = true;
133                             pc.get(k).copied().unwrap_or([1.0; 3])
134                         }
135                         None => [1.0; 3],
136                     });
137                     corner_uvs.push(match &point_uvs {
138                         Some(pu) => {
139                             any_uvs = true;
140                             pu.get(k).copied().unwrap_or([0.0; 2])
141                         }
142                         None => [0.0; 2],
143                     });
144                     if let Some(pn) = &point_normals {
145                         corner_normals.push(pn.get(k).copied().unwrap_or(Vec3::Y));
146                     }
147                 }
148                 mesh.triangles.push(t.map(|i| i + base));
149                 mesh.tri_material.push(slot);
150             }
151         }
152         if any_colors {
153             // Corner colours replace the material's colour where present
154             // (`Mesh::corner_color`), so a material with a factor carries
155             // it into them: the spec multiplies the two.
156             let tinted = corner_colors
157                 .iter()
158                 .enumerate()
159                 .map(|(i, c)| {
160                     let f = mesh.materials[mesh.tri_material[i / 3] as usize].color;
161                     [c[0] * f[0], c[1] * f[1], c[2] * f[2]]
162                 })
163                 .collect();
164             mesh.corner_colors = Some(tinted);
165         }
166         if any_uvs {
167             mesh.corner_uvs = Some(corner_uvs);
168         }
169         if all_normals && !mesh.triangles.is_empty() {
170             mesh.file_normals = Some(corner_normals);
171         }
172         let name = node.name().or(gmesh.name()).map(str::to_string).unwrap_or_else(|| format!("node {}", node.index()));
173         parts.push(Part { name, mesh });
174     }
175 
176     if skipped_modes > 0 {
177         log::info!("[mesh-io] glTF: skipped {skipped_modes} primitives of points or lines");
178     }
179     Ok(Scene { parts, up: UpAxis::Y, unit: Unit::Metre, textures })
180 }
181 
182 /// A primitive's index list as triangles, or `None` for points and lines.
183 fn triangles(mode: Mode, i: &[u32]) -> Option<Vec<[u32; 3]>> {
184     Some(match mode {
185         Mode::Triangles => i.chunks_exact(3).map(|t| [t[0], t[1], t[2]]).collect(),
186         // Every other triangle of a strip is reversed to keep the winding.
187         Mode::TriangleStrip => (0..i.len().saturating_sub(2))
188             .map(|k| if k % 2 == 0 { [i[k], i[k + 1], i[k + 2]] } else { [i[k + 1], i[k], i[k + 2]] })
189             .collect(),
190         Mode::TriangleFan => (1..i.len().saturating_sub(1)).map(|k| [i[0], i[k], i[k + 1]]).collect(),
191         Mode::Points | Mode::Lines | Mode::LineLoop | Mode::LineStrip => return None,
192     })
193 }
194 
195 fn load_buffers(file: &gltf::Gltf, dir: Option<&Path>) -> Result<Vec<Vec<u8>>, String> {
196     file.buffers()
197         .map(|b| {
198             let data = match b.source() {
199                 Source::Bin => file.blob.clone().ok_or("the file names a binary chunk it does not have")?,
200                 Source::Uri(uri) if uri.starts_with("data:") => {
201                     let (_, payload) = uri.split_once(";base64,").ok_or("a data URI that is not base64")?;
202                     base64::engine::general_purpose::STANDARD
203                         .decode(payload)
204                         .map_err(|e| format!("a bad base64 buffer: {e}"))?
205                 }
206                 Source::Uri(uri) => {
207                     let path = dir.unwrap_or(Path::new(".")).join(percent_decode(uri));
208                     std::fs::read(&path).map_err(|e| format!("buffer {}: {e}", path.display()))?
209                 }
210             };
211             if data.len() < b.length() {
212                 return Err(format!("buffer {} is {} bytes, shorter than the {} it declares", b.index(), data.len(), b.length()));
213             }
214             Ok(data)
215         })
216         .collect()
217 }
218 
219 /// Decode an image a texture names: from a buffer view (a GLB's usual way),
220 /// a data URI, or a file beside the glTF.
221 fn load_image(image: &gltf::Image, buffers: &[Vec<u8>], dir: Option<&Path>) -> Result<Texture, String> {
222     match image.source() {
223         gltf::image::Source::View { view, .. } => {
224             let buffer = buffers.get(view.buffer().index()).ok_or("an image in a buffer that is not there")?;
225             let bytes = buffer
226                 .get(view.offset()..view.offset() + view.length())
227                 .ok_or("an image past the end of its buffer")?;
228             Texture::decode(bytes)
229         }
230         gltf::image::Source::Uri { uri, .. } if uri.starts_with("data:") => {
231             let (_, payload) = uri.split_once(";base64,").ok_or("an image data URI that is not base64")?;
232             let bytes = base64::engine::general_purpose::STANDARD.decode(payload).map_err(|e| format!("bad base64: {e}"))?;
233             Texture::decode(&bytes)
234         }
235         gltf::image::Source::Uri { uri, .. } => {
236             let path = dir.unwrap_or(Path::new(".")).join(percent_decode(uri));
237             let bytes = std::fs::read(&path).map_err(|e| format!("{}: {e}", path.display()))?;
238             Texture::decode(&bytes)
239         }
240     }
241 }
242 
243 /// `my%20model.bin` → `my model.bin`: URIs in a glTF are percent-encoded.
244 fn percent_decode(s: &str) -> String {
245     let b = s.as_bytes();
246     let mut out = Vec::with_capacity(b.len());
247     let mut i = 0;
248     while i < b.len() {
249         if b[i] == b'%' && i + 2 < b.len() {
250             if let Ok(v) = u8::from_str_radix(&s[i + 1..i + 3], 16) {
251                 out.push(v);
252                 i += 3;
253                 continue;
254             }
255         }
256         out.push(b[i]);
257         i += 1;
258     }
259     String::from_utf8_lossy(&out).into_owned()
260 }
261 
262 #[cfg(test)]
263 mod tests {
264     use super::*;
265 
266     /// One triangle's points and indices as a base64 buffer, and the JSON
267     /// that describes it, with `nodes` and `extra` spliced in.
268     fn gltf_json(nodes: &str, mode: u32, extra: &str) -> (String, Vec<u8>) {
269         let mut bin = Vec::new();
270         for p in [[0f32, 0., 0.], [1., 0., 0.], [0., 1., 0.], [1., 1., 0.]] {
271             for c in p {
272                 bin.extend(c.to_le_bytes());
273             }
274         }
275         for i in [0u16, 1, 2, 3] {
276             bin.extend(i.to_le_bytes());
277         }
278         let json = format!(
279             r#"{{"asset":{{"version":"2.0"}},"scene":0,"scenes":[{{"nodes":[0]}}],"nodes":{nodes},
280             "meshes":[{{"name":"tri","primitives":[{{"attributes":{{"POSITION":0}},"indices":1,"mode":{mode},"material":0}}]}}],
281             "materials":[{{"pbrMetallicRoughness":{{"baseColorFactor":[0.8,0.2,0.1,1.0]}}}}],
282             "accessors":[{{"bufferView":0,"componentType":5126,"count":4,"type":"VEC3","min":[0,0,0],"max":[1,1,0]}},
283                          {{"bufferView":1,"componentType":5123,"count":4,"type":"SCALAR"}}],
284             "bufferViews":[{{"buffer":0,"byteOffset":0,"byteLength":48}},{{"buffer":0,"byteOffset":48,"byteLength":8}}],
285             "buffers":[{{"byteLength":56{extra}}}]}}"#
286         );
287         (json, bin)
288     }
289 
290     fn inline(nodes: &str, mode: u32) -> Vec<u8> {
291         let (_, bin) = gltf_json(nodes, mode, "");
292         let uri = format!(r#","uri":"data:application/octet-stream;base64,{}""#, base64::engine::general_purpose::STANDARD.encode(&bin));
293         gltf_json(nodes, mode, &uri).0.into_bytes()
294     }
295 
296     #[test]
297     fn a_node_transform_places_the_mesh_and_the_material_colours_it() {
298         let s = read(&inline(r#"[{"name":"moved","mesh":0,"translation":[10,0,0],"scale":[2,2,2]}]"#, 5), None).unwrap();
299         assert_eq!(s.up, UpAxis::Y);
300         assert_eq!(s.unit, Unit::Metre);
301         assert_eq!(Unit::Metre.millimetres(), Some(1000.0));
302         let part = &s.parts[0];
303         assert_eq!(part.name, "moved");
304         assert_eq!(part.mesh.positions[1], Vec3::new(12.0, 0.0, 0.0));
305         assert_eq!(part.mesh.triangles.len(), 2, "a strip of four points is two triangles");
306         assert_eq!(part.mesh.corner_color(0, 0), [0.8, 0.2, 0.1]);
307         assert_eq!(part.mesh.material(0).roughness, 1.0, "the spec's default factor");
308     }
309 
310     #[test]
311     fn children_inherit_their_parents_transform() {
312         let nodes = r#"[{"translation":[0,5,0],"children":[1]},{"mesh":0,"translation":[1,0,0]}]"#;
313         let s = read(&inline(nodes, 4), None).unwrap();
314         assert_eq!(s.parts.len(), 1);
315         assert_eq!(s.parts[0].name, "tri", "an unnamed node takes its mesh's name");
316         assert_eq!(s.parts[0].mesh.positions[0], Vec3::new(1.0, 5.0, 0.0));
317     }
318 
319     #[test]
320     fn a_mirroring_node_keeps_faces_pointing_out() {
321         let plain = read(&inline(r#"[{"mesh":0}]"#, 4), None).unwrap();
322         let mirrored = read(&inline(r#"[{"mesh":0,"scale":[-1,1,1]}]"#, 4), None).unwrap();
323         let normal = |m: &Mesh| {
324             let [a, b, c] = m.triangles[0].map(|i| m.positions[i as usize]);
325             (b - a).cross(c - a).normalize()
326         };
327         assert_eq!(normal(&plain.parts[0].mesh), Vec3::Z);
328         assert_eq!(normal(&mirrored.parts[0].mesh), Vec3::Z, "the mirror flipped the winding back");
329     }
330 
331     #[test]
332     fn a_glb_is_read_from_its_binary_chunk() {
333         let (json, bin) = gltf_json(r#"[{"mesh":0}]"#, 4, "");
334         let mut json = json.into_bytes();
335         while json.len() % 4 != 0 {
336             json.push(b' ');
337         }
338         let mut glb = Vec::new();
339         glb.extend(b"glTF");
340         glb.extend(2u32.to_le_bytes());
341         glb.extend(((12 + 8 + json.len() + 8 + bin.len()) as u32).to_le_bytes());
342         glb.extend((json.len() as u32).to_le_bytes());
343         glb.extend(b"JSON");
344         glb.extend(&json);
345         glb.extend((bin.len() as u32).to_le_bytes());
346         glb.extend(b"BIN\0");
347         glb.extend(&bin);
348         let s = read(&glb, None).unwrap();
349         assert_eq!(s.parts[0].mesh.triangles, vec![[0, 1, 2]]);
350     }
351 
352     #[test]
353     fn lines_are_skipped_and_a_missing_buffer_file_is_an_error() {
354         let s = read(&inline(r#"[{"mesh":0}]"#, 1), None).unwrap();
355         assert!(s.parts[0].mesh.triangles.is_empty());
356         let (json, _) = gltf_json(r#"[{"mesh":0}]"#, 4, r#","uri":"nowhere.bin""#);
357         let e = read(json.as_bytes(), Some(Path::new("/nonexistent"))).unwrap_err();
358         assert!(e.contains("nowhere.bin"), "{e}");
359     }
360 
361     #[test]
362     fn a_textured_glb_brings_uvs_normals_and_its_image() {
363         // One triangle with NORMAL and TEXCOORD_0, a 2x2 PNG in a buffer
364         // view, and a node turned a quarter about Y (normals must turn too).
365         let mut png = Vec::new();
366         image::RgbaImage::from_fn(2, 2, |x, _| image::Rgba([if x == 0 { 255 } else { 0 }, 0, 0, 255]))
367             .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
368             .unwrap();
369         let mut bin = Vec::new();
370         for f in [0f32, 0., 0., 1., 0., 0., 0., 1., 0.] {
371             bin.extend(f.to_le_bytes()); // POSITION, 36 bytes
372         }
373         for _ in 0..3 {
374             for f in [0f32, 0., 1.] {
375                 bin.extend(f.to_le_bytes()); // NORMAL +Z, 36 bytes
376             }
377         }
378         for f in [0f32, 0., 1., 0., 0., 1.] {
379             bin.extend(f.to_le_bytes()); // TEXCOORD_0, 24 bytes
380         }
381         let png_at = bin.len();
382         bin.extend(&png);
383         while bin.len() % 4 != 0 {
384             bin.push(0);
385         }
386         let json = format!(
387             r#"{{"asset":{{"version":"2.0"}},"scene":0,"scenes":[{{"nodes":[0]}}],
388             "nodes":[{{"mesh":0,"rotation":[0,0.7071068,0,0.7071068]}}],
389             "meshes":[{{"primitives":[{{"attributes":{{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2}},"material":0}}]}}],
390             "materials":[{{"pbrMetallicRoughness":{{"baseColorTexture":{{"index":0}},"metallicFactor":0.25,"roughnessFactor":0.5}}}}],
391             "textures":[{{"source":0}}],"images":[{{"bufferView":3,"mimeType":"image/png"}}],
392             "accessors":[{{"bufferView":0,"componentType":5126,"count":3,"type":"VEC3","min":[0,0,0],"max":[1,1,0]}},
393                          {{"bufferView":1,"componentType":5126,"count":3,"type":"VEC3"}},
394                          {{"bufferView":2,"componentType":5126,"count":3,"type":"VEC2"}}],
395             "bufferViews":[{{"buffer":0,"byteOffset":0,"byteLength":36}},{{"buffer":0,"byteOffset":36,"byteLength":36}},
396                            {{"buffer":0,"byteOffset":72,"byteLength":24}},{{"buffer":0,"byteOffset":{png_at},"byteLength":{}}}],
397             "buffers":[{{"byteLength":{}}}]}}"#,
398             png.len(),
399             bin.len()
400         );
401         let mut json = json.into_bytes();
402         while json.len() % 4 != 0 {
403             json.push(b' ');
404         }
405         let mut glb = Vec::new();
406         glb.extend(b"glTF");
407         glb.extend(2u32.to_le_bytes());
408         glb.extend(((12 + 8 + json.len() + 8 + bin.len()) as u32).to_le_bytes());
409         glb.extend((json.len() as u32).to_le_bytes());
410         glb.extend(b"JSON");
411         glb.extend(&json);
412         glb.extend((bin.len() as u32).to_le_bytes());
413         glb.extend(b"BIN\0");
414         glb.extend(&bin);
415 
416         let s = read(&glb, None).unwrap();
417         assert_eq!(s.textures.len(), 1);
418         assert_eq!(s.textures[0].sample([0.1, 0.5])[0], 255, "the left column is red");
419         assert_eq!(s.textures[0].sample([0.9, 0.5])[0], 0);
420         let m = &s.parts[0].mesh;
421         let mat = m.material(0);
422         assert_eq!((mat.texture, mat.metallic, mat.roughness), (Some(0), 0.25, 0.5));
423         assert_eq!(mat.color, [1.0; 3], "the factor defaults to white");
424         assert_eq!(m.corner_uvs.as_ref().unwrap()[1], [1.0, 0.0]);
425         let n = m.file_normals.as_ref().unwrap()[0];
426         assert!((n - Vec3::X).length() < 1e-5, "+Z turned a quarter about Y is +X, got {n}");
427     }
428 
429     #[test]
430     fn percent_escapes_are_decoded() {
431         assert_eq!(percent_decode("my%20model.bin"), "my model.bin");
432         assert_eq!(percent_decode("100%"), "100%");
433     }
434 }