Skip to main content

oxihuman_mesh/
mesh_diff.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(dead_code)]
5
6use crate::heat_map::{scalars_to_colors_range, ColorRamp};
7use crate::mesh::MeshBuffers;
8use crate::normals::compute_normals;
9
10/// Per-vertex displacement between two meshes.
11pub struct DisplacementField {
12    /// Displacement vectors: new_pos - old_pos for each vertex.
13    pub deltas: Vec<[f32; 3]>,
14    /// Scalar magnitudes of each displacement.
15    pub magnitudes: Vec<f32>,
16    /// Max displacement magnitude.
17    pub max_magnitude: f32,
18    /// Mean displacement magnitude.
19    pub mean_magnitude: f32,
20    /// RMS displacement.
21    pub rms: f32,
22}
23
24impl DisplacementField {
25    /// Compute a displacement field between `base` and `deformed` meshes.
26    ///
27    /// Returns an error if the vertex counts differ.
28    pub fn compute(base: &MeshBuffers, deformed: &MeshBuffers) -> anyhow::Result<Self> {
29        compute_displacement(base, deformed)
30    }
31
32    /// Number of vertices in this displacement field.
33    pub fn vertex_count(&self) -> usize {
34        self.deltas.len()
35    }
36
37    /// True when every displacement magnitude is effectively zero.
38    pub fn is_zero(&self) -> bool {
39        self.max_magnitude < f32::EPSILON
40    }
41
42    /// Return indices of the top `n` vertices with the largest displacement,
43    /// sorted descending by magnitude.
44    pub fn top_displaced(&self, n: usize) -> Vec<usize> {
45        let mut indices: Vec<usize> = (0..self.magnitudes.len()).collect();
46        indices.sort_unstable_by(|&a, &b| {
47            self.magnitudes[b]
48                .partial_cmp(&self.magnitudes[a])
49                .unwrap_or(std::cmp::Ordering::Equal)
50        });
51        indices.truncate(n);
52        indices
53    }
54
55    /// Return a new field with all deltas scaled by `factor`.
56    pub fn scale(&self, factor: f32) -> Self {
57        let deltas: Vec<[f32; 3]> = self
58            .deltas
59            .iter()
60            .map(|d| [d[0] * factor, d[1] * factor, d[2] * factor])
61            .collect();
62        let magnitudes: Vec<f32> = deltas
63            .iter()
64            .map(|d| (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt())
65            .collect();
66        let max_magnitude = magnitudes.iter().cloned().fold(0.0f32, f32::max);
67        let mean_magnitude = if magnitudes.is_empty() {
68            0.0
69        } else {
70            magnitudes.iter().sum::<f32>() / magnitudes.len() as f32
71        };
72        let rms = if magnitudes.is_empty() {
73            0.0
74        } else {
75            let mean_sq = magnitudes.iter().map(|m| m * m).sum::<f32>() / magnitudes.len() as f32;
76            mean_sq.sqrt()
77        };
78        DisplacementField {
79            deltas,
80            magnitudes,
81            max_magnitude,
82            mean_magnitude,
83            rms,
84        }
85    }
86
87    /// Apply this displacement field to `mesh`, returning a new mesh with
88    /// updated positions (and recomputed normals).
89    pub fn apply_to(&self, mesh: &MeshBuffers) -> MeshBuffers {
90        let count = mesh.positions.len().min(self.deltas.len());
91        let mut new_positions = mesh.positions.clone();
92        for (p, d) in new_positions.iter_mut().zip(self.deltas.iter()).take(count) {
93            p[0] += d[0];
94            p[1] += d[1];
95            p[2] += d[2];
96        }
97        let mut result = MeshBuffers {
98            positions: new_positions,
99            normals: mesh.normals.clone(),
100            tangents: mesh.tangents.clone(),
101            uvs: mesh.uvs.clone(),
102            indices: mesh.indices.clone(),
103            colors: mesh.colors.clone(),
104            has_suit: mesh.has_suit,
105        };
106        compute_normals(&mut result);
107        result
108    }
109
110    /// Return a new field where deltas with magnitude below `min_magnitude`
111    /// are zeroed out.
112    pub fn threshold(&self, min_magnitude: f32) -> Self {
113        let deltas: Vec<[f32; 3]> = self
114            .deltas
115            .iter()
116            .zip(self.magnitudes.iter())
117            .map(|(d, &mag)| {
118                if mag < min_magnitude {
119                    [0.0, 0.0, 0.0]
120                } else {
121                    *d
122                }
123            })
124            .collect();
125        let magnitudes: Vec<f32> = deltas
126            .iter()
127            .map(|d| (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt())
128            .collect();
129        let max_magnitude = magnitudes.iter().cloned().fold(0.0f32, f32::max);
130        let mean_magnitude = if magnitudes.is_empty() {
131            0.0
132        } else {
133            magnitudes.iter().sum::<f32>() / magnitudes.len() as f32
134        };
135        let rms = if magnitudes.is_empty() {
136            0.0
137        } else {
138            let mean_sq = magnitudes.iter().map(|m| m * m).sum::<f32>() / magnitudes.len() as f32;
139            mean_sq.sqrt()
140        };
141        DisplacementField {
142            deltas,
143            magnitudes,
144            max_magnitude,
145            mean_magnitude,
146            rms,
147        }
148    }
149}
150
151/// Statistics comparing two meshes with the same topology.
152pub struct MeshDiffStats {
153    pub vertex_count: usize,
154    pub max_displacement: f32,
155    pub mean_displacement: f32,
156    pub rms_displacement: f32,
157    /// Approximated Hausdorff distance: max of symmetric mean displacements.
158    pub hausdorff_approx: f32,
159    /// Percentage of vertices with displacement greater than `epsilon`.
160    pub percent_changed: f32,
161    pub epsilon: f32,
162}
163
164/// Compute a per-vertex displacement field between two same-topology meshes.
165///
166/// Returns an error when vertex counts differ.
167pub fn compute_displacement(
168    base: &MeshBuffers,
169    deformed: &MeshBuffers,
170) -> anyhow::Result<DisplacementField> {
171    if base.positions.len() != deformed.positions.len() {
172        anyhow::bail!(
173            "mesh_diff: vertex count mismatch ({} vs {})",
174            base.positions.len(),
175            deformed.positions.len()
176        );
177    }
178    let deltas: Vec<[f32; 3]> = base
179        .positions
180        .iter()
181        .zip(deformed.positions.iter())
182        .map(|(b, d)| [d[0] - b[0], d[1] - b[1], d[2] - b[2]])
183        .collect();
184    let magnitudes: Vec<f32> = deltas
185        .iter()
186        .map(|d| (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt())
187        .collect();
188    let max_magnitude = magnitudes.iter().cloned().fold(0.0f32, f32::max);
189    let mean_magnitude = if magnitudes.is_empty() {
190        0.0
191    } else {
192        magnitudes.iter().sum::<f32>() / magnitudes.len() as f32
193    };
194    let rms = if magnitudes.is_empty() {
195        0.0
196    } else {
197        let mean_sq = magnitudes.iter().map(|m| m * m).sum::<f32>() / magnitudes.len() as f32;
198        mean_sq.sqrt()
199    };
200    Ok(DisplacementField {
201        deltas,
202        magnitudes,
203        max_magnitude,
204        mean_magnitude,
205        rms,
206    })
207}
208
209/// Compute statistics comparing two same-topology meshes.
210///
211/// `epsilon` is the threshold below which a vertex is considered unchanged.
212pub fn mesh_diff_stats(
213    base: &MeshBuffers,
214    deformed: &MeshBuffers,
215    epsilon: f32,
216) -> anyhow::Result<MeshDiffStats> {
217    let field_ab = compute_displacement(base, deformed)?;
218    // For Hausdorff approximation we use the symmetric mean:
219    // max(mean A→B, mean B→A).  Since we compare same-topology meshes the
220    // B→A magnitudes are identical to A→B (same per-vertex distances), so
221    // hausdorff_approx == mean_displacement.
222    let hausdorff_approx = field_ab.mean_magnitude;
223
224    let n = field_ab.magnitudes.len();
225    let changed = field_ab.magnitudes.iter().filter(|&&m| m > epsilon).count();
226    let percent_changed = if n == 0 {
227        0.0
228    } else {
229        changed as f32 / n as f32 * 100.0
230    };
231
232    Ok(MeshDiffStats {
233        vertex_count: n,
234        max_displacement: field_ab.max_magnitude,
235        mean_displacement: field_ab.mean_magnitude,
236        rms_displacement: field_ab.rms,
237        hausdorff_approx,
238        percent_changed,
239        epsilon,
240    })
241}
242
243/// Return `true` when both meshes have the same vertex count and every
244/// corresponding pair of positions is within `epsilon` of each other.
245pub fn meshes_approx_equal(a: &MeshBuffers, b: &MeshBuffers, epsilon: f32) -> bool {
246    if a.positions.len() != b.positions.len() {
247        return false;
248    }
249    a.positions.iter().zip(b.positions.iter()).all(|(pa, pb)| {
250        let dx = pa[0] - pb[0];
251        let dy = pa[1] - pb[1];
252        let dz = pa[2] - pb[2];
253        (dx * dx + dy * dy + dz * dz).sqrt() <= epsilon
254    })
255}
256
257/// Linearly blend all vertex positions between `base` (t = 0) and `target` (t = 1).
258///
259/// Returns an error when vertex counts differ.
260pub fn blend_meshes(
261    base: &MeshBuffers,
262    target: &MeshBuffers,
263    t: f32,
264) -> anyhow::Result<MeshBuffers> {
265    if base.positions.len() != target.positions.len() {
266        anyhow::bail!(
267            "blend_meshes: vertex count mismatch ({} vs {})",
268            base.positions.len(),
269            target.positions.len()
270        );
271    }
272    let t = t.clamp(0.0, 1.0);
273    let positions: Vec<[f32; 3]> = base
274        .positions
275        .iter()
276        .zip(target.positions.iter())
277        .map(|(b, tgt)| {
278            [
279                b[0] + (tgt[0] - b[0]) * t,
280                b[1] + (tgt[1] - b[1]) * t,
281                b[2] + (tgt[2] - b[2]) * t,
282            ]
283        })
284        .collect();
285    let mut result = MeshBuffers {
286        positions,
287        normals: base.normals.clone(),
288        tangents: base.tangents.clone(),
289        uvs: base.uvs.clone(),
290        indices: base.indices.clone(),
291        colors: base.colors.clone(),
292        has_suit: base.has_suit,
293    };
294    compute_normals(&mut result);
295    Ok(result)
296}
297
298/// Interpolate a sequence of mesh frames at normalised time `t` (0..1 across
299/// the whole sequence).
300///
301/// Returns an error when `frames` is empty.
302pub fn interpolate_mesh_sequence(frames: &[MeshBuffers], t: f32) -> anyhow::Result<MeshBuffers> {
303    if frames.is_empty() {
304        anyhow::bail!("interpolate_mesh_sequence: frames slice is empty");
305    }
306    if frames.len() == 1 {
307        return Ok(frames[0].clone());
308    }
309    let t = t.clamp(0.0, 1.0);
310    let last = (frames.len() - 1) as f32;
311    let pos = t * last;
312    let lo = (pos.floor() as usize).min(frames.len() - 2);
313    let hi = lo + 1;
314    let frac = pos - lo as f32;
315    blend_meshes(&frames[lo], &frames[hi], frac)
316}
317
318/// Create a copy of `base` whose per-vertex colors encode the displacement
319/// magnitude via the `Rainbow` heat-map ramp.
320pub fn displacement_to_heat_mesh(base: &MeshBuffers, field: &DisplacementField) -> MeshBuffers {
321    let ramp = ColorRamp::Rainbow;
322    let max_mag = field.max_magnitude;
323    let min_mag = 0.0f32;
324    let rgb_colors = scalars_to_colors_range(&field.magnitudes, &ramp, min_mag, max_mag);
325    let rgba_colors: Vec<[f32; 4]> = rgb_colors
326        .into_iter()
327        .map(|c| [c[0], c[1], c[2], 1.0])
328        .collect();
329    MeshBuffers {
330        positions: base.positions.clone(),
331        normals: base.normals.clone(),
332        tangents: base.tangents.clone(),
333        uvs: base.uvs.clone(),
334        indices: base.indices.clone(),
335        colors: Some(rgba_colors),
336        has_suit: base.has_suit,
337    }
338}
339
340// ── Tests ─────────────────────────────────────────────────────────────────────
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use oxihuman_morph::engine::MeshBuffers as MB;
346
347    fn make_mesh(positions: Vec<[f32; 3]>) -> MeshBuffers {
348        let n = positions.len();
349        let normals = vec![[0.0f32, 0.0, 1.0]; n];
350        let uvs = vec![[0.0f32, 0.0]; n];
351        // Simple triangle strip of fans; just need valid indices for normals.
352        let indices: Vec<u32> = if n >= 3 {
353            (0..((n as u32) - 2))
354                .flat_map(|i| [0, i + 1, i + 2])
355                .collect()
356        } else {
357            vec![]
358        };
359        MeshBuffers::from_morph(MB {
360            positions,
361            normals,
362            uvs,
363            indices,
364            has_suit: false,
365        })
366    }
367
368    fn base_triangle() -> MeshBuffers {
369        make_mesh(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
370    }
371
372    fn deformed_triangle() -> MeshBuffers {
373        make_mesh(vec![
374            [0.0, 0.0, 1.0], // displaced by 1 in Z
375            [1.0, 0.0, 1.0],
376            [0.0, 1.0, 1.0],
377        ])
378    }
379
380    // ── DisplacementField ────────────────────────────────────────────────────
381
382    #[test]
383    fn test_displacement_field_zero() {
384        let base = base_triangle();
385        let field = DisplacementField::compute(&base, &base).expect("should succeed");
386        assert_eq!(field.vertex_count(), 3);
387        assert!(field.is_zero());
388        assert!(field.max_magnitude < 1e-6);
389    }
390
391    #[test]
392    fn test_displacement_field_nonzero() {
393        let base = base_triangle();
394        let deformed = deformed_triangle();
395        let field = DisplacementField::compute(&base, &deformed).expect("should succeed");
396        assert!(!field.is_zero());
397        assert!((field.max_magnitude - 1.0).abs() < 1e-5);
398    }
399
400    #[test]
401    fn test_displacement_magnitudes() {
402        let base = base_triangle();
403        let deformed = deformed_triangle();
404        let field = DisplacementField::compute(&base, &deformed).expect("should succeed");
405        assert_eq!(field.magnitudes.len(), 3);
406        for &m in &field.magnitudes {
407            assert!(
408                (m - 1.0).abs() < 1e-5,
409                "each magnitude should be 1.0, got {m}"
410            );
411        }
412        assert!((field.mean_magnitude - 1.0).abs() < 1e-5);
413        assert!((field.rms - 1.0).abs() < 1e-5);
414    }
415
416    #[test]
417    fn test_displacement_top_displaced() {
418        let base = make_mesh(vec![
419            [0.0, 0.0, 0.0],
420            [0.0, 0.0, 0.0],
421            [0.0, 0.0, 0.0],
422            [0.0, 0.0, 0.0],
423        ]);
424        let deformed = make_mesh(vec![
425            [0.0, 0.0, 3.0],
426            [0.0, 0.0, 1.0],
427            [0.0, 0.0, 4.0],
428            [0.0, 0.0, 2.0],
429        ]);
430        let field = DisplacementField::compute(&base, &deformed).expect("should succeed");
431        let top2 = field.top_displaced(2);
432        assert_eq!(top2.len(), 2);
433        // Largest is index 2 (mag=4), second is index 0 (mag=3)
434        assert_eq!(top2[0], 2);
435        assert_eq!(top2[1], 0);
436    }
437
438    #[test]
439    fn test_displacement_scale() {
440        let base = base_triangle();
441        let deformed = deformed_triangle();
442        let field = DisplacementField::compute(&base, &deformed).expect("should succeed");
443        let scaled = field.scale(2.0);
444        assert!((scaled.max_magnitude - 2.0).abs() < 1e-5);
445        assert!((scaled.mean_magnitude - 2.0).abs() < 1e-5);
446        for d in &scaled.deltas {
447            assert!((d[2] - 2.0).abs() < 1e-5);
448        }
449    }
450
451    #[test]
452    fn test_displacement_threshold() {
453        let base = make_mesh(vec![[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]);
454        let deformed = make_mesh(vec![
455            [0.0, 0.0, 0.5], // mag 0.5 – below threshold 0.8
456            [0.0, 0.0, 1.0], // mag 1.0 – above threshold
457            [0.0, 0.0, 0.3], // mag 0.3 – below threshold
458        ]);
459        let field = DisplacementField::compute(&base, &deformed).expect("should succeed");
460        let thresholded = field.threshold(0.8);
461        // Only index 1 should survive
462        assert!(thresholded.magnitudes[0] < 1e-6);
463        assert!(thresholded.magnitudes[2] < 1e-6);
464        assert!((thresholded.magnitudes[1] - 1.0).abs() < 1e-5);
465    }
466
467    #[test]
468    fn test_displacement_apply_to() {
469        let base = base_triangle();
470        let deformed = deformed_triangle();
471        let field = DisplacementField::compute(&base, &deformed).expect("should succeed");
472        let result = field.apply_to(&base);
473        // Positions should match deformed
474        for (r, d) in result.positions.iter().zip(deformed.positions.iter()) {
475            assert!((r[0] - d[0]).abs() < 1e-5);
476            assert!((r[1] - d[1]).abs() < 1e-5);
477            assert!((r[2] - d[2]).abs() < 1e-5);
478        }
479    }
480
481    // ── meshes_approx_equal ──────────────────────────────────────────────────
482
483    #[test]
484    fn test_meshes_approx_equal_true() {
485        let a = base_triangle();
486        let b = base_triangle();
487        assert!(meshes_approx_equal(&a, &b, 1e-6));
488    }
489
490    #[test]
491    fn test_meshes_approx_equal_false() {
492        let a = base_triangle();
493        let b = deformed_triangle();
494        assert!(!meshes_approx_equal(&a, &b, 1e-6));
495        // But with a large enough epsilon it should pass
496        assert!(meshes_approx_equal(&a, &b, 2.0));
497    }
498
499    // ── blend_meshes ─────────────────────────────────────────────────────────
500
501    #[test]
502    fn test_blend_meshes_zero() {
503        let base = base_triangle();
504        let target = deformed_triangle();
505        let blended = blend_meshes(&base, &target, 0.0).expect("should succeed");
506        assert!(meshes_approx_equal(&blended, &base, 1e-5));
507    }
508
509    #[test]
510    fn test_blend_meshes_one() {
511        let base = base_triangle();
512        let target = deformed_triangle();
513        let blended = blend_meshes(&base, &target, 1.0).expect("should succeed");
514        assert!(meshes_approx_equal(&blended, &target, 1e-5));
515    }
516
517    #[test]
518    fn test_blend_meshes_half() {
519        let base = base_triangle();
520        let target = deformed_triangle();
521        let blended = blend_meshes(&base, &target, 0.5).expect("should succeed");
522        // Every vertex should be at Z = 0.5
523        for p in &blended.positions {
524            assert!((p[2] - 0.5).abs() < 1e-5, "expected Z=0.5, got {}", p[2]);
525        }
526    }
527
528    // ── mesh_diff_stats ──────────────────────────────────────────────────────
529
530    #[test]
531    fn test_mesh_diff_stats() {
532        let base = base_triangle();
533        let deformed = deformed_triangle();
534        let stats = mesh_diff_stats(&base, &deformed, 0.5).expect("should succeed");
535        assert_eq!(stats.vertex_count, 3);
536        assert!((stats.max_displacement - 1.0).abs() < 1e-5);
537        assert!((stats.mean_displacement - 1.0).abs() < 1e-5);
538        assert!((stats.rms_displacement - 1.0).abs() < 1e-5);
539        // All 3 vertices displaced by 1.0 > epsilon 0.5
540        assert!((stats.percent_changed - 100.0).abs() < 1e-3);
541    }
542
543    // ── interpolate_mesh_sequence ────────────────────────────────────────────
544
545    #[test]
546    fn test_interpolate_sequence() {
547        let f0 = base_triangle();
548        let f1 = deformed_triangle();
549        let frames = vec![f0.clone(), f1.clone()];
550
551        // t=0 → first frame
552        let r0 = interpolate_mesh_sequence(&frames, 0.0).expect("should succeed");
553        assert!(meshes_approx_equal(&r0, &f0, 1e-5));
554
555        // t=1 → last frame
556        let r1 = interpolate_mesh_sequence(&frames, 1.0).expect("should succeed");
557        assert!(meshes_approx_equal(&r1, &f1, 1e-5));
558
559        // t=0.5 → midpoint (Z = 0.5 for all verts)
560        let r_half = interpolate_mesh_sequence(&frames, 0.5).expect("should succeed");
561        for p in &r_half.positions {
562            assert!((p[2] - 0.5).abs() < 1e-5);
563        }
564
565        // Empty slice → error
566        let empty: Vec<MeshBuffers> = vec![];
567        assert!(interpolate_mesh_sequence(&empty, 0.5).is_err());
568    }
569}