Skip to main content

oxihuman_morph/
expression_transfer.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Transfer expression blend shapes between character topologies using
5//! nearest-vertex or barycentric projection.
6
7/// Interpolation strategy for expression transfer.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum TransferInterp {
11    /// Nearest-vertex lookup.
12    Nearest,
13    /// Barycentric projection onto closest triangle.
14    Barycentric,
15}
16
17/// Configuration for expression transfer.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct ExpressionTransferConfig {
21    /// Maximum search radius for closest-point lookup (default 0.1).
22    pub max_search_radius: f32,
23    /// Scale transferred deltas by mesh scale ratio.
24    pub normalize_by_scale: bool,
25    /// Interpolation mode.
26    pub interpolation: TransferInterp,
27}
28
29impl Default for ExpressionTransferConfig {
30    fn default() -> Self {
31        Self {
32            max_search_radius: 0.1,
33            normalize_by_scale: false,
34            interpolation: TransferInterp::Nearest,
35        }
36    }
37}
38
39/// A single transferred expression.
40#[allow(dead_code)]
41#[derive(Debug, Clone)]
42pub struct TransferredExpression {
43    /// Name of the expression.
44    pub name: String,
45    /// Per-vertex delta vectors on the target mesh.
46    pub deltas: Vec<[f32; 3]>,
47    /// Fraction of target vertices with valid transfer.
48    pub coverage: f32,
49    /// Maximum delta magnitude across all vertices.
50    pub max_delta_magnitude: f32,
51}
52
53/// Batch result from transferring multiple expressions.
54#[allow(dead_code)]
55#[derive(Debug, Clone)]
56pub struct ExpressionTransferBatch {
57    /// All transferred expressions.
58    pub expressions: Vec<TransferredExpression>,
59    /// Vertex count of source mesh.
60    pub source_vertex_count: usize,
61    /// Vertex count of target mesh.
62    pub target_vertex_count: usize,
63}
64
65// ── Public API ─────────────────────────────────────────────────────────────
66
67/// Transfer a single expression from source topology to target topology.
68///
69/// For each target vertex, find the closest source vertex (Nearest) or closest
70/// triangle (Barycentric) and interpolate the delta.
71#[allow(dead_code)]
72pub fn transfer_expression(
73    name: &str,
74    source_verts: &[[f32; 3]],
75    source_deltas: &[[f32; 3]],
76    target_verts: &[[f32; 3]],
77    cfg: &ExpressionTransferConfig,
78) -> TransferredExpression {
79    let scale = if cfg.normalize_by_scale {
80        mesh_scale_ratio(source_verts, target_verts)
81    } else {
82        1.0
83    };
84
85    let mut valid = 0usize;
86    let mut deltas: Vec<[f32; 3]> = Vec::with_capacity(target_verts.len());
87
88    for &tv in target_verts {
89        let d = match cfg.interpolation {
90            TransferInterp::Nearest => {
91                transfer_nearest(tv, source_verts, source_deltas, cfg.max_search_radius)
92            }
93            TransferInterp::Barycentric => {
94                // Try to find the closest triangle and use barycentric interpolation.
95                // We build triangles implicitly from consecutive triplets of source verts,
96                // or fall back to nearest if source doesn't form full triangles.
97                transfer_barycentric_or_nearest(
98                    tv,
99                    source_verts,
100                    source_deltas,
101                    cfg.max_search_radius,
102                )
103            }
104        };
105
106        if let Some(delta) = d {
107            deltas.push(scale3(delta, scale));
108            valid += 1;
109        } else {
110            deltas.push([0.0, 0.0, 0.0]);
111        }
112    }
113
114    let coverage = if target_verts.is_empty() {
115        1.0
116    } else {
117        valid as f32 / target_verts.len() as f32
118    };
119    let max_delta_magnitude = delta_magnitude(&deltas);
120
121    TransferredExpression {
122        name: name.to_string(),
123        deltas,
124        coverage,
125        max_delta_magnitude,
126    }
127}
128
129/// Transfer a batch of expressions from source topology to target topology.
130#[allow(dead_code)]
131pub fn transfer_expression_batch(
132    expressions: &[(&str, Vec<[f32; 3]>)],
133    source_verts: &[[f32; 3]],
134    target_verts: &[[f32; 3]],
135    cfg: &ExpressionTransferConfig,
136) -> ExpressionTransferBatch {
137    let transferred = expressions
138        .iter()
139        .map(|(name, deltas)| transfer_expression(name, source_verts, deltas, target_verts, cfg))
140        .collect();
141
142    ExpressionTransferBatch {
143        expressions: transferred,
144        source_vertex_count: source_verts.len(),
145        target_vertex_count: target_verts.len(),
146    }
147}
148
149/// Compute barycentric coordinates (u, v, w) for point `p` projected onto
150/// triangle (a, b, c). Returns `None` if the triangle is degenerate.
151#[allow(dead_code)]
152pub fn barycentric_coords(p: [f32; 3], a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> Option<[f32; 3]> {
153    let v0 = sub3(b, a);
154    let v1 = sub3(c, a);
155    let v2 = sub3(p, a);
156
157    let d00 = dot3(v0, v0);
158    let d01 = dot3(v0, v1);
159    let d11 = dot3(v1, v1);
160    let d20 = dot3(v2, v0);
161    let d21 = dot3(v2, v1);
162
163    let denom = d00 * d11 - d01 * d01;
164    if denom.abs() < 1e-10 {
165        return None; // degenerate
166    }
167
168    let v = (d11 * d20 - d01 * d21) / denom;
169    let w = (d00 * d21 - d01 * d20) / denom;
170    let u = 1.0 - v - w;
171    Some([u, v, w])
172}
173
174/// Interpolate a delta using barycentric coordinates (u, v, w).
175#[allow(dead_code)]
176pub fn interpolate_delta_barycentric(
177    d0: [f32; 3],
178    d1: [f32; 3],
179    d2: [f32; 3],
180    uvw: [f32; 3],
181) -> [f32; 3] {
182    [
183        d0[0] * uvw[0] + d1[0] * uvw[1] + d2[0] * uvw[2],
184        d0[1] * uvw[0] + d1[1] * uvw[1] + d2[1] * uvw[2],
185        d0[2] * uvw[0] + d1[2] * uvw[1] + d2[2] * uvw[2],
186    ]
187}
188
189/// Compute ratio of bounding-box diagonal sizes: source_diag / target_diag.
190/// Returns 1.0 when either mesh is empty or degenerate.
191#[allow(dead_code)]
192pub fn mesh_scale_ratio(source_verts: &[[f32; 3]], target_verts: &[[f32; 3]]) -> f32 {
193    let sd = bbox_diagonal(source_verts);
194    let td = bbox_diagonal(target_verts);
195    if td < 1e-10 || sd < 1e-10 {
196        return 1.0;
197    }
198    sd / td
199}
200
201/// Maximum delta magnitude (L2 norm) over all vertices.
202#[allow(dead_code)]
203pub fn delta_magnitude(deltas: &[[f32; 3]]) -> f32 {
204    deltas
205        .iter()
206        .map(|&d| (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt())
207        .fold(0.0f32, f32::max)
208}
209
210// ── Private helpers ────────────────────────────────────────────────────────
211
212fn transfer_nearest(
213    tv: [f32; 3],
214    source_verts: &[[f32; 3]],
215    source_deltas: &[[f32; 3]],
216    max_dist: f32,
217) -> Option<[f32; 3]> {
218    let mut best_idx = None;
219    let mut best_dist = max_dist;
220    for (i, &sv) in source_verts.iter().enumerate() {
221        let d = dist3(tv, sv);
222        if d < best_dist {
223            best_dist = d;
224            best_idx = Some(i);
225        }
226    }
227    best_idx.map(|i| source_deltas[i])
228}
229
230fn transfer_barycentric_or_nearest(
231    tv: [f32; 3],
232    source_verts: &[[f32; 3]],
233    source_deltas: &[[f32; 3]],
234    max_dist: f32,
235) -> Option<[f32; 3]> {
236    // Try barycentric over consecutive triangles (triplets)
237    let n_tris = source_verts.len() / 3;
238    let mut best: Option<[f32; 3]> = None;
239    let mut best_dist = max_dist;
240
241    for t in 0..n_tris {
242        let i0 = t * 3;
243        let i1 = i0 + 1;
244        let i2 = i0 + 2;
245        if let Some(uvw) =
246            barycentric_coords(tv, source_verts[i0], source_verts[i1], source_verts[i2])
247        {
248            // Compute projected point and distance
249            let proj = interpolate_delta_barycentric(
250                source_verts[i0],
251                source_verts[i1],
252                source_verts[i2],
253                uvw,
254            );
255            let d = dist3(tv, proj);
256            if d < best_dist {
257                best_dist = d;
258                best = Some(interpolate_delta_barycentric(
259                    source_deltas[i0],
260                    source_deltas[i1],
261                    source_deltas[i2],
262                    uvw,
263                ));
264            }
265        }
266    }
267
268    if best.is_some() {
269        return best;
270    }
271    // fallback to nearest
272    transfer_nearest(tv, source_verts, source_deltas, max_dist)
273}
274
275fn bbox_diagonal(verts: &[[f32; 3]]) -> f32 {
276    if verts.is_empty() {
277        return 0.0;
278    }
279    let mut mn = verts[0];
280    let mut mx = verts[0];
281    for &v in verts {
282        if v[0] < mn[0] {
283            mn[0] = v[0];
284        }
285        if v[1] < mn[1] {
286            mn[1] = v[1];
287        }
288        if v[2] < mn[2] {
289            mn[2] = v[2];
290        }
291        if v[0] > mx[0] {
292            mx[0] = v[0];
293        }
294        if v[1] > mx[1] {
295            mx[1] = v[1];
296        }
297        if v[2] > mx[2] {
298            mx[2] = v[2];
299        }
300    }
301    dist3(mn, mx)
302}
303
304#[inline]
305fn dist3(a: [f32; 3], b: [f32; 3]) -> f32 {
306    let dx = a[0] - b[0];
307    let dy = a[1] - b[1];
308    let dz = a[2] - b[2];
309    (dx * dx + dy * dy + dz * dz).sqrt()
310}
311
312#[inline]
313fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
314    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
315}
316
317#[inline]
318fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
319    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
320}
321
322#[inline]
323fn scale3(v: [f32; 3], s: f32) -> [f32; 3] {
324    [v[0] * s, v[1] * s, v[2] * s]
325}
326
327// ── Tests ──────────────────────────────────────────────────────────────────
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    fn cfg_nearest() -> ExpressionTransferConfig {
334        ExpressionTransferConfig {
335            max_search_radius: 10.0,
336            normalize_by_scale: false,
337            interpolation: TransferInterp::Nearest,
338        }
339    }
340
341    // 1. barycentric_coords centroid returns (1/3, 1/3, 1/3)
342    #[test]
343    fn test_barycentric_centroid() {
344        let a = [0.0f32, 0.0, 0.0];
345        let b = [1.0, 0.0, 0.0];
346        let c = [0.0, 1.0, 0.0];
347        let centroid = [1.0 / 3.0, 1.0 / 3.0, 0.0];
348        let uvw = barycentric_coords(centroid, a, b, c).expect("should succeed");
349        assert!((uvw[0] - 1.0 / 3.0).abs() < 1e-5, "uvw[0]={}", uvw[0]);
350        assert!((uvw[1] - 1.0 / 3.0).abs() < 1e-5, "uvw[1]={}", uvw[1]);
351        assert!((uvw[2] - 1.0 / 3.0).abs() < 1e-5, "uvw[2]={}", uvw[2]);
352    }
353
354    // 2. barycentric_coords at corner A returns (1, 0, 0)
355    #[test]
356    fn test_barycentric_corner_a() {
357        let a = [0.0f32, 0.0, 0.0];
358        let b = [1.0, 0.0, 0.0];
359        let c = [0.0, 1.0, 0.0];
360        let uvw = barycentric_coords(a, a, b, c).expect("should succeed");
361        assert!((uvw[0] - 1.0).abs() < 1e-5);
362        assert!(uvw[1].abs() < 1e-5);
363        assert!(uvw[2].abs() < 1e-5);
364    }
365
366    // 3. barycentric_coords degenerate triangle returns None
367    #[test]
368    fn test_barycentric_degenerate() {
369        let a = [0.0f32, 0.0, 0.0];
370        let b = [1.0, 0.0, 0.0];
371        let c = [2.0, 0.0, 0.0]; // collinear
372                                 // denom approaches zero for degenerate triangles
373                                 // a and b and c are collinear so degenerate
374        assert!(barycentric_coords([0.5, 0.0, 0.0], a, b, c).is_none());
375    }
376
377    // 4. interpolate_delta_barycentric at corner returns that corner's delta
378    #[test]
379    fn test_interpolate_delta_at_corner() {
380        let d0 = [1.0f32, 0.0, 0.0];
381        let d1 = [0.0, 1.0, 0.0];
382        let d2 = [0.0, 0.0, 1.0];
383        let uvw = [1.0, 0.0, 0.0];
384        let r = interpolate_delta_barycentric(d0, d1, d2, uvw);
385        assert!((r[0] - 1.0).abs() < 1e-6);
386        assert!(r[1].abs() < 1e-6);
387        assert!(r[2].abs() < 1e-6);
388    }
389
390    // 5. interpolate_delta_barycentric at centroid returns average
391    #[test]
392    fn test_interpolate_delta_at_centroid() {
393        let d0 = [3.0f32, 0.0, 0.0];
394        let d1 = [0.0, 3.0, 0.0];
395        let d2 = [0.0, 0.0, 3.0];
396        let uvw = [1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0];
397        let r = interpolate_delta_barycentric(d0, d1, d2, uvw);
398        assert!((r[0] - 1.0).abs() < 1e-5);
399        assert!((r[1] - 1.0).abs() < 1e-5);
400        assert!((r[2] - 1.0).abs() < 1e-5);
401    }
402
403    // 6. mesh_scale_ratio identical meshes = 1.0
404    #[test]
405    fn test_mesh_scale_ratio_identical() {
406        let verts = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
407        let r = mesh_scale_ratio(&verts, &verts);
408        assert!((r - 1.0).abs() < 1e-5);
409    }
410
411    // 7. mesh_scale_ratio different scales
412    #[test]
413    fn test_mesh_scale_ratio_double() {
414        let src = vec![[0.0f32, 0.0, 0.0], [2.0, 0.0, 0.0]];
415        let tgt = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0]];
416        let r = mesh_scale_ratio(&src, &tgt);
417        assert!((r - 2.0).abs() < 1e-5);
418    }
419
420    // 8. transfer_expression identity topology → same deltas
421    #[test]
422    fn test_transfer_expression_identity_topology() {
423        let verts = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0]];
424        let deltas = vec![[0.1f32, 0.2, 0.3], [0.4, 0.5, 0.6]];
425        let cfg = cfg_nearest();
426        let result = transfer_expression("test", &verts, &deltas, &verts, &cfg);
427        for (a, b) in deltas.iter().zip(result.deltas.iter()) {
428            assert!((a[0] - b[0]).abs() < 1e-5);
429            assert!((a[1] - b[1]).abs() < 1e-5);
430            assert!((a[2] - b[2]).abs() < 1e-5);
431        }
432    }
433
434    // 9. coverage = 1.0 for same topology within radius
435    #[test]
436    fn test_transfer_expression_full_coverage() {
437        let verts = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0]];
438        let deltas = vec![[0.1f32, 0.0, 0.0], [0.2, 0.0, 0.0]];
439        let cfg = cfg_nearest();
440        let result = transfer_expression("test", &verts, &deltas, &verts, &cfg);
441        assert!((result.coverage - 1.0).abs() < 1e-6);
442    }
443
444    // 10. coverage < 1.0 when target verts are far from source
445    #[test]
446    fn test_transfer_expression_partial_coverage() {
447        let source_verts = vec![[0.0f32, 0.0, 0.0]];
448        let source_deltas = vec![[0.1f32, 0.0, 0.0]];
449        let target_verts = vec![[0.0f32, 0.0, 0.0], [100.0, 0.0, 0.0]];
450        let cfg = ExpressionTransferConfig {
451            max_search_radius: 0.5,
452            ..Default::default()
453        };
454        let result =
455            transfer_expression("test", &source_verts, &source_deltas, &target_verts, &cfg);
456        assert!(result.coverage < 1.0);
457    }
458
459    // 11. batch count matches input
460    #[test]
461    fn test_transfer_batch_count_matches() {
462        let verts = vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0]];
463        let exprs: Vec<(&str, Vec<[f32; 3]>)> = vec![
464            ("smile", vec![[0.1, 0.0, 0.0], [0.2, 0.0, 0.0]]),
465            ("frown", vec![[0.0, 0.1, 0.0], [0.0, 0.2, 0.0]]),
466            ("blink", vec![[0.0, 0.0, 0.1], [0.0, 0.0, 0.2]]),
467        ];
468        let cfg = cfg_nearest();
469        let batch = transfer_expression_batch(&exprs, &verts, &verts, &cfg);
470        assert_eq!(batch.expressions.len(), 3);
471        assert_eq!(batch.source_vertex_count, 2);
472        assert_eq!(batch.target_vertex_count, 2);
473    }
474
475    // 12. delta_magnitude empty returns 0.0
476    #[test]
477    fn test_delta_magnitude_empty() {
478        assert_eq!(delta_magnitude(&[]), 0.0);
479    }
480
481    // 13. delta_magnitude correct value
482    #[test]
483    fn test_delta_magnitude_value() {
484        let d = vec![[3.0f32, 4.0, 0.0]];
485        assert!((delta_magnitude(&d) - 5.0).abs() < 1e-5);
486    }
487
488    // 14. barycentric_coords at corner B returns (0, 1, 0)
489    #[test]
490    fn test_barycentric_corner_b() {
491        let a = [0.0f32, 0.0, 0.0];
492        let b = [1.0, 0.0, 0.0];
493        let c = [0.0, 1.0, 0.0];
494        let uvw = barycentric_coords(b, a, b, c).expect("should succeed");
495        assert!(uvw[0].abs() < 1e-5);
496        assert!((uvw[1] - 1.0).abs() < 1e-5);
497        assert!(uvw[2].abs() < 1e-5);
498    }
499}