Skip to main content

oxihuman_morph/
delta_mush.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Delta mush smoothing deformer stub.
6
7/// Configuration for the delta mush deformer.
8#[derive(Debug, Clone)]
9pub struct DeltaMushConfig {
10    /// Number of smoothing iterations.
11    pub iterations: usize,
12    /// Smoothing amount per iteration.
13    pub smoothing: f32,
14    /// Delta scale factor.
15    pub delta_scale: f32,
16}
17
18impl Default for DeltaMushConfig {
19    fn default() -> Self {
20        DeltaMushConfig {
21            iterations: 10,
22            smoothing: 0.5,
23            delta_scale: 1.0,
24        }
25    }
26}
27
28/// Delta mush deformer state.
29#[derive(Debug, Clone)]
30pub struct DeltaMush {
31    pub config: DeltaMushConfig,
32    /// Cached smoothed positions per vertex.
33    pub smoothed: Vec<[f32; 3]>,
34}
35
36impl DeltaMush {
37    pub fn new(vertex_count: usize) -> Self {
38        DeltaMush {
39            config: DeltaMushConfig::default(),
40            smoothed: vec![[0.0; 3]; vertex_count],
41        }
42    }
43}
44
45/// Create a new delta mush deformer.
46pub fn new_delta_mush(vertex_count: usize) -> DeltaMush {
47    DeltaMush::new(vertex_count)
48}
49
50/// Smooth positions using a simple Laplacian pass (stub: just blends toward zero).
51#[allow(clippy::needless_range_loop)]
52pub fn delta_mush_smooth(dm: &mut DeltaMush, positions: &[[f32; 3]]) {
53    let n = dm.smoothed.len().min(positions.len());
54    for i in 0..n {
55        let p = positions[i];
56        let s = dm.smoothed[i];
57        let t = dm.config.smoothing;
58        dm.smoothed[i] = [
59            s[0] + t * (p[0] - s[0]),
60            s[1] + t * (p[1] - s[1]),
61            s[2] + t * (p[2] - s[2]),
62        ];
63    }
64}
65
66/// Return the vertex count.
67pub fn delta_mush_vertex_count(dm: &DeltaMush) -> usize {
68    dm.smoothed.len()
69}
70
71/// Return a JSON-like string.
72pub fn delta_mush_to_json(dm: &DeltaMush) -> String {
73    format!(
74        r#"{{"iterations":{},"smoothing":{:.4},"delta_scale":{:.4},"vertices":{}}}"#,
75        dm.config.iterations,
76        dm.config.smoothing,
77        dm.config.delta_scale,
78        dm.smoothed.len()
79    )
80}
81
82/// Reset all smoothed positions to zero.
83pub fn delta_mush_reset(dm: &mut DeltaMush) {
84    for s in &mut dm.smoothed {
85        *s = [0.0; 3];
86    }
87}
88
89/// Set the smoothing amount.
90pub fn delta_mush_set_smoothing(dm: &mut DeltaMush, smoothing: f32) {
91    dm.config.smoothing = smoothing.clamp(0.0, 1.0);
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_new_delta_mush_vertex_count() {
100        let dm = new_delta_mush(20);
101        assert_eq!(
102            delta_mush_vertex_count(&dm),
103            20, /* vertex count must match */
104        );
105    }
106
107    #[test]
108    fn test_default_iterations() {
109        let dm = new_delta_mush(5);
110        assert_eq!(dm.config.iterations, 10 /* default iterations is 10 */,);
111    }
112
113    #[test]
114    fn test_smooth_moves_toward_target() {
115        let mut dm = new_delta_mush(1);
116        delta_mush_smooth(&mut dm, &[[2.0, 0.0, 0.0]]);
117        assert!(dm.smoothed[0][0] > 0.0, /* smoothing should move toward target */);
118    }
119
120    #[test]
121    fn test_reset_zeroes_positions() {
122        let mut dm = new_delta_mush(3);
123        delta_mush_smooth(
124            &mut dm,
125            &[[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [3.0, 3.0, 3.0]],
126        );
127        delta_mush_reset(&mut dm);
128        for s in &dm.smoothed {
129            assert!((s[0]).abs() < 1e-6, /* reset should zero all positions */);
130        }
131    }
132
133    #[test]
134    fn test_set_smoothing_clamps() {
135        let mut dm = new_delta_mush(2);
136        delta_mush_set_smoothing(&mut dm, 2.0);
137        assert!((dm.config.smoothing - 1.0).abs() < 1e-5, /* smoothing clamped to 1 */);
138    }
139
140    #[test]
141    fn test_set_smoothing_negative_clamps() {
142        let mut dm = new_delta_mush(2);
143        delta_mush_set_smoothing(&mut dm, -1.0);
144        assert!((dm.config.smoothing).abs() < 1e-6, /* negative smoothing clamped to 0 */);
145    }
146
147    #[test]
148    fn test_to_json_contains_iterations() {
149        let dm = new_delta_mush(4);
150        let j = delta_mush_to_json(&dm);
151        assert!(j.contains("iterations"), /* JSON must contain iterations */);
152    }
153
154    #[test]
155    fn test_smoothed_initialized_zero() {
156        let dm = new_delta_mush(5);
157        for s in &dm.smoothed {
158            assert!((s[0]).abs() < 1e-6, /* initial smoothed values are zero */);
159        }
160    }
161
162    #[test]
163    fn test_smooth_ignores_extra_positions() {
164        let mut dm = new_delta_mush(2);
165        delta_mush_smooth(&mut dm, &[[1.0, 0.0, 0.0]; 10]);
166        assert_eq!(
167            delta_mush_vertex_count(&dm),
168            2, /* vertex count unchanged */
169        );
170    }
171
172    #[test]
173    fn test_delta_scale_default_one() {
174        let dm = new_delta_mush(1);
175        assert!((dm.config.delta_scale - 1.0).abs() < 1e-5, /* default delta scale is 1.0 */);
176    }
177
178    #[test]
179    fn test_to_json_contains_vertices() {
180        let dm = new_delta_mush(7);
181        let j = delta_mush_to_json(&dm);
182        assert!(j.contains("7") /* JSON should contain vertex count */,);
183    }
184}