Skip to main content

oxihuman_morph/
target_authoring.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Programmatic creation and editing of morph targets from mesh pairs.
5
6use oxihuman_core::parser::target::Delta;
7
8/// Configuration for morph target authoring.
9#[allow(dead_code)]
10pub struct AuthoringConfig {
11    /// Minimum delta magnitude to include (default 1e-5).
12    pub threshold: f32,
13    /// Number of Laplacian smooth iterations before storing (default 0).
14    pub smooth_iterations: u32,
15    /// Normalize max delta to 1.0 (default false).
16    pub normalize: bool,
17}
18
19impl Default for AuthoringConfig {
20    fn default() -> Self {
21        Self {
22            threshold: 1e-5,
23            smooth_iterations: 0,
24            normalize: false,
25        }
26    }
27}
28
29/// A morph target produced by the authoring pipeline.
30#[allow(dead_code)]
31pub struct AuthoredTarget {
32    pub name: String,
33    pub deltas: Vec<Delta>,
34    pub nonzero_count: usize,
35    pub max_magnitude: f32,
36    /// `[min_xyz, max_xyz]` bounding box of the delta field.
37    pub bounds: [[f32; 3]; 2],
38}
39
40// ── helpers ──────────────────────────────────────────────────────────────────
41
42#[inline]
43fn magnitude(v: [f32; 3]) -> f32 {
44    (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
45}
46
47fn build_authored(name: &str, raw: Vec<[f32; 3]>, cfg: &AuthoringConfig) -> AuthoredTarget {
48    // optionally smooth in-place
49    let mut field = raw;
50    if cfg.smooth_iterations > 0 {
51        // We have no topology here so we use a simple averaging over the whole
52        // field as a stand-in (zero-index array = no edge info).
53        // real callers should use smooth_target_deltas with proper indices.
54        for _ in 0..cfg.smooth_iterations {
55            let len = field.len();
56            if len < 2 {
57                break;
58            }
59            let mut smoothed = field.clone();
60            for i in 0..len {
61                let prev = if i == 0 { len - 1 } else { i - 1 };
62                let next = (i + 1) % len;
63                smoothed[i] = [
64                    (field[prev][0] + field[i][0] + field[next][0]) / 3.0,
65                    (field[prev][1] + field[i][1] + field[next][1]) / 3.0,
66                    (field[prev][2] + field[i][2] + field[next][2]) / 3.0,
67                ];
68            }
69            field = smoothed;
70        }
71    }
72
73    // optional normalize
74    if cfg.normalize {
75        let max_m = field.iter().map(|v| magnitude(*v)).fold(0.0_f32, f32::max);
76        if max_m > 1e-12 {
77            for v in &mut field {
78                v[0] /= max_m;
79                v[1] /= max_m;
80                v[2] /= max_m;
81            }
82        }
83    }
84
85    // build deltas with threshold filter
86    let mut deltas: Vec<Delta> = Vec::new();
87    for (vid, v) in field.iter().enumerate() {
88        if magnitude(*v) >= cfg.threshold {
89            deltas.push(Delta {
90                vid: vid as u32,
91                dx: v[0],
92                dy: v[1],
93                dz: v[2],
94            });
95        }
96    }
97
98    let nonzero_count = deltas.len();
99    let max_magnitude = deltas
100        .iter()
101        .map(|d| magnitude([d.dx, d.dy, d.dz]))
102        .fold(0.0_f32, f32::max);
103    let bounds = target_delta_bounds(&deltas);
104
105    AuthoredTarget {
106        name: name.to_owned(),
107        deltas,
108        nonzero_count,
109        max_magnitude,
110        bounds,
111    }
112}
113
114// ── public API ────────────────────────────────────────────────────────────────
115
116/// Compute per-vertex delta = deformed − base, filter by threshold, then
117/// optionally smooth and/or normalize.
118#[allow(dead_code)]
119pub fn create_target_from_mesh_pair(
120    name: &str,
121    base: &[[f32; 3]],
122    deformed: &[[f32; 3]],
123    cfg: &AuthoringConfig,
124) -> AuthoredTarget {
125    let len = base.len().min(deformed.len());
126    let raw: Vec<[f32; 3]> = (0..len)
127        .map(|i| {
128            [
129                deformed[i][0] - base[i][0],
130                deformed[i][1] - base[i][1],
131                deformed[i][2] - base[i][2],
132            ]
133        })
134        .collect();
135    build_authored(name, raw, cfg)
136}
137
138/// Build an `AuthoredTarget` directly from a delta array.
139#[allow(dead_code)]
140pub fn create_target_from_delta_field(
141    name: &str,
142    deltas: &[[f32; 3]],
143    cfg: &AuthoringConfig,
144) -> AuthoredTarget {
145    build_authored(name, deltas.to_vec(), cfg)
146}
147
148/// Weighted blend of two targets' delta fields (union of nonzero vertices).
149#[allow(dead_code)]
150pub fn merge_targets(a: &AuthoredTarget, b: &AuthoredTarget, blend: f32) -> AuthoredTarget {
151    let blend = blend.clamp(0.0, 1.0);
152    // determine the maximum vertex index referenced
153    let max_vid_a = a.deltas.iter().map(|d| d.vid).max().unwrap_or(0);
154    let max_vid_b = b.deltas.iter().map(|d| d.vid).max().unwrap_or(0);
155    let n = (max_vid_a.max(max_vid_b) as usize) + 1;
156
157    let mut field = vec![[0.0_f32; 3]; n];
158    for d in &a.deltas {
159        field[d.vid as usize] = [
160            d.dx * (1.0 - blend),
161            d.dy * (1.0 - blend),
162            d.dz * (1.0 - blend),
163        ];
164    }
165    for d in &b.deltas {
166        let v = &mut field[d.vid as usize];
167        v[0] += d.dx * blend;
168        v[1] += d.dy * blend;
169        v[2] += d.dz * blend;
170    }
171
172    let cfg = AuthoringConfig {
173        threshold: 0.0,
174        ..AuthoringConfig::default()
175    };
176    let mut result = build_authored(&a.name, field, &cfg);
177    result.name = format!("{}_merge_{}", a.name, b.name);
178    result
179}
180
181/// Multiply all deltas by `scale`.
182#[allow(dead_code)]
183pub fn scale_target(t: &AuthoredTarget, scale: f32) -> AuthoredTarget {
184    let field: Vec<[f32; 3]> = {
185        let max_vid = t.deltas.iter().map(|d| d.vid).max().unwrap_or(0) as usize;
186        let mut v = vec![[0.0_f32; 3]; max_vid + 1];
187        for d in &t.deltas {
188            v[d.vid as usize] = [d.dx * scale, d.dy * scale, d.dz * scale];
189        }
190        v
191    };
192    let cfg = AuthoringConfig {
193        threshold: 0.0,
194        ..AuthoringConfig::default()
195    };
196    let mut result = build_authored(&t.name, field, &cfg);
197    result.name = t.name.clone();
198    result
199}
200
201/// Negate all deltas.
202#[allow(dead_code)]
203pub fn invert_target(t: &AuthoredTarget) -> AuthoredTarget {
204    scale_target(t, -1.0)
205}
206
207/// Mirror deltas along the X axis using symmetry vertex pairs.
208#[allow(dead_code)]
209pub fn mirror_target_x(t: &AuthoredTarget, sym_pairs: &[(usize, usize)]) -> AuthoredTarget {
210    let max_vid = t.deltas.iter().map(|d| d.vid).max().unwrap_or(0) as usize;
211    // also ensure field covers all vertex indices referenced in sym_pairs
212    let max_sym_vid = sym_pairs
213        .iter()
214        .flat_map(|(a, b)| [*a, *b])
215        .max()
216        .unwrap_or(0);
217    let field_len = max_vid.max(max_sym_vid) + 1;
218    let mut field = vec![[0.0_f32; 3]; field_len];
219    for d in &t.deltas {
220        field[d.vid as usize] = [d.dx, d.dy, d.dz];
221    }
222
223    let mut mirrored = field.clone();
224    for (l, r) in sym_pairs {
225        if *l < field.len() && *r < field.len() {
226            // mirror X component sign
227            mirrored[*r] = [-field[*l][0], field[*l][1], field[*l][2]];
228            mirrored[*l] = [-field[*r][0], field[*r][1], field[*r][2]];
229        }
230    }
231
232    let cfg = AuthoringConfig {
233        threshold: 0.0,
234        ..AuthoringConfig::default()
235    };
236    let mut result = build_authored(&t.name, mirrored, &cfg);
237    result.name = format!("{}_mirrored", t.name);
238    result
239}
240
241/// Compute `[min_xyz, max_xyz]` bounds of a delta field.
242#[allow(dead_code)]
243pub fn target_delta_bounds(deltas: &[Delta]) -> [[f32; 3]; 2] {
244    if deltas.is_empty() {
245        return [[0.0; 3], [0.0; 3]];
246    }
247    let mut mn = [f32::MAX; 3];
248    let mut mx = [f32::MIN; 3];
249    for d in deltas {
250        let v = [d.dx, d.dy, d.dz];
251        for i in 0..3 {
252            mn[i] = mn[i].min(v[i]);
253            mx[i] = mx[i].max(v[i]);
254        }
255    }
256    [mn, mx]
257}
258
259/// Laplacian-smooth a delta field in place using mesh indices for adjacency.
260/// `indices` is a flat triangle list (groups of 3).
261#[allow(dead_code)]
262pub fn smooth_target_deltas(deltas: &mut [[f32; 3]], indices: &[u32], iterations: u32) {
263    let n = deltas.len();
264    if n == 0 || iterations == 0 {
265        return;
266    }
267
268    // Build adjacency
269    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
270    for tri in indices.chunks_exact(3) {
271        let (a, b, c) = (tri[0] as usize, tri[1] as usize, tri[2] as usize);
272        if a < n && b < n && c < n {
273            if !adj[a].contains(&b) {
274                adj[a].push(b);
275            }
276            if !adj[a].contains(&c) {
277                adj[a].push(c);
278            }
279            if !adj[b].contains(&a) {
280                adj[b].push(a);
281            }
282            if !adj[b].contains(&c) {
283                adj[b].push(c);
284            }
285            if !adj[c].contains(&a) {
286                adj[c].push(a);
287            }
288            if !adj[c].contains(&b) {
289                adj[c].push(b);
290            }
291        }
292    }
293
294    for _ in 0..iterations {
295        let prev = deltas.to_vec();
296        for i in 0..n {
297            if adj[i].is_empty() {
298                continue;
299            }
300            let mut sum = [0.0_f32; 3];
301            for &nb in &adj[i] {
302                sum[0] += prev[nb][0];
303                sum[1] += prev[nb][1];
304                sum[2] += prev[nb][2];
305            }
306            let cnt = adj[i].len() as f32;
307            deltas[i] = [sum[0] / cnt, sum[1] / cnt, sum[2] / cnt];
308        }
309    }
310}
311
312/// Return a human-readable stats string for an `AuthoredTarget`.
313#[allow(dead_code)]
314pub fn authored_target_stats(t: &AuthoredTarget) -> String {
315    format!(
316        "Target '{}': {} nonzero deltas, max_magnitude={:.6}, bounds=min{:?} max{:?}",
317        t.name, t.nonzero_count, t.max_magnitude, t.bounds[0], t.bounds[1]
318    )
319}
320
321// ── tests ─────────────────────────────────────────────────────────────────────
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    fn default_cfg() -> AuthoringConfig {
328        AuthoringConfig::default()
329    }
330
331    // 1. create_target_from_mesh_pair produces correct delta count
332    #[test]
333    fn test_mesh_pair_delta_count() {
334        let base = vec![[0.0, 0.0, 0.0]; 10];
335        let mut deformed = base.clone();
336        deformed[3] = [1.0, 0.0, 0.0];
337        deformed[7] = [0.0, 2.0, 0.0];
338        let t = create_target_from_mesh_pair("t", &base, &deformed, &default_cfg());
339        assert_eq!(t.nonzero_count, 2);
340    }
341
342    // 2. threshold filters small deltas
343    #[test]
344    fn test_threshold_filters() {
345        let base = vec![[0.0, 0.0, 0.0]; 5];
346        let deformed = vec![
347            [1e-6, 0.0, 0.0],
348            [1.0, 0.0, 0.0],
349            [0.0, 0.0, 0.0],
350            [0.0, 0.0, 0.0],
351            [0.0, 0.0, 0.0],
352        ];
353        let cfg = AuthoringConfig {
354            threshold: 1e-5,
355            ..Default::default()
356        };
357        let t = create_target_from_mesh_pair("t", &base, &deformed, &cfg);
358        // 1e-6 < 1e-5 should be filtered out; only 1.0 survives
359        assert_eq!(t.nonzero_count, 1);
360    }
361
362    // 3. create_target_from_delta_field nonzero_count
363    #[test]
364    fn test_delta_field_nonzero_count() {
365        let deltas = vec![[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
366        let t = create_target_from_delta_field("d", &deltas, &default_cfg());
367        assert_eq!(t.nonzero_count, 2);
368    }
369
370    // 4. merge_targets blend=0 returns a's deltas
371    #[test]
372    fn test_merge_blend0_is_a() {
373        let base = vec![[0.0; 3]; 4];
374        let def_a = vec![
375            [1.0, 0.0, 0.0],
376            [0.0, 0.0, 0.0],
377            [0.0, 0.0, 0.0],
378            [0.0, 0.0, 0.0],
379        ];
380        let def_b = vec![
381            [0.0, 0.0, 0.0],
382            [0.0, 2.0, 0.0],
383            [0.0, 0.0, 0.0],
384            [0.0, 0.0, 0.0],
385        ];
386        let a = create_target_from_mesh_pair("a", &base, &def_a, &default_cfg());
387        let b = create_target_from_mesh_pair("b", &base, &def_b, &default_cfg());
388        let m = merge_targets(&a, &b, 0.0);
389        // At blend=0, vertex 0 should have dx≈1.0
390        let d0 = m
391            .deltas
392            .iter()
393            .find(|d| d.vid == 0)
394            .expect("should succeed");
395        assert!((d0.dx - 1.0).abs() < 1e-5);
396        // vertex 1 should have zero contribution
397        assert!(m.deltas.iter().all(|d| d.vid != 1 || d.dy.abs() < 1e-5));
398    }
399
400    // 5. merge_targets blend=1 returns b's deltas
401    #[test]
402    fn test_merge_blend1_is_b() {
403        let base = vec![[0.0; 3]; 4];
404        let def_a = vec![
405            [1.0, 0.0, 0.0],
406            [0.0, 0.0, 0.0],
407            [0.0, 0.0, 0.0],
408            [0.0, 0.0, 0.0],
409        ];
410        let def_b = vec![
411            [0.0, 0.0, 0.0],
412            [0.0, 2.0, 0.0],
413            [0.0, 0.0, 0.0],
414            [0.0, 0.0, 0.0],
415        ];
416        let a = create_target_from_mesh_pair("a", &base, &def_a, &default_cfg());
417        let b = create_target_from_mesh_pair("b", &base, &def_b, &default_cfg());
418        let m = merge_targets(&a, &b, 1.0);
419        // At blend=1, vertex 1 should have dy≈2.0
420        let d1 = m
421            .deltas
422            .iter()
423            .find(|d| d.vid == 1)
424            .expect("should succeed");
425        assert!((d1.dy - 2.0).abs() < 1e-5);
426        assert!(m.deltas.iter().all(|d| d.vid != 0 || d.dx.abs() < 1e-5));
427    }
428
429    // 6. scale_target doubles
430    #[test]
431    fn test_scale_target_doubles() {
432        let deltas = vec![[1.0, 2.0, 3.0]];
433        let t = create_target_from_delta_field("s", &deltas, &default_cfg());
434        let scaled = scale_target(&t, 2.0);
435        let d = &scaled.deltas[0];
436        assert!((d.dx - 2.0).abs() < 1e-5);
437        assert!((d.dy - 4.0).abs() < 1e-5);
438        assert!((d.dz - 6.0).abs() < 1e-5);
439    }
440
441    // 7. invert_target negates
442    #[test]
443    fn test_invert_target_negates() {
444        let deltas = vec![[1.0, -2.0, 3.0]];
445        let t = create_target_from_delta_field("i", &deltas, &default_cfg());
446        let inv = invert_target(&t);
447        let d = &inv.deltas[0];
448        assert!((d.dx + 1.0).abs() < 1e-5);
449        assert!((d.dy - 2.0).abs() < 1e-5);
450        assert!((d.dz + 3.0).abs() < 1e-5);
451    }
452
453    // 8. invert twice = identity
454    #[test]
455    fn test_invert_twice_identity() {
456        let deltas = vec![[1.0, -2.0, 3.0], [0.5, 0.5, 0.5]];
457        let t = create_target_from_delta_field("i2", &deltas, &default_cfg());
458        let inv2 = invert_target(&invert_target(&t));
459        for (orig, inv) in t.deltas.iter().zip(inv2.deltas.iter()) {
460            assert!((orig.dx - inv.dx).abs() < 1e-4);
461            assert!((orig.dy - inv.dy).abs() < 1e-4);
462            assert!((orig.dz - inv.dz).abs() < 1e-4);
463        }
464    }
465
466    // 9. target_delta_bounds min ≤ max
467    #[test]
468    fn test_bounds_min_le_max() {
469        let deltas = vec![[1.0, -2.0, 3.0], [-1.0, 4.0, 0.5]];
470        let t = create_target_from_delta_field("b", &deltas, &default_cfg());
471        let b = &t.bounds;
472        for (i, (mn, mx)) in b[0].iter().zip(b[1].iter()).enumerate() {
473            assert!(mn <= mx, "min > max at axis {i}");
474        }
475    }
476
477    // 10. target_delta_bounds correct values
478    #[test]
479    fn test_bounds_values() {
480        let d = vec![
481            Delta {
482                vid: 0,
483                dx: 1.0,
484                dy: -2.0,
485                dz: 3.0,
486            },
487            Delta {
488                vid: 1,
489                dx: -1.0,
490                dy: 4.0,
491                dz: 0.5,
492            },
493        ];
494        let b = target_delta_bounds(&d);
495        assert!((b[0][0] - (-1.0)).abs() < 1e-6);
496        assert!((b[1][0] - 1.0).abs() < 1e-6);
497        assert!((b[0][1] - (-2.0)).abs() < 1e-6);
498        assert!((b[1][1] - 4.0).abs() < 1e-6);
499    }
500
501    // 11. smooth_target_deltas reduces magnitude on a connected mesh
502    #[test]
503    fn test_smooth_reduces_magnitude() {
504        // Triangle: 0-1-2
505        let indices = vec![0u32, 1, 2];
506        let mut deltas = vec![[10.0_f32, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
507        let before = deltas[0][0];
508        smooth_target_deltas(&mut deltas, &indices, 1);
509        let after = deltas[0][0];
510        assert!(after < before, "smoothing should reduce peak magnitude");
511    }
512
513    // 12. authored_target_stats non-empty
514    #[test]
515    fn test_authored_target_stats_non_empty() {
516        let deltas = vec![[1.0, 0.0, 0.0]];
517        let t = create_target_from_delta_field("stats", &deltas, &default_cfg());
518        let s = authored_target_stats(&t);
519        assert!(!s.is_empty());
520        assert!(s.contains("stats"));
521    }
522
523    // 13. mirror_target_x swaps pairs
524    #[test]
525    fn test_mirror_target_x_swaps() {
526        let deltas = vec![[1.0, 2.0, 3.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
527        let t = create_target_from_delta_field("mx", &deltas, &default_cfg());
528        let mirrored = mirror_target_x(&t, &[(0, 1)]);
529        // vertex 1 should now have dx = -1.0 (mirrored X)
530        let d1 = mirrored.deltas.iter().find(|d| d.vid == 1);
531        assert!(d1.is_some(), "mirrored vertex 1 should appear");
532        let d1 = d1.expect("should succeed");
533        assert!(
534            (d1.dx - (-1.0)).abs() < 1e-5,
535            "X should be negated: got {}",
536            d1.dx
537        );
538    }
539
540    // 14. normalize flag scales max to 1
541    #[test]
542    fn test_normalize_flag() {
543        let deltas = vec![[0.0, 0.0, 5.0], [0.0, 0.0, 3.0]];
544        let cfg = AuthoringConfig {
545            normalize: true,
546            ..Default::default()
547        };
548        let t = create_target_from_delta_field("norm", &deltas, &cfg);
549        assert!(
550            (t.max_magnitude - 1.0).abs() < 1e-5,
551            "max_magnitude should be 1.0 after normalize, got {}",
552            t.max_magnitude
553        );
554    }
555
556    // 15. empty bounds on empty delta list
557    #[test]
558    fn test_bounds_empty() {
559        let b = target_delta_bounds(&[]);
560        assert_eq!(b, [[0.0; 3], [0.0; 3]]);
561    }
562}