Skip to main content

oxihuman_morph/
pose_space_deform.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Pose space deformation (PSD) stub.
6
7/// A single PSD example pose.
8#[derive(Debug, Clone)]
9pub struct PsdExample {
10    pub pose: Vec<f32>,
11    pub deltas: Vec<[f32; 3]>,
12    pub weight: f32,
13}
14
15/// Pose space deformer.
16#[derive(Debug, Clone)]
17pub struct PoseSpaceDeform {
18    pub examples: Vec<PsdExample>,
19    pub current_deltas: Vec<[f32; 3]>,
20}
21
22impl PoseSpaceDeform {
23    pub fn new(vertex_count: usize) -> Self {
24        PoseSpaceDeform {
25            examples: Vec::new(),
26            current_deltas: vec![[0.0; 3]; vertex_count],
27        }
28    }
29}
30
31/// Create a new PSD deformer.
32pub fn new_psd(vertex_count: usize) -> PoseSpaceDeform {
33    PoseSpaceDeform::new(vertex_count)
34}
35
36/// Add a PSD example.
37pub fn psd_add_example(psd: &mut PoseSpaceDeform, pose: Vec<f32>, deltas: Vec<[f32; 3]>) {
38    psd.examples.push(PsdExample {
39        pose,
40        deltas,
41        weight: 0.0,
42    });
43}
44
45/// Return example count.
46pub fn psd_example_count(psd: &PoseSpaceDeform) -> usize {
47    psd.examples.len()
48}
49
50/// Evaluate PSD given current pose via normalized weighted blending of all example deltas.
51///
52/// Weight per example: `w_e = 1 / (1 + dist(pose_e, current_pose))`.
53/// Exact match (dist < 1e-6): returns that example's deltas directly (no blending).
54///
55/// Blend formula: `Σ w_e * deltas_e[v] / Σ w_e`
56pub fn psd_evaluate<'a>(psd: &'a mut PoseSpaceDeform, current_pose: &[f32]) -> &'a [[f32; 3]] {
57    // Compute per-example weights and check for an exact match.
58    for ex in &mut psd.examples {
59        let n = ex.pose.len().min(current_pose.len());
60        let dist: f32 = (0..n)
61            .map(|i| (ex.pose[i] - current_pose[i]).powi(2))
62            .sum::<f32>()
63            .sqrt();
64        ex.weight = if dist < 1e-6 {
65            f32::INFINITY
66        } else {
67            1.0 / (1.0 + dist)
68        };
69    }
70
71    // Short-circuit: if any example is an exact match, use its deltas directly.
72    let exact_idx = psd.examples.iter().position(|ex| ex.weight.is_infinite());
73
74    if let Some(idx) = exact_idx {
75        let deltas = psd.examples[idx].deltas.clone();
76        let nv = psd.current_deltas.len().min(deltas.len());
77        for d in &mut psd.current_deltas {
78            *d = [0.0; 3];
79        }
80        psd.current_deltas[..nv].copy_from_slice(&deltas[..nv]);
81        return &psd.current_deltas;
82    }
83
84    // Normalized weighted blend: Σ w_e * delta_e[v] / Σ w_e
85    let weight_sum: f32 = psd.examples.iter().map(|ex| ex.weight).sum();
86
87    // Zero out current_deltas before accumulation.
88    for d in &mut psd.current_deltas {
89        *d = [0.0; 3];
90    }
91
92    if weight_sum < 1e-12 {
93        return &psd.current_deltas;
94    }
95
96    let nv = psd.current_deltas.len();
97    for ex in psd.examples.iter() {
98        let w = ex.weight / weight_sum;
99        let copy_len = nv.min(ex.deltas.len());
100        for v in 0..copy_len {
101            psd.current_deltas[v][0] += w * ex.deltas[v][0];
102            psd.current_deltas[v][1] += w * ex.deltas[v][1];
103            psd.current_deltas[v][2] += w * ex.deltas[v][2];
104        }
105    }
106
107    &psd.current_deltas
108}
109
110/// Reset all current deltas to zero.
111pub fn psd_reset(psd: &mut PoseSpaceDeform) {
112    for d in &mut psd.current_deltas {
113        *d = [0.0; 3];
114    }
115}
116
117/// Return a JSON-like string.
118pub fn psd_to_json(psd: &PoseSpaceDeform) -> String {
119    format!(
120        r#"{{"examples":{},"vertices":{}}}"#,
121        psd.examples.len(),
122        psd.current_deltas.len()
123    )
124}
125
126/// Return vertex count.
127pub fn psd_vertex_count(psd: &PoseSpaceDeform) -> usize {
128    psd.current_deltas.len()
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_new_psd_vertex_count() {
137        let p = new_psd(12);
138        assert_eq!(psd_vertex_count(&p), 12 /* vertex count must match */,);
139    }
140
141    #[test]
142    fn test_initial_no_examples() {
143        let p = new_psd(5);
144        assert_eq!(
145            psd_example_count(&p),
146            0, /* should start with no examples */
147        );
148    }
149
150    #[test]
151    fn test_add_example_increases_count() {
152        let mut p = new_psd(5);
153        psd_add_example(&mut p, vec![0.0; 4], vec![[0.0; 3]; 5]);
154        assert_eq!(psd_example_count(&p), 1 /* count should increase */,);
155    }
156
157    #[test]
158    fn test_evaluate_exact_pose_sets_deltas() {
159        let mut p = new_psd(3);
160        psd_add_example(&mut p, vec![1.0, 0.0], vec![[0.5, 0.0, 0.0]; 3]);
161        psd_evaluate(&mut p, &[1.0, 0.0]);
162        assert!(p.current_deltas[0][0] > 0.0, /* deltas should be set for exact pose */);
163    }
164
165    #[test]
166    fn test_reset_zeroes_deltas() {
167        let mut p = new_psd(3);
168        psd_add_example(&mut p, vec![0.0; 2], vec![[1.0; 3]; 3]);
169        psd_evaluate(&mut p, &[0.0; 2]);
170        psd_reset(&mut p);
171        for d in &p.current_deltas {
172            assert!((d[0]).abs() < 1e-6 /* reset should zero deltas */,);
173        }
174    }
175
176    #[test]
177    fn test_to_json_contains_examples() {
178        let p = new_psd(4);
179        let j = psd_to_json(&p);
180        assert!(j.contains("examples") /* JSON must contain examples */,);
181    }
182
183    #[test]
184    fn test_to_json_contains_vertices() {
185        let p = new_psd(7);
186        let j = psd_to_json(&p);
187        assert!(j.contains("7") /* JSON should contain vertex count */,);
188    }
189
190    #[test]
191    fn test_initial_deltas_zero() {
192        let p = new_psd(6);
193        for d in &p.current_deltas {
194            assert!((d[0]).abs() < 1e-6 /* initial deltas should be 0 */,);
195        }
196    }
197
198    #[test]
199    fn test_multiple_examples() {
200        let mut p = new_psd(2);
201        psd_add_example(&mut p, vec![0.0], vec![[0.0; 3]; 2]);
202        psd_add_example(&mut p, vec![1.0], vec![[1.0; 3]; 2]);
203        assert_eq!(
204            psd_example_count(&p),
205            2, /* two examples should be stored */
206        );
207    }
208
209    #[test]
210    fn test_example_weights_initially_zero() {
211        let mut p = new_psd(2);
212        psd_add_example(&mut p, vec![0.0], vec![[0.0; 3]; 2]);
213        assert!((p.examples[0].weight).abs() < 1e-6, /* initial example weight is 0 */);
214    }
215
216    #[test]
217    fn test_evaluate_no_examples_keeps_zero() {
218        let mut p = new_psd(3);
219        psd_evaluate(&mut p, &[0.5]);
220        for d in &p.current_deltas {
221            assert!((d[0]).abs() < 1e-6 /* no examples means zero deltas */,);
222        }
223    }
224
225    #[test]
226    fn test_blend_two_equidistant_examples_is_average() {
227        // Two examples equidistant from current_pose = [0.5].
228        // Example A: pose=[0.0], deltas=[[2.0, 0.0, 0.0]; 2]
229        // Example B: pose=[1.0], deltas=[[0.0, 0.0, 0.0]; 2]
230        // dist_A = 0.5, dist_B = 0.5 → w_A = 1/1.5, w_B = 1/1.5 (equal).
231        // Normalized: w_A = w_B = 0.5 → output = average = [[1.0, 0.0, 0.0]; 2]
232        let mut p = new_psd(2);
233        psd_add_example(&mut p, vec![0.0], vec![[2.0f32, 0.0, 0.0]; 2]);
234        psd_add_example(&mut p, vec![1.0], vec![[0.0f32, 0.0, 0.0]; 2]);
235        psd_evaluate(&mut p, &[0.5]);
236        // Because weights are equal the result must be the average: [1.0, 0.0, 0.0].
237        let expected_x = 1.0f32;
238        for (i, d) in p.current_deltas.iter().enumerate() {
239            assert!(
240                (d[0] - expected_x).abs() < 1e-4,
241                "vertex[{i}][0]: expected {expected_x}, got {}",
242                d[0]
243            );
244            assert!(
245                (d[1]).abs() < 1e-6,
246                "vertex[{i}][1]: expected 0, got {}",
247                d[1]
248            );
249        }
250    }
251
252    #[test]
253    fn test_exact_pose_match_returns_that_example_directly() {
254        // Exact match: current_pose == example A's pose → A's deltas returned verbatim.
255        let mut p = new_psd(2);
256        psd_add_example(&mut p, vec![1.0, 0.0], vec![[5.0f32, 6.0, 7.0]; 2]);
257        psd_add_example(&mut p, vec![0.0, 1.0], vec![[0.0f32; 3]; 2]);
258        psd_evaluate(&mut p, &[1.0, 0.0]);
259        assert!(
260            (p.current_deltas[0][0] - 5.0).abs() < 1e-5,
261            "exact match must copy A deltas"
262        );
263        assert!(
264            (p.current_deltas[0][1] - 6.0).abs() < 1e-5,
265            "exact match must copy A deltas"
266        );
267    }
268
269    #[test]
270    fn test_weighted_blend_asymmetric() {
271        // Pose A at distance 1.0 from query → w_A = 1/2.0 = 0.5
272        // Pose B at distance 4.0 from query → w_B = 1/5.0 = 0.2
273        // w_sum = 0.7, normalized: w_A = 0.5/0.7 ≈ 0.714, w_B = 0.2/0.7 ≈ 0.286
274        // A delta = [1.0, 0, 0], B delta = [0.0, 0, 0]
275        // Expected output[0] ≈ [0.714, 0, 0]
276        let mut p = new_psd(1);
277        psd_add_example(&mut p, vec![0.0f32], vec![[1.0f32, 0.0, 0.0]]);
278        psd_add_example(&mut p, vec![0.0f32], vec![[0.0f32, 0.0, 0.0]]);
279        // Override weights manually for deterministic distance:
280        // distance from current_pose=[1.0] to A=[0.0] = 1.0 → w_A = 0.5
281        // distance from current_pose=[1.0] to B=[5.0] = 4.0 → w_B = 0.2
282        // Re-add with controlled poses.
283        let mut p2 = new_psd(1);
284        psd_add_example(&mut p2, vec![0.0f32], vec![[1.0f32, 0.0, 0.0]]);
285        psd_add_example(&mut p2, vec![5.0f32], vec![[0.0f32, 0.0, 0.0]]);
286        psd_evaluate(&mut p2, &[1.0]);
287        let expected = 0.5f32 / (0.5 + 0.2); // ≈ 0.7143
288        assert!(
289            (p2.current_deltas[0][0] - expected).abs() < 1e-3,
290            "asymmetric blend: expected ≈{expected:.4}, got {}",
291            p2.current_deltas[0][0]
292        );
293    }
294}