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/mesh.rs (16.9K)

  1 //! The geometry every reader produces: a triangle mesh with a material per
  2 //! triangle and, when the file has them, a colour, a texture coordinate and a
  3 //! normal per corner.
  4 //!
  5 //! ## Why meshes are welded
  6 //!
  7 //! An STL has no shared points: each triangle carries its own corners. A
  8 //! smooth normal is the average of the faces meeting at a point, which needs
  9 //! those faces to name the same point, so [`crate::load`] welds every part by
 10 //! position. Colours, texture coordinates and the file's own normals ride on
 11 //! CORNERS, not points, so welding never has to choose between two of them
 12 //! meeting at one place (a texture seam, a hard edge).
 13 
 14 use std::collections::HashMap;
 15 
 16 use glam::Vec3;
 17 
 18 /// The colour of a surface that names none (an STL; an OBJ without a
 19 /// material), linear RGB: a warm clay.
 20 pub const CLAY: [f32; 3] = [0.42, 0.40, 0.36];
 21 
 22 /// How a surface looks: glTF's metallic-roughness model, which the other
 23 /// formats map onto (an MTL's `Kd` is the colour, its `Ns` a roughness).
 24 #[derive(Debug, Clone, Copy, PartialEq)]
 25 pub struct Material {
 26     /// Linear RGB, multiplying the texture when there is one.
 27     pub color: [f32; 3],
 28     /// An index into the scene's `textures`.
 29     pub texture: Option<usize>,
 30     /// 0 a dielectric, 1 a metal.
 31     pub metallic: f32,
 32     /// 0 a mirror, 1 fully rough.
 33     pub roughness: f32,
 34 }
 35 
 36 impl Default for Material {
 37     fn default() -> Self {
 38         Self { color: CLAY, texture: None, metallic: 0.0, roughness: 0.8 }
 39     }
 40 }
 41 
 42 impl Material {
 43     /// A plain surface of this colour.
 44     pub fn colour(color: [f32; 3]) -> Self {
 45         Self { color, ..Self::default() }
 46     }
 47 }
 48 
 49 #[derive(Debug, Clone, Default)]
 50 pub struct Mesh {
 51     pub positions: Vec<Vec3>,
 52     /// Counter-clockwise seen from outside.
 53     pub triangles: Vec<[u32; 3]>,
 54     /// One per triangle: an index into `materials`.
 55     pub tri_material: Vec<u32>,
 56     pub materials: Vec<Material>,
 57     /// Three per triangle, in `triangles` order, linear RGB, when the file
 58     /// colours its points (PLY, glTF `COLOR_0`). They multiply the
 59     /// material's colour (and replace it in [`Mesh::corner_color`]).
 60     pub corner_colors: Option<Vec<[f32; 3]>>,
 61     /// Three per triangle, when the file has texture coordinates. Image
 62     /// convention: (0, 0) the top-left texel (an OBJ `vt` is flipped in v
 63     /// on the way in). They may run past 0..1: textures repeat.
 64     pub corner_uvs: Option<Vec<[f32; 2]>>,
 65     /// Three per triangle, unit length, when the file brings its own
 66     /// normals; a caller without them derives them ([`Mesh::corner_normals`]).
 67     pub file_normals: Option<Vec<Vec3>>,
 68 }
 69 
 70 impl Mesh {
 71     /// The axis-aligned box around every point, or `None` for no points.
 72     pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
 73         let first = *self.positions.first()?;
 74         Some(self.positions.iter().fold((first, first), |(lo, hi), p| (lo.min(*p), hi.max(*p))))
 75     }
 76 
 77     /// The material of triangle `t`.
 78     pub fn material(&self, t: usize) -> Material {
 79         self.materials.get(self.tri_material[t] as usize).copied().unwrap_or_default()
 80     }
 81 
 82     /// The colour of corner `k` (0..3) of triangle `t`, without its texture:
 83     /// the corner's own colour if the file gives one, else its material's.
 84     pub fn corner_color(&self, t: usize, k: usize) -> [f32; 3] {
 85         if let Some(c) = &self.corner_colors {
 86             return c[t * 3 + k];
 87         }
 88         self.material(t).color
 89     }
 90 
 91     /// Add `other`'s triangles to this mesh, its points and materials after
 92     /// this one's (texture indices are the scene's, so they are kept).
 93     /// Corner colours survive: if either side has them, the other's
 94     /// triangles are given their material colours as corner colours. Corner
 95     /// texture coordinates survive likewise, (0, 0) where a side had none;
 96     /// the file's normals survive only if both sides have them.
 97     pub fn append(&mut self, other: &Mesh) {
 98         let base = self.positions.len() as u32;
 99         let first = self.materials.len() as u32;
100         if self.corner_colors.is_some() || other.corner_colors.is_some() {
101             let mine = self.expanded_corner_colors();
102             let theirs = other.expanded_corner_colors();
103             self.corner_colors = Some(mine.into_iter().chain(theirs).collect());
104         }
105         if self.corner_uvs.is_some() || other.corner_uvs.is_some() {
106             let uvs = |m: &Mesh| m.corner_uvs.clone().unwrap_or_else(|| vec![[0.0; 2]; m.triangles.len() * 3]);
107             let mut mine = uvs(self);
108             mine.extend(uvs(other));
109             self.corner_uvs = Some(mine);
110         }
111         let empty = self.triangles.is_empty();
112         self.file_normals = match (self.file_normals.take(), &other.file_normals) {
113             (Some(mut mine), Some(theirs)) => {
114                 mine.extend_from_slice(theirs);
115                 Some(mine)
116             }
117             (None, Some(theirs)) if empty => Some(theirs.clone()),
118             _ => None,
119         };
120         self.positions.extend_from_slice(&other.positions);
121         self.triangles.extend(other.triangles.iter().map(|t| t.map(|i| i + base)));
122         self.tri_material.extend(other.tri_material.iter().map(|c| c + first));
123         self.materials.extend_from_slice(&other.materials);
124     }
125 
126     fn expanded_corner_colors(&self) -> Vec<[f32; 3]> {
127         if let Some(c) = &self.corner_colors {
128             return c.clone();
129         }
130         (0..self.triangles.len()).flat_map(|t| [0, 1, 2].map(|k| self.corner_color(t, k))).collect()
131     }
132 
133     /// Turn a Z-up mesh Y-up: (x, y, z) → (x, z, −y). A rotation, not a
134     /// mirror, so the winding and with it the outward side of every face are
135     /// kept.
136     pub fn z_up_to_y_up(&mut self) {
137         let turn = |p: &mut Vec3| *p = Vec3::new(p.x, p.z, -p.y);
138         self.positions.iter_mut().for_each(turn);
139         if let Some(n) = &mut self.file_normals {
140             n.iter_mut().for_each(turn);
141         }
142     }
143 
144     /// Move and scale the mesh so its bounding box is centred on the origin
145     /// and its farthest point is 1 from it, and return the (centre, radius)
146     /// it had, so its real size can still be reported. The radius is the
147     /// farthest point's, not the box's half-diagonal: a sphere's box corners
148     /// sit 1.7 times farther out than any of its points.
149     pub fn fit_to_unit(&mut self) -> (Vec3, f32) {
150         let Some((lo, hi)) = self.bounds() else { return (Vec3::ZERO, 1.0) };
151         let centre = (lo + hi) / 2.0;
152         let radius = self.positions.iter().map(|p| p.distance(centre)).fold(0.0, f32::max).max(f32::MIN_POSITIVE);
153         for p in &mut self.positions {
154             *p = (*p - centre) / radius;
155         }
156         (centre, radius)
157     }
158 
159     /// Merge points that sit at the same place (to a millionth of the
160     /// mesh's size) and drop the triangles that collapse doing so.
161     pub fn weld(&mut self) {
162         let Some((lo, hi)) = self.bounds() else { return };
163         let cell = ((hi - lo).length() * 1e-6).max(f32::MIN_POSITIVE);
164         let key = |p: Vec3| {
165             let q = (p - lo) / cell;
166             [q.x.round() as i64, q.y.round() as i64, q.z.round() as i64]
167         };
168         // Sized for a closed mesh's usual one point per six corners: an STL
169         // of millions of triangles would otherwise rehash its way up.
170         let mut index: HashMap<[i64; 3], u32> = HashMap::with_capacity(self.positions.len() / 4);
171         let mut positions = Vec::with_capacity(self.positions.len() / 4);
172         let remap: Vec<u32> = self
173             .positions
174             .iter()
175             .map(|p| {
176                 *index.entry(key(*p)).or_insert_with(|| {
177                     positions.push(*p);
178                     (positions.len() - 1) as u32
179                 })
180             })
181             .collect();
182         drop(index);
183         let mut triangles = Vec::with_capacity(self.triangles.len());
184         let mut tri_material = Vec::with_capacity(self.triangles.len());
185         let mut kept = Vec::with_capacity(self.triangles.len());
186         for (t, tri) in self.triangles.iter().enumerate() {
187             let [a, b, c] = tri.map(|i| remap[i as usize]);
188             if a != b && b != c && a != c {
189                 triangles.push([a, b, c]);
190                 tri_material.push(self.tri_material[t]);
191                 kept.push(t);
192             }
193         }
194         // The corner attributes of the triangles that stayed.
195         fn keep<T: Copy>(src: &Option<Vec<T>>, kept: &[usize]) -> Option<Vec<T>> {
196             src.as_ref().map(|s| kept.iter().flat_map(|&t| [s[t * 3], s[t * 3 + 1], s[t * 3 + 2]]).collect())
197         }
198         let corner_colors = keep(&self.corner_colors, &kept);
199         let corner_uvs = keep(&self.corner_uvs, &kept);
200         let file_normals = keep(&self.file_normals, &kept);
201         positions.shrink_to_fit();
202         self.positions = positions;
203         self.triangles = triangles;
204         self.tri_material = tri_material;
205         self.corner_colors = corner_colors;
206         self.corner_uvs = corner_uvs;
207         self.file_normals = file_normals;
208     }
209 
210     /// A normal for each triangle corner (three per triangle, in order):
211     /// the area-weighted average of the faces at that point which meet this
212     /// triangle within `crease_degrees`, so a cube keeps its edges and a
213     /// sphere's facets blend.
214     pub fn corner_normals(&self, crease_degrees: f32) -> Vec<Vec3> {
215         let crease_cos = crease_degrees.to_radians().cos();
216         // The cross product's length is twice the area, so summing raw
217         // crosses weights each face by its area.
218         let cross: Vec<Vec3> = self
219             .triangles
220             .iter()
221             .map(|t| {
222                 let [a, b, c] = t.map(|i| self.positions[i as usize]);
223                 (b - a).cross(c - a)
224             })
225             .collect();
226         let unit: Vec<Vec3> = cross.iter().map(|n| n.normalize_or_zero()).collect();
227 
228         // Faces at each point, as one flat list with offsets.
229         let mut start = vec![0u32; self.positions.len() + 1];
230         for t in &self.triangles {
231             for &v in t {
232                 start[v as usize + 1] += 1;
233             }
234         }
235         for i in 1..start.len() {
236             start[i] += start[i - 1];
237         }
238         let mut fill = start.clone();
239         let mut faces = vec![0u32; self.triangles.len() * 3];
240         for (f, t) in self.triangles.iter().enumerate() {
241             for &v in t {
242                 faces[fill[v as usize] as usize] = f as u32;
243                 fill[v as usize] += 1;
244             }
245         }
246 
247         let mut out = Vec::with_capacity(self.triangles.len() * 3);
248         for (f, t) in self.triangles.iter().enumerate() {
249             for &v in t {
250                 let around = &faces[start[v as usize] as usize..start[v as usize + 1] as usize];
251                 let sum: Vec3 = around
252                     .iter()
253                     .filter(|&&g| unit[g as usize].dot(unit[f]) >= crease_cos)
254                     .map(|&g| cross[g as usize])
255                     .sum();
256                 out.push(sum.try_normalize().unwrap_or(unit[f]));
257             }
258         }
259         out
260     }
261 }
262 
263 /// An 8-bit sRGB channel (as PLY colours are stored) as linear 0..1.
264 pub fn srgb_to_linear(c: u8) -> f32 {
265     let c = c as f32 / 255.0;
266     if c <= 0.04045 {
267         c / 12.92
268     } else {
269         ((c + 0.055) / 1.055).powf(2.4)
270     }
271 }
272 
273 #[cfg(test)]
274 pub(crate) mod tests {
275     use super::*;
276 
277     /// A unit cube as an STL would carry it: twelve triangles, every corner
278     /// its own point.
279     pub(crate) fn soup_cube() -> Mesh {
280         let c = |x: f32, y: f32, z: f32| Vec3::new(x, y, z);
281         let quads = [
282             [c(1., 0., 0.), c(1., 1., 0.), c(1., 1., 1.), c(1., 0., 1.)],
283             [c(0., 0., 1.), c(0., 1., 1.), c(0., 1., 0.), c(0., 0., 0.)],
284             [c(0., 1., 0.), c(0., 1., 1.), c(1., 1., 1.), c(1., 1., 0.)],
285             [c(0., 0., 1.), c(0., 0., 0.), c(1., 0., 0.), c(1., 0., 1.)],
286             [c(0., 0., 1.), c(1., 0., 1.), c(1., 1., 1.), c(0., 1., 1.)],
287             [c(1., 0., 0.), c(0., 0., 0.), c(0., 1., 0.), c(1., 1., 0.)],
288         ];
289         let mut m = Mesh::default();
290         for q in quads {
291             for i in [0, 1, 2, 0, 2, 3] {
292                 m.positions.push(q[i]);
293             }
294         }
295         m.triangles = (0..12).map(|t| [t * 3, t * 3 + 1, t * 3 + 2]).collect();
296         m.tri_material = vec![0; 12];
297         m.materials = vec![Material::default()];
298         m
299     }
300 
301     #[test]
302     fn welding_a_cube_leaves_its_eight_corners() {
303         let mut m = soup_cube();
304         assert_eq!(m.positions.len(), 36);
305         m.weld();
306         assert_eq!(m.positions.len(), 8);
307         assert_eq!(m.triangles.len(), 12);
308     }
309 
310     #[test]
311     fn a_cube_keeps_its_edges_and_its_faces_point_out() {
312         let mut m = soup_cube();
313         m.weld();
314         let normals = m.corner_normals(40.0);
315         let (lo, hi) = m.bounds().unwrap();
316         let center = (lo + hi) / 2.0;
317         for (f, t) in m.triangles.iter().enumerate() {
318             let [a, b, c] = t.map(|i| m.positions[i as usize]);
319             let face = (b - a).cross(c - a).normalize();
320             assert!(face.dot((a + b + c) / 3.0 - center) > 0.0, "triangle {f} faces inward");
321             for k in 0..3 {
322                 assert!(normals[f * 3 + k].dot(face) > 0.999, "triangle {f} corner {k} was smoothed");
323             }
324         }
325     }
326 
327     #[test]
328     fn a_shallow_fold_is_smoothed() {
329         let mut m = Mesh::default();
330         let lift = 20f32.to_radians().tan();
331         m.positions = vec![Vec3::ZERO, Vec3::Z, Vec3::new(-1.0, 0.0, 0.5), Vec3::new(1.0, lift, 0.5)];
332         m.triangles = vec![[0, 1, 2], [0, 3, 1]];
333         m.tri_material = vec![0, 0];
334         let n = m.corner_normals(40.0);
335         assert!(n[0].dot(n[3]) > 0.9999, "the shared point has two normals");
336     }
337 
338     #[test]
339     fn welding_drops_a_collapsed_triangle_and_its_corner_colours() {
340         let mut m = Mesh::default();
341         m.positions = vec![Vec3::ZERO, Vec3::X, Vec3::Y, Vec3::X, Vec3::X * (1.0 + 1e-9), Vec3::Y];
342         m.triangles = vec![[0, 1, 2], [3, 4, 5]];
343         m.tri_material = vec![0, 0];
344         m.corner_colors = Some(vec![[1.0, 0.0, 0.0]; 3].into_iter().chain(vec![[0.0, 1.0, 0.0]; 3]).collect());
345         m.weld();
346         assert_eq!(m.triangles, vec![[0, 1, 2]]);
347         assert_eq!(m.corner_colors.unwrap(), vec![[1.0, 0.0, 0.0]; 3]);
348     }
349 
350     #[test]
351     fn a_fitted_mesh_lies_in_the_unit_sphere() {
352         let mut m = soup_cube();
353         for p in &mut m.positions {
354             *p = *p * 25.0 + Vec3::new(-12.5, -12.5, 0.0);
355         }
356         let (centre, radius) = m.fit_to_unit();
357         assert_eq!(centre, Vec3::new(0.0, 0.0, 12.5));
358         assert!((radius - 25.0 * 3f32.sqrt() / 2.0).abs() < 1e-3, "a cube's corners are its farthest points");
359         assert!(m.positions.iter().all(|p| p.length() <= 1.0 + 1e-5));
360     }
361 
362     #[test]
363     fn z_up_becomes_y_up_without_turning_faces_inside_out() {
364         let mut m = soup_cube();
365         m.z_up_to_y_up();
366         let mut w = m.clone();
367         w.weld();
368         let (lo, hi) = w.bounds().unwrap();
369         assert_eq!((lo, hi), (Vec3::new(0.0, 0.0, -1.0), Vec3::new(1.0, 1.0, 0.0)));
370         let centre = (lo + hi) / 2.0;
371         for t in &w.triangles {
372             let [a, b, c] = t.map(|i| w.positions[i as usize]);
373             assert!((b - a).cross(c - a).dot((a + b + c) / 3.0 - centre) > 0.0);
374         }
375     }
376 
377     #[test]
378     fn appending_keeps_colours_on_both_sides() {
379         let mut a = soup_cube();
380         let mut b = soup_cube();
381         b.materials = vec![Material::colour([0.0, 0.0, 1.0])];
382         b.corner_colors = Some(vec![[0.0, 1.0, 0.0]; 36]);
383         a.append(&b);
384         assert_eq!(a.triangles.len(), 24);
385         assert_eq!(a.triangles[12], [36, 37, 38]);
386         assert_eq!(a.corner_color(0, 0), CLAY, "the first mesh's material colour, as a corner colour");
387         assert_eq!(a.corner_color(12, 0), [0.0, 1.0, 0.0]);
388     }
389 
390     #[test]
391     fn welding_keeps_each_kept_triangles_uvs_and_normals() {
392         let mut m = Mesh::default();
393         m.positions = vec![Vec3::ZERO, Vec3::X, Vec3::Y, Vec3::X, Vec3::X * (1.0 + 1e-9), Vec3::Y, Vec3::Z, Vec3::X, Vec3::Y];
394         m.triangles = vec![[0, 1, 2], [3, 4, 5], [6, 7, 8]];
395         m.tri_material = vec![0; 3];
396         m.corner_uvs = Some((0..9).map(|i| [i as f32, 0.0]).collect());
397         m.file_normals = Some((0..9).map(|i| Vec3::splat(i as f32)).collect());
398         m.weld();
399         assert_eq!(m.triangles.len(), 2, "the middle one collapsed");
400         assert_eq!(m.corner_uvs.unwrap()[3], [6.0, 0.0], "the third triangle's uvs moved up");
401         assert_eq!(m.file_normals.unwrap()[3], Vec3::splat(6.0));
402     }
403 
404     #[test]
405     fn appending_keeps_uvs_and_drops_normals_one_side_lacks() {
406         let mut a = soup_cube();
407         a.corner_uvs = Some(vec![[1.0, 1.0]; 36]);
408         a.file_normals = Some(vec![Vec3::Y; 36]);
409         a.append(&soup_cube());
410         assert_eq!(a.corner_uvs.as_ref().unwrap()[36], [0.0, 0.0]);
411         assert_eq!(a.corner_uvs.unwrap().len(), 72);
412         assert!(a.file_normals.is_none());
413     }
414 
415     #[test]
416     fn srgb_round_numbers() {
417         assert_eq!(srgb_to_linear(0), 0.0);
418         assert!((srgb_to_linear(255) - 1.0).abs() < 1e-6);
419         assert!((srgb_to_linear(188) - 0.5).abs() < 0.01);
420     }
421 }