rapier2d/dynamics/coefficient_combine_rule.rs
1use crate::math::Real;
2// Provides `sqrt` in no-std builds (same pattern as island_manager/local_split.rs).
3#[allow(unused_imports)]
4use simba::scalar::ComplexField as _;
5
6/// How to combine friction/restitution values when two colliders touch.
7///
8/// When two colliders with different friction (or restitution) values collide, Rapier
9/// needs to decide what the effective friction/restitution should be. Each collider has
10/// a combine rule, and the "stronger" rule wins
11/// (GeometricMean > ClampedSum > Max > Multiply > Min > Average).
12///
13/// ## Combine Rules
14///
15/// **Most games use Average (the default)** and never change this.
16///
17/// - **Average** (default): `(friction1 + friction2) / 2` - Balanced, intuitive
18/// - **Min**: `min(friction1, friction2).abs()` - "Slippery wins" (ice on any surface = ice)
19/// - **Multiply**: `friction1 × friction2` - Both must be high for high friction
20/// - **Max**: `max(friction1, friction2)` - "Sticky wins" (rubber on any surface = rubber)
21/// - **ClampedSum**: `sum(friction1, friction2).clamp(0, 1)` - Sum of both frictions, clamped to range 0, 1.
22/// - **GeometricMean**: `sqrt(friction1 × friction2)` - Between Multiply and Average; zero if either is zero.
23///
24/// ## Example
25/// ```
26/// # use rapier3d::prelude::*;
27/// // Ice collider that makes everything slippery
28/// let ice = ColliderBuilder::cuboid(10.0, 0.1, 10.0)
29/// .friction(0.0)
30/// .friction_combine_rule(CoefficientCombineRule::Min) // Ice wins!
31/// .build();
32/// ```
33///
34/// ## Priority System
35/// If colliders disagree on rules, the "higher" one wins:
36/// GeometricMean > ClampedSum > Max > Multiply > Min > Average
37#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
38#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
39pub enum CoefficientCombineRule {
40 /// Average the two values (default, most common).
41 #[default]
42 Average = 0,
43 /// Use the smaller value ("slippery/soft wins").
44 Min = 1,
45 /// Multiply the two values (both must be high).
46 Multiply = 2,
47 /// Use the larger value ("sticky/bouncy wins").
48 Max = 3,
49 /// The clamped sum of the two coefficients.
50 ClampedSum = 4,
51 /// The square root of the product of the two values.
52 ///
53 /// A common convention in other engines (e.g. Bullet, PhysX): stricter than Average
54 /// (either value being zero results in zero) but less aggressive than Multiply for
55 /// values below 1.
56 GeometricMean = 5,
57}
58
59impl CoefficientCombineRule {
60 /// Combines two coefficients according to the given combine rules.
61 ///
62 /// If the two combine rules differ, the one with the higher discriminant wins:
63 ///
64 /// ```text
65 /// GeometricMean > ClampedSum > Max > Multiply > Min > Average
66 /// ```
67 pub fn combine(
68 coeff1: Real,
69 coeff2: Real,
70 rule1: CoefficientCombineRule,
71 rule2: CoefficientCombineRule,
72 ) -> Real {
73 let effective_rule = rule1.max(rule2);
74
75 match effective_rule {
76 CoefficientCombineRule::Average => (coeff1 + coeff2) / 2.0,
77 CoefficientCombineRule::Min => {
78 // Even though coeffs are meant to be positive, godot use-case has negative values.
79 // We're following their logic here.
80 // Context: https://github.com/dimforge/rapier/pull/741#discussion_r1862402948
81 coeff1.min(coeff2).abs()
82 }
83 CoefficientCombineRule::Multiply => coeff1 * coeff2,
84 CoefficientCombineRule::Max => coeff1.max(coeff2),
85 CoefficientCombineRule::ClampedSum => (coeff1 + coeff2).clamp(0.0, 1.0),
86 // Negative coefficients are tolerated (see the Min comment above), so clamp
87 // before taking the square root to avoid NaN on a negative product.
88 CoefficientCombineRule::GeometricMean => (coeff1.max(0.0) * coeff2.max(0.0)).sqrt(),
89 }
90 }
91}
92
93#[cfg(test)]
94mod test {
95 use super::CoefficientCombineRule;
96 use crate::math::Real;
97
98 fn combine(c1: Real, c2: Real, rule: CoefficientCombineRule) -> Real {
99 CoefficientCombineRule::combine(c1, c2, rule, rule)
100 }
101
102 #[test]
103 fn geometric_mean_combine() {
104 assert_eq!(
105 combine(0.25, 1.0, CoefficientCombineRule::GeometricMean),
106 0.5
107 );
108 assert_eq!(
109 combine(0.7, 0.0, CoefficientCombineRule::GeometricMean),
110 0.0
111 );
112 // Negative coefficients (tolerated for the godot use-case) must not produce NaN.
113 assert_eq!(
114 combine(-0.5, 0.5, CoefficientCombineRule::GeometricMean),
115 0.0
116 );
117 assert_eq!(
118 combine(-0.5, -0.5, CoefficientCombineRule::GeometricMean),
119 0.0
120 );
121 }
122
123 #[test]
124 fn geometric_mean_wins_rule_priority() {
125 assert_eq!(
126 CoefficientCombineRule::combine(
127 0.25,
128 1.0,
129 CoefficientCombineRule::GeometricMean,
130 CoefficientCombineRule::Average,
131 ),
132 0.5
133 );
134 }
135}