Skip to main content

oxihuman_morph/
volume_morph.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Volume-preserving morphs using Jacobian correction.
5
6#[allow(dead_code)]
7#[derive(Debug, Clone)]
8pub struct VolumeMorphConfig {
9    pub preservation_strength: f32,
10    pub iterations: u32,
11    pub smooth_correction: bool,
12}
13
14impl Default for VolumeMorphConfig {
15    fn default() -> Self {
16        Self {
17            preservation_strength: 0.5,
18            iterations: 3,
19            smooth_correction: true,
20        }
21    }
22}
23
24#[allow(dead_code)]
25#[derive(Debug, Clone)]
26pub struct VolumeMorphResult {
27    pub corrected_deltas: Vec<[f32; 3]>,
28    pub original_volume: f32,
29    pub morphed_volume: f32,
30    pub corrected_volume: f32,
31    pub volume_error_pct: f32,
32}
33
34/// Signed volume via divergence theorem (triangle mesh).
35#[allow(dead_code)]
36pub fn compute_mesh_volume(positions: &[[f32; 3]], indices: &[u32]) -> f32 {
37    let mut volume = 0.0f32;
38    let tri_count = indices.len() / 3;
39    for t in 0..tri_count {
40        let i0 = indices[t * 3] as usize;
41        let i1 = indices[t * 3 + 1] as usize;
42        let i2 = indices[t * 3 + 2] as usize;
43        if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
44            continue;
45        }
46        let v0 = positions[i0];
47        let v1 = positions[i1];
48        let v2 = positions[i2];
49        // Signed volume contribution: (v0 · (v1 × v2)) / 6
50        let cross_x = v1[1] * v2[2] - v1[2] * v2[1];
51        let cross_y = v1[2] * v2[0] - v1[0] * v2[2];
52        let cross_z = v1[0] * v2[1] - v1[1] * v2[0];
53        volume += v0[0] * cross_x + v0[1] * cross_y + v0[2] * cross_z;
54    }
55    volume / 6.0
56}
57
58#[allow(dead_code)]
59pub fn volume_preserving_delta(
60    base: &[[f32; 3]],
61    indices: &[u32],
62    deltas: &[[f32; 3]],
63    cfg: &VolumeMorphConfig,
64) -> VolumeMorphResult {
65    let n = base.len();
66
67    // Apply deltas to get morphed positions
68    let morphed: Vec<[f32; 3]> = base
69        .iter()
70        .enumerate()
71        .map(|(i, b)| {
72            let d = if i < deltas.len() {
73                deltas[i]
74            } else {
75                [0.0; 3]
76            };
77            [b[0] + d[0], b[1] + d[1], b[2] + d[2]]
78        })
79        .collect();
80
81    let original_volume = compute_mesh_volume(base, indices);
82    let morphed_volume = compute_mesh_volume(&morphed, indices);
83
84    // Compute scale factor
85    let ratio = mesh_volume_ratio(base, &morphed, indices);
86    // Blend with preservation_strength
87    let scale = 1.0 + (ratio - 1.0) * cfg.preservation_strength;
88
89    let mut corrected_deltas: Vec<[f32; 3]> = uniform_scale_correction(base, deltas, scale);
90
91    if cfg.smooth_correction {
92        corrected_deltas = laplacian_smooth_deltas(&corrected_deltas, indices, cfg.iterations);
93    }
94
95    // Recompute corrected volume
96    let corrected_positions: Vec<[f32; 3]> = base
97        .iter()
98        .enumerate()
99        .map(|(i, b)| {
100            let d = if i < n { corrected_deltas[i] } else { [0.0; 3] };
101            [b[0] + d[0], b[1] + d[1], b[2] + d[2]]
102        })
103        .collect();
104    let corrected_volume = compute_mesh_volume(&corrected_positions, indices);
105    let volume_error_pct = volume_error_percent(original_volume, corrected_volume);
106
107    VolumeMorphResult {
108        corrected_deltas,
109        original_volume,
110        morphed_volume,
111        corrected_volume,
112        volume_error_pct,
113    }
114}
115
116#[allow(dead_code)]
117pub fn uniform_scale_correction(
118    _base: &[[f32; 3]],
119    deltas: &[[f32; 3]],
120    scale: f32,
121) -> Vec<[f32; 3]> {
122    deltas
123        .iter()
124        .map(|d| [d[0] * scale, d[1] * scale, d[2] * scale])
125        .collect()
126}
127
128#[allow(dead_code)]
129pub fn laplacian_smooth_deltas(
130    deltas: &[[f32; 3]],
131    indices: &[u32],
132    iterations: u32,
133) -> Vec<[f32; 3]> {
134    let n = deltas.len();
135    if n == 0 || indices.is_empty() {
136        return deltas.to_vec();
137    }
138
139    // Build one-ring adjacency
140    let mut neighbors: Vec<Vec<usize>> = vec![Vec::new(); n];
141    let tri_count = indices.len() / 3;
142    for t in 0..tri_count {
143        let i0 = indices[t * 3] as usize;
144        let i1 = indices[t * 3 + 1] as usize;
145        let i2 = indices[t * 3 + 2] as usize;
146        if i0 < n && i1 < n && i2 < n {
147            if !neighbors[i0].contains(&i1) {
148                neighbors[i0].push(i1);
149            }
150            if !neighbors[i0].contains(&i2) {
151                neighbors[i0].push(i2);
152            }
153            if !neighbors[i1].contains(&i0) {
154                neighbors[i1].push(i0);
155            }
156            if !neighbors[i1].contains(&i2) {
157                neighbors[i1].push(i2);
158            }
159            if !neighbors[i2].contains(&i0) {
160                neighbors[i2].push(i0);
161            }
162            if !neighbors[i2].contains(&i1) {
163                neighbors[i2].push(i1);
164            }
165        }
166    }
167
168    let mut current = deltas.to_vec();
169    for _ in 0..iterations {
170        let prev = current.clone();
171        for i in 0..n {
172            let nb = &neighbors[i];
173            if nb.is_empty() {
174                continue;
175            }
176            let mut avg = [0.0f32; 3];
177            for &j in nb {
178                avg[0] += prev[j][0];
179                avg[1] += prev[j][1];
180                avg[2] += prev[j][2];
181            }
182            let k = nb.len() as f32;
183            current[i] = [avg[0] / k, avg[1] / k, avg[2] / k];
184        }
185    }
186    current
187}
188
189#[allow(dead_code)]
190pub fn mesh_volume_ratio(
191    base_positions: &[[f32; 3]],
192    morphed_positions: &[[f32; 3]],
193    indices: &[u32],
194) -> f32 {
195    let v_base = compute_mesh_volume(base_positions, indices);
196    let v_morphed = compute_mesh_volume(morphed_positions, indices);
197    if v_morphed.abs() < 1e-10 {
198        return 1.0;
199    }
200    v_base / v_morphed
201}
202
203#[allow(dead_code)]
204pub fn volume_error_percent(original: f32, corrected: f32) -> f32 {
205    if original.abs() < 1e-10 {
206        return 0.0;
207    }
208    (corrected - original) / original * 100.0
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    /// Build a regular tetrahedron with vertices at (1,1,1), (-1,-1,1), (-1,1,-1), (1,-1,-1).
216    /// Volume = 8/3.
217    fn tetra_positions() -> Vec<[f32; 3]> {
218        vec![
219            [1.0, 1.0, 1.0],
220            [-1.0, -1.0, 1.0],
221            [-1.0, 1.0, -1.0],
222            [1.0, -1.0, -1.0],
223        ]
224    }
225
226    fn tetra_indices() -> Vec<u32> {
227        // Four faces of the tetrahedron, all wound consistently outward
228        vec![0, 1, 2, 0, 2, 3, 0, 3, 1, 1, 3, 2]
229    }
230
231    #[test]
232    fn test_compute_mesh_volume_tetrahedron() {
233        let pos = tetra_positions();
234        let idx = tetra_indices();
235        let vol = compute_mesh_volume(&pos, &idx).abs();
236        // Volume of this regular tetrahedron = 8/3 ≈ 2.667
237        assert!((vol - 8.0 / 3.0).abs() < 0.01, "tetra volume: {vol}");
238    }
239
240    #[test]
241    fn test_compute_mesh_volume_empty() {
242        let vol = compute_mesh_volume(&[], &[]);
243        assert_eq!(vol, 0.0);
244    }
245
246    #[test]
247    fn test_compute_mesh_volume_single_triangle() {
248        // Degenerate: single triangle contributes a wedge
249        let pos = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
250        let idx = vec![0u32, 1, 2];
251        let vol = compute_mesh_volume(&pos, &idx).abs();
252        // Signed wedge volume = (1/6)|v0·(v1×v2)|
253        // v1×v2 = (1,0,0)×(0,0,1)-(0,1,0)×(0,0,1) ... = (1,-1,-1) wait, let's compute:
254        // v1=(0,1,0), v2=(0,0,1) => v1×v2=(1*1-0*0, 0*0-0*1, 0*0-1*0)=(1,0,0)
255        // v0·(1,0,0) = 1 => vol = 1/6
256        assert!((vol - 1.0 / 6.0).abs() < 1e-5, "single tri vol: {vol}");
257    }
258
259    #[test]
260    fn test_volume_error_percent_formula() {
261        let pct = volume_error_percent(100.0, 105.0);
262        assert!((pct - 5.0).abs() < 1e-5, "5% error: {pct}");
263    }
264
265    #[test]
266    fn test_volume_error_percent_zero_original() {
267        let pct = volume_error_percent(0.0, 5.0);
268        assert_eq!(pct, 0.0);
269    }
270
271    #[test]
272    fn test_uniform_scale_correction() {
273        let base = vec![[0.0, 0.0, 0.0]];
274        let deltas = vec![[1.0, 2.0, 3.0]];
275        let result = uniform_scale_correction(&base, &deltas, 2.0);
276        assert!((result[0][0] - 2.0).abs() < 1e-6);
277        assert!((result[0][1] - 4.0).abs() < 1e-6);
278        assert!((result[0][2] - 6.0).abs() < 1e-6);
279    }
280
281    #[test]
282    fn test_uniform_scale_correction_identity() {
283        let base = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
284        let deltas = vec![[2.0, 0.0, 0.0], [3.0, 0.0, 0.0]];
285        let result = uniform_scale_correction(&base, &deltas, 1.0);
286        assert!((result[0][0] - 2.0).abs() < 1e-6);
287        assert!((result[1][0] - 3.0).abs() < 1e-6);
288    }
289
290    #[test]
291    fn test_laplacian_smooth_reduces_magnitude() {
292        // A single spike at vertex 0 should be smoothed down
293        let deltas = vec![[10.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
294        let indices = vec![0u32, 1, 2];
295        let smoothed = laplacian_smooth_deltas(&deltas, &indices, 1);
296        // vertex 0 neighbors are 1,2 → both have 0 delta → smoothed[0] = 0
297        assert!(
298            smoothed[0][0].abs() < deltas[0][0].abs(),
299            "spike should be reduced: {}",
300            smoothed[0][0]
301        );
302    }
303
304    #[test]
305    fn test_laplacian_smooth_zero_delta_noop() {
306        let deltas = vec![[0.0, 0.0, 0.0]; 4];
307        let indices = vec![0u32, 1, 2, 0, 2, 3];
308        let smoothed = laplacian_smooth_deltas(&deltas, &indices, 3);
309        for d in &smoothed {
310            for &v in d {
311                assert!(v.abs() < 1e-6);
312            }
313        }
314    }
315
316    #[test]
317    fn test_mesh_volume_ratio_same_mesh() {
318        let pos = tetra_positions();
319        let idx = tetra_indices();
320        let ratio = mesh_volume_ratio(&pos, &pos, &idx);
321        assert!((ratio - 1.0).abs() < 1e-5, "same mesh ratio=1: {ratio}");
322    }
323
324    #[test]
325    fn test_volume_preserving_delta_reduces_error() {
326        let pos = tetra_positions();
327        let idx = tetra_indices();
328        // Inflate all vertices by 20% — this changes volume significantly
329        let inflate: Vec<[f32; 3]> = pos
330            .iter()
331            .map(|v| [v[0] * 0.2, v[1] * 0.2, v[2] * 0.2])
332            .collect();
333        let cfg = VolumeMorphConfig {
334            preservation_strength: 1.0,
335            iterations: 1,
336            smooth_correction: false,
337        };
338        let result = volume_preserving_delta(&pos, &idx, &inflate, &cfg);
339        // Corrected volume error should be less than uncorrected
340        let uncorrected_pct =
341            volume_error_percent(result.original_volume, result.morphed_volume).abs();
342        let corrected_pct = result.volume_error_pct.abs();
343        assert!(
344            corrected_pct < uncorrected_pct,
345            "corrected error {corrected_pct} should be < uncorrected {uncorrected_pct}"
346        );
347    }
348
349    #[test]
350    fn test_volume_preserving_delta_zero_deltas() {
351        let pos = tetra_positions();
352        let idx = tetra_indices();
353        let deltas = vec![[0.0, 0.0, 0.0]; pos.len()];
354        let cfg = VolumeMorphConfig::default();
355        let result = volume_preserving_delta(&pos, &idx, &deltas, &cfg);
356        assert!(
357            (result.original_volume - result.morphed_volume).abs() < 1e-5,
358            "zero delta: volumes should match"
359        );
360    }
361
362    #[test]
363    fn test_laplacian_smooth_empty() {
364        let result = laplacian_smooth_deltas(&[], &[], 3);
365        assert!(result.is_empty());
366    }
367}