1use std::collections::HashMap;
6
7use super::math::{self, V3};
8use super::{Mesh, Model};
9
10pub fn weld(positions: &[V3]) -> Vec<u32> {
14 let mut first: HashMap<[u32; 3], u32> = HashMap::with_capacity(positions.len());
15 positions
16 .iter()
17 .enumerate()
18 .map(|(i, p)| {
19 let key = p.map(|c| if c == 0.0 { 0 } else { c.to_bits() });
21 *first.entry(key).or_insert(i as u32)
22 })
23 .collect()
24}
25
26pub fn face_normal(a: V3, b: V3, c: V3) -> V3 {
29 math::normalize(math::cross(math::sub(b, a), math::sub(c, a)))
30}
31
32fn double_area(a: V3, b: V3, c: V3) -> f32 {
34 math::length(math::cross(math::sub(b, a), math::sub(c, a)))
35}
36
37pub fn smooth_normals(mesh: &Mesh) -> Vec<V3> {
42 if let Some(n) = &mesh.normals {
43 return n.iter().map(|&v| math::normalize(v)).collect();
44 }
45 let welded = weld(&mesh.positions);
46 let mut acc = vec![[0.0f32; 3]; mesh.positions.len()];
47 for t in mesh.indices.as_chunks::<3>().0.iter() {
48 let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
49 let n = math::cross(math::sub(b, a), math::sub(c, a));
51 for &i in t {
52 let w = welded[i as usize] as usize;
53 acc[w] = math::add(acc[w], n);
54 }
55 }
56 welded.iter().map(|&w| math::normalize(acc[w as usize])).collect()
57}
58
59pub fn feature_edges(mesh: &Mesh, threshold_deg: f32) -> Vec<[V3; 2]> {
64 let welded = weld(&mesh.positions);
65 let cos_limit = threshold_deg.to_radians().cos();
66 let mut edges: HashMap<(u32, u32), (V3, u8, bool)> = HashMap::new();
69 let mut order: Vec<(u32, u32)> = Vec::new();
70 for t in mesh.indices.as_chunks::<3>().0.iter() {
71 let w = t.map(|i| welded[i as usize]);
72 let [a, b, c] = w.map(|i| mesh.positions[i as usize]);
73 let n = face_normal(a, b, c);
74 if n == [0.0; 3] {
75 continue;
76 }
77 for (p, q) in [(w[0], w[1]), (w[1], w[2]), (w[2], w[0])] {
78 if p == q {
79 continue;
80 }
81 let key = (p.min(q), p.max(q));
82 match edges.get_mut(&key) {
83 None => {
84 edges.insert(key, (n, 1, false));
85 order.push(key);
86 }
87 Some(e) => {
88 e.1 = e.1.saturating_add(1);
89 if math::dot(e.0, n) <= cos_limit {
90 e.2 = true;
91 }
92 }
93 }
94 }
95 }
96 order
97 .into_iter()
98 .filter(|k| edges[k].1 == 1 || edges[k].2)
99 .map(|(p, q)| [mesh.positions[p as usize], mesh.positions[q as usize]])
100 .collect()
101}
102
103pub fn is_closed(mesh: &Mesh) -> bool {
107 let welded = weld(&mesh.positions);
108 let mut directed: HashMap<(u32, u32), u32> = HashMap::new();
109 for t in mesh.indices.as_chunks::<3>().0.iter() {
110 let w = t.map(|i| welded[i as usize]);
111 for (p, q) in [(w[0], w[1]), (w[1], w[2]), (w[2], w[0])] {
112 *directed.entry((p, q)).or_default() += 1;
113 }
114 }
115 !directed.is_empty() && directed.iter().all(|(&(p, q), &n)| n == 1 && directed.get(&(q, p)) == Some(&1))
116}
117
118#[derive(Debug, Clone, PartialEq)]
120pub struct Stats {
121 pub vertices: usize,
122 pub triangles: usize,
123 pub size: V3,
125 pub volume: Option<f64>,
127 pub surface: f64,
129}
130
131pub fn stats(model: &Model) -> Stats {
132 let size = model.bounds().map_or([0.0; 3], |(lo, hi)| math::sub(hi, lo));
133 let mut surface = 0.0f64;
134 let mut volume = 0.0f64;
135 let mut closed = !model.meshes.is_empty();
136 for m in &model.meshes {
137 closed &= is_closed(m);
138 for t in m.indices.as_chunks::<3>().0.iter() {
139 let [a, b, c] = t.map(|i| m.positions[i as usize]);
140 surface += f64::from(double_area(a, b, c)) / 2.0;
141 let [a, b, c] = [a, b, c].map(|v| v.map(f64::from));
143 volume += (a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0]) + a[2] * (b[0] * c[1] - b[1] * c[0])) / 6.0;
144 }
145 }
146 Stats { vertices: model.vertex_count(), triangles: model.triangle_count(), size, volume: closed.then_some(volume.abs()), surface }
147}
148
149pub fn shade(base: V3, n: V3, to_eye: V3) -> V3 {
156 let d = math::dot(n, to_eye).abs();
157 let spec = SPECULAR * d.powi(SHININESS);
158 base.map(|c| (c * (AMBIENT + DIFFUSE * d) + spec).min(1.0))
159}
160
161pub fn normal_mapped(n: V3, q0: V3, q1: V3, st0: [f32; 2], st1: [f32; 2], texel: V3) -> V3 {
169 let (p1, p0) = (math::cross(q1, n), math::cross(n, q0));
170 let t = math::add(math::scale(p1, st0[0]), math::scale(p0, st1[0]));
171 let b = math::add(math::scale(p1, st0[1]), math::scale(p0, st1[1]));
172 let det = math::dot(t, t).max(math::dot(b, b));
173 if !(det.is_finite() && det > 0.0) {
174 return n;
175 }
176 let k = det.sqrt().recip();
177 let (x, y, z) = (texel[0] * 2.0 - 1.0, texel[1] * 2.0 - 1.0, texel[2] * 2.0 - 1.0);
178 let tilted = math::add(math::add(math::scale(t, x * k), math::scale(b, -y * k)), math::scale(n, z));
180 let out = math::normalize(tilted);
181 if out == [0.0; 3] { n } else { out }
182}
183
184pub fn bumped(n: V3, q0: V3, q1: V3, st0: [f32; 2], st1: [f32; 2], dh: [f32; 2]) -> V3 {
189 let (p1, p0) = (math::cross(q1, n), math::cross(n, q0));
190 let det = math::dot(q0, p1);
191 if !(det.is_finite() && det != 0.0) {
192 return n;
193 }
194 let du = math::add(math::scale(p1, st0[0] / det), math::scale(p0, st1[0] / det));
196 let dv = math::add(math::scale(p1, st0[1] / det), math::scale(p0, st1[1] / det));
197 let slope = math::add(math::scale(du, dh[0]), math::scale(dv, dh[1]));
198 let out = math::normalize(math::sub(n, slope));
199 if out == [0.0; 3] || !out.iter().all(|c| c.is_finite()) { n } else { out }
200}
201
202pub fn add_light(lit: V3, emissive: V3) -> V3 {
205 std::array::from_fn(|k| (lit[k] + emissive[k]).min(1.0))
206}
207
208pub fn center(mesh: &Mesh) -> Option<V3> {
210 let mut b: Option<(V3, V3)> = None;
211 for &i in &mesh.indices {
212 let p = *mesh.positions.get(i as usize)?;
213 if !p.iter().all(|c| c.is_finite()) {
214 continue;
215 }
216 let (lo, hi) = b.get_or_insert((p, p));
217 for k in 0..3 {
218 lo[k] = lo[k].min(p[k]);
219 hi[k] = hi[k].max(p[k]);
220 }
221 }
222 b.map(|(lo, hi)| math::scale(math::add(lo, hi), 0.5))
223}
224
225pub fn back_to_front(centers: &[V3], vp: &math::M4) -> Vec<usize> {
228 let depth = |p: V3| {
229 let v = [p[0], p[1], p[2], 1.0];
230 let z: f32 = (0..4).map(|k| vp[k][2] * v[k]).sum();
231 let w: f32 = (0..4).map(|k| vp[k][3] * v[k]).sum();
232 if w > 0.0 { z / w } else { f32::NEG_INFINITY }
235 };
236 let keyed: Vec<f32> = centers.iter().map(|&c| depth(c)).collect();
237 let mut order: Vec<usize> = (0..centers.len()).collect();
238 order.sort_by(|&a, &b| keyed[b].total_cmp(&keyed[a]));
239 order
240}
241
242pub const AMBIENT: f32 = 136.0 / 255.0;
244pub const DIFFUSE: f32 = 136.0 / 255.0;
245pub const SPECULAR: f32 = 0.045;
247pub const SHININESS: i32 = 30;
248
249#[cfg(test)]
250pub(crate) mod tests {
251 use super::*;
252
253 #[test]
256 fn normal_maps_tilt_along_the_picture() {
257 let n = [0.0, 0.0, 1.0];
258 let (q0, q1, st0, st1) = ([2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [1.0, 0.0], [0.0, -1.0]);
260 let flat = normal_mapped(n, q0, q1, st0, st1, [0.5, 0.5, 1.0]);
261 assert!(math::length(math::sub(flat, n)) < 1e-6, "{flat:?}");
262 let right = normal_mapped(n, q0, q1, st0, st1, [1.0, 0.5, 0.5]);
263 assert!(right[0] > 0.99 && right[1].abs() < 1e-5, "{right:?}");
264 let up = normal_mapped(n, q0, q1, st0, st1, [0.5, 1.0, 0.5]);
265 assert!(up[1] > 0.99 && up[0].abs() < 1e-5, "{up:?}");
266 let other = normal_mapped(n, [1.0, 1.0, 0.0], [0.0, 3.0, 0.0], [0.5, -0.5], [0.0, -1.5], [1.0, 0.5, 0.5]);
268 assert!(math::length(math::sub(other, right)) < 1e-5, "{other:?}");
269 assert_eq!(normal_mapped(n, q0, q1, [0.0; 2], [0.0; 2], [1.0, 0.5, 0.5]), n);
271 }
272
273 #[test]
275 fn bumps_lean_away_from_rising_height() {
276 let n = [0.0, 0.0, 1.0];
277 let (q0, q1, st0, st1) = ([2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [1.0, 0.0], [0.0, -1.0]);
278 assert_eq!(bumped(n, q0, q1, st0, st1, [0.0, 0.0]), n);
279 let b = bumped(n, q0, q1, st0, st1, [2.0, 0.0]);
281 assert!((b[0] + 0.5f32.sqrt()).abs() < 1e-5 && (b[2] - 0.5f32.sqrt()).abs() < 1e-5, "{b:?}");
282 let b = bumped(n, q0, q1, st0, st1, [0.0, 2.0]);
284 assert!(b[1] > 0.7, "{b:?}");
285 assert_eq!(bumped(n, [0.0; 3], q1, st0, st1, [1.0, 1.0]), n);
287 }
288
289 #[test]
290 fn see_through_parts_sort_far_to_near() {
291 let view = math::look_at([0.0, 0.0, 5.0], [0.0; 3], [0.0, 1.0, 0.0]);
292 let proj = math::perspective(1.0, 1.0, 0.1, 100.0);
293 let vp = math::mul(&proj, &view);
294 assert_eq!(back_to_front(&[[0.0, 0.0, 1.0], [0.0, 0.0, -3.0], [0.0, 0.0, 9.0], [0.0, 0.0, 0.0]], &vp), [1, 3, 0, 2]);
295 }
296
297 #[test]
298 fn centers_use_the_vertices_of_triangles() {
299 let m = Mesh { positions: vec![[0.0; 3], [2.0, 0.0, 0.0], [0.0, 4.0, 0.0], [100.0; 3]], indices: vec![0, 1, 2], ..Default::default() };
300 assert_eq!(center(&m), Some([1.0, 2.0, 0.0]));
301 assert_eq!(center(&Mesh::default()), None);
302 }
303
304 #[test]
305 fn own_light_adds_and_clamps() {
306 assert_eq!(add_light([0.25, 0.5, 0.75], [0.25, 0.75, 0.0]), [0.5, 1.0, 0.75]);
307 }
308
309 pub(crate) fn cube(split: bool) -> Mesh {
312 let corners: [V3; 8] =
313 [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.], [0., 0., 1.], [1., 0., 1.], [1., 1., 1.], [0., 1., 1.]];
314 let quads = [[0, 3, 2, 1], [4, 5, 6, 7], [0, 1, 5, 4], [2, 3, 7, 6], [1, 2, 6, 5], [0, 4, 7, 3]];
315 let tris: Vec<u32> = quads.iter().flat_map(|q| [q[0], q[1], q[2], q[0], q[2], q[3]]).collect();
316 if split {
317 Mesh { positions: tris.iter().map(|&i| corners[i as usize]).collect(), indices: (0..36).collect(), ..Default::default() }
318 } else {
319 Mesh { positions: corners.to_vec(), indices: tris, ..Default::default() }
320 }
321 }
322
323 #[test]
324 fn welding_joins_repeated_corners() {
325 let w = weld(&[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
326 assert_eq!(w, vec![0, 1, 0, 1]);
327 }
328
329 #[test]
330 fn cube_measures() {
331 for split in [false, true] {
332 let model = Model { meshes: vec![cube(split)] };
333 let s = stats(&model);
334 assert_eq!(s.triangles, 12);
335 assert_eq!(s.size, [1.0, 1.0, 1.0]);
336 assert!((s.surface - 6.0).abs() < 1e-9);
337 assert!((s.volume.unwrap() - 1.0).abs() < 1e-9, "closed when split too");
338 }
339 let mut open = cube(false);
341 open.indices.truncate(30);
342 assert_eq!(stats(&Model { meshes: vec![open] }).volume, None);
343 }
344
345 #[test]
346 fn cube_edges_skip_the_face_diagonals() {
347 assert_eq!(feature_edges(&cube(true), 1.0).len(), 12);
349 let sq = Mesh {
351 positions: vec![[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.]],
352 indices: vec![0, 1, 2, 0, 2, 3],
353 ..Default::default()
354 };
355 assert_eq!(feature_edges(&sq, 1.0).len(), 4);
356 assert_eq!(feature_edges(&cube(false), 89.0).len(), 12);
359 assert_eq!(feature_edges(&cube(false), 91.0).len(), 0);
360 }
361
362 #[test]
363 fn smooth_normals_point_out_of_the_corners() {
364 let n = smooth_normals(&cube(true));
365 let k = -1.0 / 3f32.sqrt();
367 assert!(n[0].iter().all(|c| (c - k).abs() < 1e-5), "{:?}", n[0]);
368 let mut m = cube(false);
370 m.normals = Some(vec![[0.0, 2.0, 0.0]; 8]);
371 assert_eq!(smooth_normals(&m)[3], [0.0, 1.0, 0.0]);
372 }
373
374 #[test]
375 fn shading_is_two_sided_and_brightest_head_on() {
376 let base = super::super::DEFAULT_COLOR;
377 let head_on = shade(base, [0.0, 0.0, 1.0], [0.0, 0.0, 1.0]);
378 let back = shade(base, [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]);
379 let grazing = shade(base, [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
380 assert_eq!(head_on, back);
381 assert!(head_on[0] > grazing[0]);
382 assert!((grazing[0] - base[0] * AMBIENT).abs() < 1e-6);
384 assert!(head_on[0] <= 1.0);
385 }
386
387 #[test]
388 fn model_finish_drops_empty_meshes_and_bad_indices() {
389 let mut bad = cube(false);
390 bad.indices.extend([0, 1, 99]);
391 bad.colors = Some(vec![[1.0; 3]; 3]);
392 let m = Model { meshes: vec![bad, Mesh::default()] }.finish().unwrap();
393 assert_eq!(m.meshes.len(), 1);
394 assert_eq!(m.triangle_count(), 12);
395 assert_eq!(m.meshes[0].colors, None);
396 assert!(Model::default().finish().is_err());
397 let (c, r) = m.bounding_sphere().unwrap();
398 assert_eq!(c, [0.5, 0.5, 0.5]);
399 assert!((r - 0.75f32.sqrt()).abs() < 1e-6);
400 }
401}