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 }