rapier3d/dynamics/
coefficient_combine_rule.rs

1use crate::math::Real;
2
3/// How to combine friction/restitution values when two colliders touch.
4///
5/// When two colliders with different friction (or restitution) values collide, Rapier
6/// needs to decide what the effective friction/restitution should be. Each collider has
7/// a combine rule, and the "stronger" rule wins (Max > Multiply > Min > Average).
8///
9/// ## Combine Rules
10///
11/// **Most games use Average (the default)** and never change this.
12///
13/// - **Average** (default): `(friction1 + friction2) / 2` - Balanced, intuitive
14/// - **Min**: `min(friction1, friction2).abs()` - "Slippery wins" (ice on any surface = ice)
15/// - **Multiply**: `friction1 × friction2` - Both must be high for high friction
16/// - **Max**: `max(friction1, friction2)` - "Sticky wins" (rubber on any surface = rubber)
17/// - **ClampedSum**: `sum(friction1, friction2).clamp(0, 1)` - Sum of both frictions, clamped to range 0, 1.
18///
19/// ## Example
20/// ```
21/// # use rapier3d::prelude::*;
22/// // Ice collider that makes everything slippery
23/// let ice = ColliderBuilder::cuboid(10.0, 0.1, 10.0)
24///     .friction(0.0)
25///     .friction_combine_rule(CoefficientCombineRule::Min)  // Ice wins!
26///     .build();
27/// ```
28///
29/// ## Priority System
30/// If colliders disagree on rules, the "higher" one wins: ClampedSum > Max > Multiply > Min > Average
31#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
32#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
33pub enum CoefficientCombineRule {
34    /// Average the two values (default, most common).
35    #[default]
36    Average = 0,
37    /// Use the smaller value ("slippery/soft wins").
38    Min = 1,
39    /// Multiply the two values (both must be high).
40    Multiply = 2,
41    /// Use the larger value ("sticky/bouncy wins").
42    Max = 3,
43    /// The clamped sum of the two coefficients.
44    ClampedSum = 4,
45}
46
47impl CoefficientCombineRule {
48    pub(crate) fn combine(
49        coeff1: Real,
50        coeff2: Real,
51        rule_value1: CoefficientCombineRule,
52        rule_value2: CoefficientCombineRule,
53    ) -> Real {
54        let effective_rule = rule_value1.max(rule_value2);
55
56        match effective_rule {
57            CoefficientCombineRule::Average => (coeff1 + coeff2) / 2.0,
58            CoefficientCombineRule::Min => {
59                // Even though coeffs are meant to be positive, godot use-case has negative values.
60                // We're following their logic here.
61                // Context: https://github.com/dimforge/rapier/pull/741#discussion_r1862402948
62                coeff1.min(coeff2).abs()
63            }
64            CoefficientCombineRule::Multiply => coeff1 * coeff2,
65            CoefficientCombineRule::Max => coeff1.max(coeff2),
66            CoefficientCombineRule::ClampedSum => (coeff1 + coeff2).clamp(0.0, 1.0),
67        }
68    }
69}