Skip to main content

oxihuman_morph/
example_based_morph.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Example-based deformation stub.
6
7/// A single example pose containing vertex positions.
8#[derive(Debug, Clone)]
9pub struct ExamplePose {
10    pub name: String,
11    pub vertices: Vec<[f32; 3]>,
12    pub weight: f32,
13}
14
15/// Example-based morph system.
16#[derive(Debug, Clone)]
17pub struct ExampleBasedMorph {
18    pub rest_pose: Vec<[f32; 3]>,
19    pub examples: Vec<ExamplePose>,
20    pub enabled: bool,
21}
22
23impl ExampleBasedMorph {
24    pub fn new(rest_pose: Vec<[f32; 3]>) -> Self {
25        ExampleBasedMorph {
26            rest_pose,
27            examples: Vec::new(),
28            enabled: true,
29        }
30    }
31}
32
33/// Create a new example-based morph from a rest pose.
34pub fn new_example_based_morph(rest_pose: Vec<[f32; 3]>) -> ExampleBasedMorph {
35    ExampleBasedMorph::new(rest_pose)
36}
37
38/// Add an example pose.
39pub fn ebm_add_example(morph: &mut ExampleBasedMorph, example: ExamplePose) {
40    morph.examples.push(example);
41}
42
43/// Evaluate the blended result using blend-shape / corrective-shape blending:
44///
45/// `output[v] = rest_pose[v] + Σ_e _weights[e] * (examples[e].vertices[v] - rest_pose[v])`
46///
47/// - Indices beyond the shorter of `_weights` / `examples` are silently ignored.
48/// - Vertex components are clamped to the length of `rest_pose`.
49/// - Mismatched example vertex lengths are handled safely (missing components treated as zero).
50pub fn ebm_evaluate(morph: &ExampleBasedMorph, _weights: &[f32]) -> Vec<[f32; 3]> {
51    let nv = morph.rest_pose.len();
52    let mut out: Vec<[f32; 3]> = morph.rest_pose.clone();
53
54    let n_examples = morph.examples.len().min(_weights.len());
55    for (ex, &w) in morph.examples[..n_examples]
56        .iter()
57        .zip(&_weights[..n_examples])
58    {
59        if w.abs() < 1e-12 {
60            continue;
61        }
62        for (i, out_v) in out[..nv].iter_mut().enumerate() {
63            let ex_pos = ex.vertices.get(i).copied().unwrap_or([0.0f32; 3]);
64            let rest = morph.rest_pose[i];
65            out_v[0] += w * (ex_pos[0] - rest[0]);
66            out_v[1] += w * (ex_pos[1] - rest[1]);
67            out_v[2] += w * (ex_pos[2] - rest[2]);
68        }
69    }
70
71    out
72}
73
74/// Return the number of examples.
75pub fn ebm_example_count(morph: &ExampleBasedMorph) -> usize {
76    morph.examples.len()
77}
78
79/// Return the vertex count.
80pub fn ebm_vertex_count(morph: &ExampleBasedMorph) -> usize {
81    morph.rest_pose.len()
82}
83
84/// Enable or disable the morph system.
85pub fn ebm_set_enabled(morph: &mut ExampleBasedMorph, enabled: bool) {
86    morph.enabled = enabled;
87}
88
89/// Serialize to JSON-like string.
90pub fn ebm_to_json(morph: &ExampleBasedMorph) -> String {
91    format!(
92        r#"{{"vertex_count":{},"example_count":{},"enabled":{}}}"#,
93        morph.rest_pose.len(),
94        morph.examples.len(),
95        morph.enabled
96    )
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_new_vertex_count() {
105        let m = new_example_based_morph(vec![[0.0; 3]; 10]);
106        assert_eq!(
107            ebm_vertex_count(&m),
108            10, /* vertex count must match rest pose */
109        );
110    }
111
112    #[test]
113    fn test_no_examples_initially() {
114        let m = new_example_based_morph(vec![[0.0; 3]; 5]);
115        assert_eq!(ebm_example_count(&m), 0 /* no examples initially */,);
116    }
117
118    #[test]
119    fn test_add_example() {
120        let mut m = new_example_based_morph(vec![[0.0; 3]; 4]);
121        ebm_add_example(
122            &mut m,
123            ExamplePose {
124                name: "smile".into(),
125                vertices: vec![[0.1, 0.0, 0.0]; 4],
126                weight: 1.0,
127            },
128        );
129        assert_eq!(ebm_example_count(&m), 1 /* one example after add */,);
130    }
131
132    #[test]
133    fn test_evaluate_length() {
134        let m = new_example_based_morph(vec![[0.0; 3]; 6]);
135        let out = ebm_evaluate(&m, &[]);
136        assert_eq!(
137            out.len(),
138            6, /* output length must match vertex count */
139        );
140    }
141
142    #[test]
143    fn test_evaluate_returns_rest() {
144        let rest = vec![[1.0, 2.0, 3.0]; 3];
145        let m = new_example_based_morph(rest.clone());
146        let out = ebm_evaluate(&m, &[]);
147        assert!((out[0][0] - rest[0][0]).abs() < 1e-6, /* must return rest pose */);
148    }
149
150    #[test]
151    fn test_set_enabled() {
152        let mut m = new_example_based_morph(vec![]);
153        ebm_set_enabled(&mut m, false);
154        assert!(!m.enabled /* enabled must be false */,);
155    }
156
157    #[test]
158    fn test_to_json() {
159        let m = new_example_based_morph(vec![[0.0; 3]; 3]);
160        let j = ebm_to_json(&m);
161        assert!(j.contains("\"vertex_count\""), /* json must contain vertex_count */);
162    }
163
164    #[test]
165    fn test_enabled_default() {
166        let m = new_example_based_morph(vec![]);
167        assert!(m.enabled /* must be enabled by default */,);
168    }
169
170    #[test]
171    fn test_multiple_examples() {
172        let mut m = new_example_based_morph(vec![[0.0; 3]; 2]);
173        for i in 0..4 {
174            ebm_add_example(
175                &mut m,
176                ExamplePose {
177                    name: format!("pose_{i}"),
178                    vertices: vec![[0.0; 3]; 2],
179                    weight: 0.5,
180                },
181            );
182        }
183        assert_eq!(
184            ebm_example_count(&m),
185            4, /* four examples must be stored */
186        );
187    }
188
189    #[test]
190    fn test_json_contains_example_count() {
191        let m = new_example_based_morph(vec![]);
192        let j = ebm_to_json(&m);
193        assert!(j.contains("\"example_count\""), /* json must contain example_count */);
194    }
195
196    #[test]
197    fn test_blend_shape_half_weight() {
198        // rest_pose = [[0,0,0]; 3], one example = [[1,1,1]; 3], weight = 0.5
199        // expected output = [[0.5, 0.5, 0.5]; 3]
200        let rest = vec![[0.0f32; 3]; 3];
201        let mut m = new_example_based_morph(rest);
202        ebm_add_example(
203            &mut m,
204            ExamplePose {
205                name: "target".into(),
206                vertices: vec![[1.0f32; 3]; 3],
207                weight: 0.5,
208            },
209        );
210        let out = ebm_evaluate(&m, &[0.5]);
211        assert_eq!(out.len(), 3, "output vertex count must match rest pose");
212        for (i, v) in out.iter().enumerate() {
213            assert!(
214                (v[0] - 0.5).abs() < 1e-5,
215                "vertex[{i}][0] must be 0.5, got {}",
216                v[0]
217            );
218            assert!(
219                (v[1] - 0.5).abs() < 1e-5,
220                "vertex[{i}][1] must be 0.5, got {}",
221                v[1]
222            );
223            assert!(
224                (v[2] - 0.5).abs() < 1e-5,
225                "vertex[{i}][2] must be 0.5, got {}",
226                v[2]
227            );
228        }
229    }
230
231    #[test]
232    fn test_blend_shape_full_weight_reaches_example() {
233        // weight = 1.0 must return exactly the example positions when rest = zero.
234        let rest = vec![[0.0f32; 3]; 2];
235        let mut m = new_example_based_morph(rest);
236        ebm_add_example(
237            &mut m,
238            ExamplePose {
239                name: "full".into(),
240                vertices: vec![[3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
241                weight: 1.0,
242            },
243        );
244        let out = ebm_evaluate(&m, &[1.0]);
245        assert!(
246            (out[0][0] - 3.0).abs() < 1e-5,
247            "full-weight must reach example position"
248        );
249        assert!(
250            (out[1][2] - 8.0).abs() < 1e-5,
251            "full-weight must reach example position"
252        );
253    }
254
255    #[test]
256    fn test_blend_shape_no_weights_returns_rest() {
257        // Passing an empty weight slice must return the rest pose unchanged.
258        let rest = vec![[1.0, 2.0, 3.0]; 4];
259        let mut m = new_example_based_morph(rest.clone());
260        ebm_add_example(
261            &mut m,
262            ExamplePose {
263                name: "ignored".into(),
264                vertices: vec![[99.0f32; 3]; 4],
265                weight: 1.0,
266            },
267        );
268        let out = ebm_evaluate(&m, &[]);
269        for (i, v) in out.iter().enumerate() {
270            assert!(
271                (v[0] - 1.0).abs() < 1e-5,
272                "vertex[{i}] must equal rest when no weights provided"
273            );
274        }
275    }
276}