Skip to main content

oxihuman_physics/
material.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Physical cloth material definitions and presets.
5//!
6//! Provides [`ClothMaterial`] with parameters used by the cloth simulation,
7//! along with common preset constructors (cotton, silk, denim, etc.)
8//! and [`ClothStack`] for layered clothing setups.
9
10// ── ClothMaterial ─────────────────────────────────────────────────────────────
11
12/// Physical properties of a cloth material.
13#[derive(Debug, Clone)]
14#[allow(dead_code)]
15pub struct ClothMaterial {
16    pub name: String,
17    /// Mass per unit area (kg/m²). Typical: 0.1 (silk) to 0.8 (denim).
18    pub surface_density: f32,
19    /// Structural spring stiffness [0, 1]. Higher = stiffer.
20    pub structural_stiffness: f32,
21    /// Shear spring stiffness [0, 1].
22    pub shear_stiffness: f32,
23    /// Bending stiffness [0, 1]. Higher = less drape.
24    pub bending_stiffness: f32,
25    /// Damping coefficient [0, 1]. Higher = faster settling.
26    pub damping: f32,
27    /// Friction coefficient [0, 1] for collisions.
28    pub friction: f32,
29    /// Stretchability [0, 1]. 0 = inelastic, 1 = very stretchy.
30    pub stretch: f32,
31    /// Thickness in meters (for collision detection).
32    pub thickness: f32,
33}
34
35impl ClothMaterial {
36    /// Create a new material with default (neutral) values.
37    pub fn new(name: impl Into<String>) -> Self {
38        Self {
39            name: name.into(),
40            surface_density: 0.2,
41            structural_stiffness: 0.5,
42            shear_stiffness: 0.5,
43            bending_stiffness: 0.5,
44            damping: 0.3,
45            friction: 0.5,
46            stretch: 0.1,
47            thickness: 0.001,
48        }
49    }
50
51    // ── Preset constructors ────────────────────────────────────────────────────
52
53    /// Cotton preset: medium weight, moderate stiffness, low stretch.
54    pub fn cotton() -> Self {
55        Self {
56            name: "cotton".to_string(),
57            surface_density: 0.2,
58            structural_stiffness: 0.8,
59            shear_stiffness: 0.6,
60            bending_stiffness: 0.4,
61            damping: 0.3,
62            friction: 0.5,
63            stretch: 0.1,
64            thickness: 0.001,
65        }
66    }
67
68    /// Silk preset: lightweight, low stiffness, excellent drape.
69    pub fn silk() -> Self {
70        Self {
71            name: "silk".to_string(),
72            surface_density: 0.1,
73            structural_stiffness: 0.5,
74            shear_stiffness: 0.3,
75            bending_stiffness: 0.1,
76            damping: 0.1,
77            friction: 0.2,
78            stretch: 0.2,
79            thickness: 0.0005,
80        }
81    }
82
83    /// Denim preset: heavy, very stiff, low stretch.
84    pub fn denim() -> Self {
85        Self {
86            name: "denim".to_string(),
87            surface_density: 0.5,
88            structural_stiffness: 0.95,
89            shear_stiffness: 0.8,
90            bending_stiffness: 0.8,
91            damping: 0.4,
92            friction: 0.7,
93            stretch: 0.05,
94            thickness: 0.002,
95        }
96    }
97
98    /// Leather preset: heavy, very stiff, minimal stretch.
99    pub fn leather() -> Self {
100        Self {
101            name: "leather".to_string(),
102            surface_density: 0.7,
103            structural_stiffness: 0.98,
104            shear_stiffness: 0.9,
105            bending_stiffness: 0.95,
106            damping: 0.5,
107            friction: 0.8,
108            stretch: 0.02,
109            thickness: 0.003,
110        }
111    }
112
113    /// Rubber preset: medium weight, low stiffness, very stretchy.
114    pub fn rubber() -> Self {
115        Self {
116            name: "rubber".to_string(),
117            surface_density: 0.4,
118            structural_stiffness: 0.3,
119            shear_stiffness: 0.2,
120            bending_stiffness: 0.2,
121            damping: 0.6,
122            friction: 0.9,
123            stretch: 0.9,
124            thickness: 0.002,
125        }
126    }
127
128    /// Wool preset: medium weight, moderate stiffness, some stretch.
129    pub fn wool() -> Self {
130        Self {
131            name: "wool".to_string(),
132            surface_density: 0.3,
133            structural_stiffness: 0.7,
134            shear_stiffness: 0.5,
135            bending_stiffness: 0.5,
136            damping: 0.5,
137            friction: 0.6,
138            stretch: 0.3,
139            thickness: 0.002,
140        }
141    }
142
143    // ── Queries ────────────────────────────────────────────────────────────────
144
145    /// Whether the material would behave "stiff" (low drape).
146    ///
147    /// Returns `true` when `bending_stiffness > 0.7`.
148    pub fn is_stiff(&self) -> bool {
149        self.bending_stiffness > 0.7
150    }
151
152    /// Whether the material would drape well.
153    ///
154    /// Returns `true` when `bending_stiffness < 0.3`.
155    pub fn is_drapeable(&self) -> bool {
156        self.bending_stiffness < 0.3
157    }
158
159    /// Compute approximate terminal velocity for gravity = 9.8 m/s².
160    ///
161    /// Uses a simplified aerodynamic drag model:
162    /// `v_t = sqrt(2 * g * surface_density / (air_density * drag_coeff))`
163    /// with `air_density = 1.225 kg/m³` and `drag_coeff = 1.0`.
164    pub fn terminal_velocity(&self) -> f32 {
165        let g = 9.8_f32;
166        let air_density = 1.225_f32;
167        let drag_coeff = 1.0_f32;
168        (2.0 * g * self.surface_density / (air_density * drag_coeff)).sqrt()
169    }
170
171    // ── Transformations ───────────────────────────────────────────────────────
172
173    /// Interpolate between two materials by factor `t` in `[0, 1]`.
174    ///
175    /// `t = 0` returns a clone of `self`; `t = 1` returns a clone of `other`.
176    pub fn lerp(&self, other: &ClothMaterial, t: f32) -> ClothMaterial {
177        let t = t.clamp(0.0, 1.0);
178        let s = 1.0 - t;
179        ClothMaterial {
180            name: if t < 0.5 {
181                self.name.clone()
182            } else {
183                other.name.clone()
184            },
185            surface_density: self.surface_density * s + other.surface_density * t,
186            structural_stiffness: self.structural_stiffness * s + other.structural_stiffness * t,
187            shear_stiffness: self.shear_stiffness * s + other.shear_stiffness * t,
188            bending_stiffness: self.bending_stiffness * s + other.bending_stiffness * t,
189            damping: self.damping * s + other.damping * t,
190            friction: self.friction * s + other.friction * t,
191            stretch: self.stretch * s + other.stretch * t,
192            thickness: self.thickness * s + other.thickness * t,
193        }
194    }
195
196    /// Scale all stiffness values by `scale` (useful for LOD reduction).
197    ///
198    /// Clamps resulting stiffness values to `[0, 1]`.
199    pub fn with_stiffness_scale(&self, scale: f32) -> ClothMaterial {
200        ClothMaterial {
201            name: self.name.clone(),
202            surface_density: self.surface_density,
203            structural_stiffness: (self.structural_stiffness * scale).clamp(0.0, 1.0),
204            shear_stiffness: (self.shear_stiffness * scale).clamp(0.0, 1.0),
205            bending_stiffness: (self.bending_stiffness * scale).clamp(0.0, 1.0),
206            damping: self.damping,
207            friction: self.friction,
208            stretch: self.stretch,
209            thickness: self.thickness,
210        }
211    }
212
213    /// Return a material adjusted for a given wind strength in `[0, 1]`.
214    ///
215    /// Higher wind increases damping (settling faster) and slightly reduces
216    /// structural/shear stiffness (cloth becomes more responsive).
217    pub fn for_wind_strength(&self, wind: f32) -> ClothMaterial {
218        let wind = wind.clamp(0.0, 1.0);
219        ClothMaterial {
220            name: self.name.clone(),
221            surface_density: self.surface_density,
222            structural_stiffness: (self.structural_stiffness * (1.0 - wind * 0.2)).clamp(0.0, 1.0),
223            shear_stiffness: (self.shear_stiffness * (1.0 - wind * 0.1)).clamp(0.0, 1.0),
224            bending_stiffness: self.bending_stiffness,
225            damping: (self.damping + wind * 0.4).clamp(0.0, 1.0),
226            friction: self.friction,
227            stretch: self.stretch,
228            thickness: self.thickness,
229        }
230    }
231
232    // ── Preset registry ───────────────────────────────────────────────────────
233
234    /// Return all built-in preset materials.
235    pub fn all_presets() -> Vec<ClothMaterial> {
236        vec![
237            ClothMaterial::cotton(),
238            ClothMaterial::silk(),
239            ClothMaterial::denim(),
240            ClothMaterial::leather(),
241            ClothMaterial::rubber(),
242            ClothMaterial::wool(),
243        ]
244    }
245
246    /// Find a preset by name (case-insensitive).
247    ///
248    /// Returns `None` if no preset matches.
249    pub fn find_preset(name: &str) -> Option<ClothMaterial> {
250        let lower = name.to_lowercase();
251        ClothMaterial::all_presets()
252            .into_iter()
253            .find(|m| m.name == lower)
254    }
255}
256
257// ── ClothStack ────────────────────────────────────────────────────────────────
258
259/// A layered cloth setup: multiple material layers (e.g., shirt + jacket).
260#[allow(dead_code)]
261pub struct ClothStack {
262    pub layers: Vec<ClothMaterial>,
263}
264
265impl ClothStack {
266    /// Create an empty stack.
267    pub fn new() -> Self {
268        Self { layers: Vec::new() }
269    }
270
271    /// Push a material layer onto the stack.
272    pub fn push(&mut self, mat: ClothMaterial) {
273        self.layers.push(mat);
274    }
275
276    /// Compute the blended effective material for the full stack.
277    ///
278    /// Returns an average over all layers weighted equally.
279    /// If the stack is empty, returns a default `ClothMaterial::new("empty")`.
280    pub fn effective_material(&self) -> ClothMaterial {
281        if self.layers.is_empty() {
282            return ClothMaterial::new("empty");
283        }
284
285        let n = self.layers.len() as f32;
286        let mut result = ClothMaterial::new("stack");
287        result.surface_density = 0.0;
288        result.structural_stiffness = 0.0;
289        result.shear_stiffness = 0.0;
290        result.bending_stiffness = 0.0;
291        result.damping = 0.0;
292        result.friction = 0.0;
293        result.stretch = 0.0;
294        result.thickness = 0.0;
295
296        for layer in &self.layers {
297            result.surface_density += layer.surface_density;
298            result.structural_stiffness += layer.structural_stiffness;
299            result.shear_stiffness += layer.shear_stiffness;
300            result.bending_stiffness += layer.bending_stiffness;
301            result.damping += layer.damping;
302            result.friction += layer.friction;
303            result.stretch += layer.stretch;
304            result.thickness += layer.thickness;
305        }
306
307        result.surface_density /= n;
308        result.structural_stiffness /= n;
309        result.shear_stiffness /= n;
310        result.bending_stiffness /= n;
311        result.damping /= n;
312        result.friction /= n;
313        result.stretch /= n;
314        result.thickness /= n;
315
316        result
317    }
318
319    /// Compute total surface density across all layers (kg/m²).
320    pub fn total_surface_density(&self) -> f32 {
321        self.layers.iter().map(|l| l.surface_density).sum()
322    }
323
324    /// Number of layers in the stack.
325    pub fn layer_count(&self) -> usize {
326        self.layers.len()
327    }
328}
329
330impl Default for ClothStack {
331    fn default() -> Self {
332        Self::new()
333    }
334}
335
336// ── Tests ─────────────────────────────────────────────────────────────────────
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn cotton_preset_name() {
344        assert_eq!(ClothMaterial::cotton().name, "cotton");
345    }
346
347    #[test]
348    fn silk_preset_lighter_than_denim() {
349        assert!(
350            ClothMaterial::silk().surface_density < ClothMaterial::denim().surface_density,
351            "silk should be lighter than denim"
352        );
353    }
354
355    #[test]
356    fn denim_is_stiff() {
357        assert!(
358            ClothMaterial::denim().is_stiff(),
359            "denim bending_stiffness should be > 0.7"
360        );
361    }
362
363    #[test]
364    fn silk_is_drapeable() {
365        assert!(
366            ClothMaterial::silk().is_drapeable(),
367            "silk bending_stiffness should be < 0.3"
368        );
369    }
370
371    #[test]
372    fn lerp_at_zero_equals_self() {
373        let cotton = ClothMaterial::cotton();
374        let denim = ClothMaterial::denim();
375        let result = cotton.lerp(&denim, 0.0);
376        assert!(
377            (result.surface_density - cotton.surface_density).abs() < 1e-5,
378            "lerp(t=0) surface_density should match self"
379        );
380        assert!(
381            (result.bending_stiffness - cotton.bending_stiffness).abs() < 1e-5,
382            "lerp(t=0) bending_stiffness should match self"
383        );
384    }
385
386    #[test]
387    fn lerp_at_one_equals_other() {
388        let cotton = ClothMaterial::cotton();
389        let denim = ClothMaterial::denim();
390        let result = cotton.lerp(&denim, 1.0);
391        assert!(
392            (result.surface_density - denim.surface_density).abs() < 1e-5,
393            "lerp(t=1) surface_density should match other"
394        );
395        assert!(
396            (result.bending_stiffness - denim.bending_stiffness).abs() < 1e-5,
397            "lerp(t=1) bending_stiffness should match other"
398        );
399    }
400
401    #[test]
402    fn lerp_midpoint_density() {
403        let cotton = ClothMaterial::cotton(); // density = 0.2
404        let denim = ClothMaterial::denim(); // density = 0.5
405        let result = cotton.lerp(&denim, 0.5);
406        let expected = (0.2 + 0.5) / 2.0;
407        assert!(
408            (result.surface_density - expected).abs() < 1e-5,
409            "lerp(0.5) density expected {expected}, got {}",
410            result.surface_density
411        );
412    }
413
414    #[test]
415    fn with_stiffness_scale_halves_structural() {
416        let cotton = ClothMaterial::cotton(); // structural_stiffness = 0.8
417        let scaled = cotton.with_stiffness_scale(0.5);
418        let expected = 0.8 * 0.5;
419        assert!(
420            (scaled.structural_stiffness - expected).abs() < 1e-5,
421            "expected structural_stiffness {expected}, got {}",
422            scaled.structural_stiffness
423        );
424    }
425
426    #[test]
427    fn for_wind_strong_increases_damping() {
428        let silk = ClothMaterial::silk(); // damping = 0.1
429        let windy = silk.for_wind_strength(1.0);
430        assert!(
431            windy.damping > silk.damping,
432            "strong wind should increase damping: {} -> {}",
433            silk.damping,
434            windy.damping
435        );
436    }
437
438    #[test]
439    fn terminal_velocity_positive() {
440        for mat in ClothMaterial::all_presets() {
441            let tv = mat.terminal_velocity();
442            assert!(
443                tv > 0.0,
444                "terminal_velocity for '{}' should be positive, got {tv}",
445                mat.name
446            );
447        }
448    }
449
450    #[test]
451    fn all_presets_count() {
452        assert_eq!(
453            ClothMaterial::all_presets().len(),
454            6,
455            "expected 6 presets: cotton, silk, denim, leather, rubber, wool"
456        );
457    }
458
459    #[test]
460    fn find_preset_by_name() {
461        let mat = ClothMaterial::find_preset("denim").expect("denim preset should exist");
462        assert_eq!(mat.name, "denim");
463    }
464
465    #[test]
466    fn find_preset_case_insensitive() {
467        let mat = ClothMaterial::find_preset("SILK").expect("SILK should match silk preset");
468        assert_eq!(mat.name, "silk");
469    }
470
471    #[test]
472    fn cloth_stack_effective_material() {
473        let mut stack = ClothStack::new();
474        stack.push(ClothMaterial::cotton()); // density = 0.2
475        stack.push(ClothMaterial::denim()); // density = 0.5
476        let eff = stack.effective_material();
477        let expected_density = (0.2 + 0.5) / 2.0;
478        assert!(
479            (eff.surface_density - expected_density).abs() < 1e-5,
480            "effective density expected {expected_density}, got {}",
481            eff.surface_density
482        );
483    }
484
485    #[test]
486    fn cloth_stack_total_density() {
487        let mut stack = ClothStack::new();
488        stack.push(ClothMaterial::cotton()); // 0.2
489        stack.push(ClothMaterial::silk()); // 0.1
490        stack.push(ClothMaterial::wool()); // 0.3
491        let total = stack.total_surface_density();
492        let expected = 0.2 + 0.1 + 0.3;
493        assert!(
494            (total - expected).abs() < 1e-5,
495            "total density expected {expected}, got {total}"
496        );
497    }
498}