Skip to main content

oxihuman_morph/
cloth_blend.rs

1//! Cloth blending and wrapping morphs for body-aware cloth deformation.
2
3#[allow(dead_code)]
4#[derive(Clone, Debug)]
5pub struct ClothBlendConfig {
6    /// Minimum distance to push cloth from body surface.
7    pub min_offset: f32,
8    /// Maximum push distance for collision resolution.
9    pub max_push: f32,
10    /// Number of Laplacian smoothing iterations.
11    pub smooth_iterations: u32,
12    /// Blend weight threshold below which a layer is ignored.
13    pub weight_threshold: f32,
14}
15
16#[allow(dead_code)]
17#[derive(Clone, Debug)]
18pub struct ClothLayer {
19    /// Unique layer identifier.
20    pub id: u32,
21    /// Blend weight [0..1].
22    pub weight: f32,
23    /// Vertex positions (each vertex is [x, y, z]).
24    pub vertices: Vec<[f32; 3]>,
25    /// Rest-pose vertex positions.
26    pub rest_vertices: Vec<[f32; 3]>,
27}
28
29#[allow(dead_code)]
30#[derive(Clone, Debug)]
31pub struct ClothBlendResult {
32    /// Blended vertex positions.
33    pub vertices: Vec<[f32; 3]>,
34    /// Per-vertex blend energy (stretch measure).
35    pub energy: Vec<f32>,
36    /// Total blend energy sum.
37    pub total_energy: f32,
38}
39
40// ---------------------------------------------------------------------------
41// Construction
42// ---------------------------------------------------------------------------
43
44/// Return a default `ClothBlendConfig`.
45#[allow(dead_code)]
46pub fn default_cloth_blend_config() -> ClothBlendConfig {
47    ClothBlendConfig {
48        min_offset: 0.002,
49        max_push: 0.05,
50        smooth_iterations: 3,
51        weight_threshold: 1e-4,
52    }
53}
54
55/// Create a new `ClothLayer` with the given vertices (used as both current and rest).
56#[allow(dead_code)]
57pub fn new_cloth_layer(id: u32, vertices: Vec<[f32; 3]>) -> ClothLayer {
58    ClothLayer {
59        id,
60        weight: 1.0,
61        rest_vertices: vertices.clone(),
62        vertices,
63    }
64}
65
66// ---------------------------------------------------------------------------
67// Blend operations
68// ---------------------------------------------------------------------------
69
70/// Weighted blend of multiple cloth layer vertex positions.
71/// Layers with weight below `config.weight_threshold` are skipped.
72/// Returns the blended vertex array; returns empty if no layers or vertex mismatch.
73#[allow(dead_code)]
74pub fn blend_cloth_layers(layers: &[ClothLayer], config: &ClothBlendConfig) -> Vec<[f32; 3]> {
75    if layers.is_empty() {
76        return Vec::new();
77    }
78    let n = layers[0].vertices.len();
79    let mut out = vec![[0.0_f32; 3]; n];
80    let mut weight_sum = 0.0_f32;
81
82    for layer in layers {
83        if layer.weight < config.weight_threshold {
84            continue;
85        }
86        if layer.vertices.len() != n {
87            continue;
88        }
89        for (o, v) in out.iter_mut().zip(layer.vertices.iter()) {
90            o[0] += v[0] * layer.weight;
91            o[1] += v[1] * layer.weight;
92            o[2] += v[2] * layer.weight;
93        }
94        weight_sum += layer.weight;
95    }
96
97    if weight_sum > 1e-8 {
98        let inv = 1.0 / weight_sum;
99        for o in &mut out {
100            o[0] *= inv;
101            o[1] *= inv;
102            o[2] *= inv;
103        }
104    }
105    out
106}
107
108/// Push each cloth vertex outward from the body centre by `offset` units.
109/// `centre` is the body origin used as reference.
110#[allow(dead_code)]
111pub fn apply_body_offset(vertices: &mut [[f32; 3]], centre: [f32; 3], offset: f32) {
112    for v in vertices.iter_mut() {
113        let dx = v[0] - centre[0];
114        let dy = v[1] - centre[1];
115        let dz = v[2] - centre[2];
116        let len = (dx * dx + dy * dy + dz * dz).sqrt();
117        if len > 1e-8 {
118            let scale = offset / len;
119            v[0] += dx * scale;
120            v[1] += dy * scale;
121            v[2] += dz * scale;
122        }
123    }
124}
125
126/// Simple sphere-capsule collision push: push cloth vertices outside the
127/// sphere defined by `centre` and `radius`.
128#[allow(dead_code)]
129pub fn cloth_collision_push(
130    vertices: &mut [[f32; 3]],
131    centre: [f32; 3],
132    radius: f32,
133    max_push: f32,
134) {
135    for v in vertices.iter_mut() {
136        let dx = v[0] - centre[0];
137        let dy = v[1] - centre[1];
138        let dz = v[2] - centre[2];
139        let dist = (dx * dx + dy * dy + dz * dz).sqrt();
140        if dist < radius && dist > 1e-8 {
141            let push = (radius - dist).min(max_push);
142            let inv = 1.0 / dist;
143            v[0] += dx * inv * push;
144            v[1] += dy * inv * push;
145            v[2] += dz * inv * push;
146        }
147    }
148}
149
150/// Laplacian smooth blend weights across a cloth layer using a ring adjacency
151/// approximation (treats each vertex as connected to its two neighbours in order).
152#[allow(dead_code)]
153pub fn smooth_cloth_blend(weights: &[f32], iterations: u32) -> Vec<f32> {
154    if weights.is_empty() {
155        return Vec::new();
156    }
157    let n = weights.len();
158    let mut cur = weights.to_vec();
159    let mut tmp = vec![0.0_f32; n];
160    for _ in 0..iterations {
161        for i in 0..n {
162            let prev = if i == 0 { n - 1 } else { i - 1 };
163            let next = if i + 1 == n { 0 } else { i + 1 };
164            tmp[i] = (cur[prev] + cur[i] + cur[next]) / 3.0;
165        }
166        cur.copy_from_slice(&tmp);
167    }
168    cur
169}
170
171// ---------------------------------------------------------------------------
172// Layer accessors
173// ---------------------------------------------------------------------------
174
175/// Return the number of layers.
176#[allow(dead_code)]
177pub fn cloth_layer_count(layers: &[ClothLayer]) -> usize {
178    layers.len()
179}
180
181/// Compute per-vertex stretch energy as Euclidean distance from rest positions.
182/// Returns a `ClothBlendResult`.
183#[allow(dead_code)]
184pub fn cloth_blend_energy(layer: &ClothLayer) -> ClothBlendResult {
185    let n = layer.vertices.len().min(layer.rest_vertices.len());
186    let mut energy = Vec::with_capacity(n);
187    let mut total = 0.0_f32;
188    for i in 0..n {
189        let v = &layer.vertices[i];
190        let r = &layer.rest_vertices[i];
191        let e = {
192            let dx = v[0] - r[0];
193            let dy = v[1] - r[1];
194            let dz = v[2] - r[2];
195            (dx * dx + dy * dy + dz * dz).sqrt()
196        };
197        energy.push(e);
198        total += e;
199    }
200    ClothBlendResult {
201        vertices: layer.vertices.clone(),
202        energy,
203        total_energy: total,
204    }
205}
206
207/// Normalize blend weights in a layer slice so that they sum to 1.0.
208#[allow(dead_code)]
209pub fn normalize_cloth_weights(layers: &mut [ClothLayer]) {
210    let sum: f32 = layers.iter().map(|l| l.weight).sum();
211    if sum > 1e-8 {
212        for l in layers.iter_mut() {
213            l.weight /= sum;
214        }
215    }
216}
217
218/// Set the weight of a specific layer by id.
219#[allow(dead_code)]
220pub fn set_layer_weight(layers: &mut [ClothLayer], id: u32, weight: f32) {
221    for l in layers.iter_mut() {
222        if l.id == id {
223            l.weight = weight.clamp(0.0, 1.0);
224            return;
225        }
226    }
227}
228
229/// Get the weight of a specific layer by id.  Returns `None` if not found.
230#[allow(dead_code)]
231pub fn get_layer_weight(layers: &[ClothLayer], id: u32) -> Option<f32> {
232    layers.iter().find(|l| l.id == id).map(|l| l.weight)
233}
234
235/// Reset all vertices in the layer to rest positions.
236#[allow(dead_code)]
237pub fn cloth_to_rest(layer: &mut ClothLayer) {
238    layer.vertices = layer.rest_vertices.clone();
239}
240
241// ---------------------------------------------------------------------------
242// Tests
243// ---------------------------------------------------------------------------
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    fn make_layer(id: u32, n: usize, weight: f32) -> ClothLayer {
250        let verts: Vec<[f32; 3]> = (0..n).map(|i| [i as f32 * 0.1, 0.0, 0.0]).collect();
251        let mut l = new_cloth_layer(id, verts);
252        l.weight = weight;
253        l
254    }
255
256    #[test]
257    fn test_default_cloth_blend_config() {
258        let c = default_cloth_blend_config();
259        assert!(c.min_offset >= 0.0);
260        assert!(c.max_push > 0.0);
261        assert!(c.smooth_iterations > 0);
262    }
263
264    #[test]
265    fn test_new_cloth_layer() {
266        let l = new_cloth_layer(0, vec![[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
267        assert_eq!(l.vertices.len(), 2);
268        assert_eq!(l.rest_vertices.len(), 2);
269        assert_eq!(l.id, 0);
270        assert!((l.weight - 1.0).abs() < 1e-6);
271    }
272
273    #[test]
274    fn test_blend_cloth_layers_single() {
275        let cfg = default_cloth_blend_config();
276        let l = make_layer(0, 3, 1.0);
277        let blended = blend_cloth_layers(std::slice::from_ref(&l), &cfg);
278        assert_eq!(blended.len(), 3);
279        assert!((blended[0][0] - l.vertices[0][0]).abs() < 1e-5);
280    }
281
282    #[test]
283    fn test_blend_cloth_layers_average() {
284        let cfg = default_cloth_blend_config();
285        let mut la = make_layer(0, 1, 1.0);
286        let mut lb = make_layer(1, 1, 1.0);
287        la.vertices[0][0] = 0.0;
288        lb.vertices[0][0] = 2.0;
289        let blended = blend_cloth_layers(&[la, lb], &cfg);
290        assert!((blended[0][0] - 1.0).abs() < 1e-5);
291    }
292
293    #[test]
294    fn test_blend_cloth_layers_empty() {
295        let cfg = default_cloth_blend_config();
296        let blended = blend_cloth_layers(&[], &cfg);
297        assert!(blended.is_empty());
298    }
299
300    #[test]
301    fn test_blend_cloth_layers_skips_low_weight() {
302        let cfg = default_cloth_blend_config();
303        let mut la = make_layer(0, 1, 1.0);
304        let mut lb = make_layer(1, 1, 0.0); // below threshold
305        la.vertices[0][0] = 0.0;
306        lb.vertices[0][0] = 10.0;
307        let blended = blend_cloth_layers(&[la, lb], &cfg);
308        assert!(
309            blended[0][0].abs() < 1e-5,
310            "zero-weight layer should be skipped"
311        );
312    }
313
314    #[test]
315    fn test_apply_body_offset() {
316        let mut verts = vec![[1.0_f32, 0.0, 0.0]];
317        apply_body_offset(&mut verts, [0.0, 0.0, 0.0], 0.1);
318        assert!(verts[0][0] > 1.0, "vertex should be pushed outward");
319    }
320
321    #[test]
322    fn test_cloth_collision_push() {
323        let mut verts = vec![[0.5_f32, 0.0, 0.0]]; // inside sphere of r=1
324        cloth_collision_push(&mut verts, [0.0, 0.0, 0.0], 1.0, 1.0);
325        assert!(
326            verts[0][0] >= 1.0 - 1e-4,
327            "should be pushed to sphere surface"
328        );
329    }
330
331    #[test]
332    fn test_cloth_collision_push_outside() {
333        let mut verts = vec![[2.0_f32, 0.0, 0.0]]; // already outside sphere
334        cloth_collision_push(&mut verts, [0.0, 0.0, 0.0], 1.0, 1.0);
335        assert!(
336            (verts[0][0] - 2.0).abs() < 1e-6,
337            "vertex outside sphere should not move"
338        );
339    }
340
341    #[test]
342    fn test_smooth_cloth_blend_uniform() {
343        let weights = vec![0.5_f32; 5];
344        let smoothed = smooth_cloth_blend(&weights, 2);
345        assert_eq!(smoothed.len(), 5);
346        for w in &smoothed {
347            assert!((w - 0.5).abs() < 1e-5);
348        }
349    }
350
351    #[test]
352    fn test_smooth_cloth_blend_empty() {
353        let smoothed = smooth_cloth_blend(&[], 3);
354        assert!(smoothed.is_empty());
355    }
356
357    #[test]
358    fn test_cloth_layer_count() {
359        let layers: Vec<ClothLayer> = (0..4).map(|i| make_layer(i, 2, 1.0)).collect();
360        assert_eq!(cloth_layer_count(&layers), 4);
361    }
362
363    #[test]
364    fn test_cloth_blend_energy_rest() {
365        let l = new_cloth_layer(0, vec![[0.0, 0.0, 0.0]; 3]);
366        let res = cloth_blend_energy(&l);
367        assert!(res.total_energy < 1e-6, "energy should be zero at rest");
368    }
369
370    #[test]
371    fn test_cloth_blend_energy_displaced() {
372        let mut l = new_cloth_layer(0, vec![[0.0, 0.0, 0.0]]);
373        l.vertices[0][0] = 1.0;
374        let res = cloth_blend_energy(&l);
375        assert!((res.total_energy - 1.0).abs() < 1e-5);
376    }
377
378    #[test]
379    fn test_normalize_cloth_weights() {
380        let mut layers: Vec<ClothLayer> = (0..3).map(|i| make_layer(i, 1, 2.0)).collect();
381        normalize_cloth_weights(&mut layers[..]);
382        let sum: f32 = layers.iter().map(|l| l.weight).sum();
383        assert!((sum - 1.0).abs() < 1e-5);
384    }
385
386    #[test]
387    fn test_set_get_layer_weight() {
388        let mut layers = vec![make_layer(5, 1, 0.5)];
389        set_layer_weight(&mut layers[..], 5, 0.8);
390        assert!((get_layer_weight(&layers, 5).expect("should succeed") - 0.8).abs() < 1e-6);
391    }
392
393    #[test]
394    fn test_get_layer_weight_missing() {
395        let layers: Vec<ClothLayer> = vec![];
396        assert!(get_layer_weight(&layers, 99).is_none());
397    }
398
399    #[test]
400    fn test_cloth_to_rest() {
401        let mut l = new_cloth_layer(0, vec![[0.0, 0.0, 0.0]]);
402        l.vertices[0][0] = 5.0;
403        cloth_to_rest(&mut l);
404        assert!((l.vertices[0][0] - 0.0).abs() < 1e-6);
405    }
406}