Skip to content

Commit 57c4e91

Browse files
authored
Implement interaction groups test mode and add the ClampedSum cofficient combine rule (#741)
1 parent 36f91a6 commit 57c4e91

File tree

7 files changed

+108
-22
lines changed

7 files changed

+108
-22
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## Unreleased
2+
3+
- `InteractionGroups` struct now contains `InteractionTestMode`. Continues [rapier/pull/170](https://github.com/dimforge/rapier/pull/170) for [rapier/issues/622](https://github.com/dimforge/rapier/issues/622)
4+
- `InteractionGroups` constructor now requires an `InteractionTestMode` parameter. If you want same behaviour as before, use `InteractionTestMode::And` (eg. `InteractionGroups::new(Group::GROUP_1, Group::GROUP_1, InteractionTestMode::And)`)
5+
- `CoefficientCombineRule::Min` - now makes sure it uses a non zero value as result by using `coeff1.min(coeff2).abs()`
6+
- `InteractionTestMode`: Specifies which method should be used to test interactions. Supports `AND` and `OR`.
7+
- `CoefficientCombineRule::ClampedSum` - Adds the two coefficients and does a clamp to have at most 1.
8+
19
## v0.30.1 (17 Oct. 2025)
210

311
- Kinematic rigid-bodies will no longer fall asleep if they have a nonzero velocity, even if that velocity is very

examples2d/collision_groups2.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ pub fn init_world(testbed: &mut Testbed) {
2424
/*
2525
* Setup groups
2626
*/
27-
const GREEN_GROUP: InteractionGroups = InteractionGroups::new(Group::GROUP_1, Group::GROUP_1);
28-
const BLUE_GROUP: InteractionGroups = InteractionGroups::new(Group::GROUP_2, Group::GROUP_2);
27+
const GREEN_GROUP: InteractionGroups =
28+
InteractionGroups::new(Group::GROUP_1, Group::GROUP_1, InteractionTestMode::And);
29+
const BLUE_GROUP: InteractionGroups =
30+
InteractionGroups::new(Group::GROUP_2, Group::GROUP_2, InteractionTestMode::And);
2931

3032
/*
3133
* A green floor that will collide with the GREEN group only.

examples3d/collision_groups3.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ pub fn init_world(testbed: &mut Testbed) {
2424
/*
2525
* Setup groups
2626
*/
27-
const GREEN_GROUP: InteractionGroups = InteractionGroups::new(Group::GROUP_1, Group::GROUP_1);
28-
const BLUE_GROUP: InteractionGroups = InteractionGroups::new(Group::GROUP_2, Group::GROUP_2);
27+
const GREEN_GROUP: InteractionGroups =
28+
InteractionGroups::new(Group::GROUP_1, Group::GROUP_1, InteractionTestMode::And);
29+
const BLUE_GROUP: InteractionGroups =
30+
InteractionGroups::new(Group::GROUP_2, Group::GROUP_2, InteractionTestMode::And);
2931

3032
/*
3133
* A green floor that will collide with the GREEN group only.

examples3d/vehicle_joints3.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ pub fn init_world(testbed: &mut Testbed) {
5353

5454
let body_co = ColliderBuilder::cuboid(0.65, 0.3, 0.9)
5555
.density(100.0)
56-
.collision_groups(InteractionGroups::new(CAR_GROUP, !CAR_GROUP));
56+
.collision_groups(InteractionGroups::new(
57+
CAR_GROUP,
58+
!CAR_GROUP,
59+
InteractionTestMode::And,
60+
));
5761
let body_rb = RigidBodyBuilder::dynamic()
5862
.pose(body_position.into())
5963
.build();
@@ -85,7 +89,11 @@ pub fn init_world(testbed: &mut Testbed) {
8589
// is mathematically simpler than a cylinder and cheaper to compute for collision-detection.
8690
let wheel_co = ColliderBuilder::ball(wheel_radius)
8791
.density(100.0)
88-
.collision_groups(InteractionGroups::new(CAR_GROUP, !CAR_GROUP))
92+
.collision_groups(InteractionGroups::new(
93+
CAR_GROUP,
94+
!CAR_GROUP,
95+
InteractionTestMode::And,
96+
))
8997
.friction(1.0);
9098
let wheel_rb = RigidBodyBuilder::dynamic().pose(wheel_center.into());
9199
let wheel_handle = bodies.insert(wheel_rb);

src/dynamics/coefficient_combine_rule.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ use crate::math::Real;
1111
/// **Most games use Average (the default)** and never change this.
1212
///
1313
/// - **Average** (default): `(friction1 + friction2) / 2` - Balanced, intuitive
14-
/// - **Min**: `min(friction1, friction2)` - "Slippery wins" (ice on any surface = ice)
14+
/// - **Min**: `min(friction1, friction2).abs()` - "Slippery wins" (ice on any surface = ice)
1515
/// - **Multiply**: `friction1 × friction2` - Both must be high for high friction
1616
/// - **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.
1718
///
1819
/// ## Example
1920
/// ```
@@ -26,7 +27,7 @@ use crate::math::Real;
2627
/// ```
2728
///
2829
/// ## Priority System
29-
/// If colliders disagree on rules, the "higher" one wins: Max > Multiply > Min > Average
30+
/// If colliders disagree on rules, the "higher" one wins: ClampedSum > Max > Multiply > Min > Average
3031
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
3132
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
3233
pub enum CoefficientCombineRule {
@@ -39,6 +40,8 @@ pub enum CoefficientCombineRule {
3940
Multiply = 2,
4041
/// Use the larger value ("sticky/bouncy wins").
4142
Max = 3,
43+
/// The clamped sum of the two coefficients.
44+
ClampedSum = 4,
4245
}
4346

4447
impl CoefficientCombineRule {
@@ -52,9 +55,15 @@ impl CoefficientCombineRule {
5255

5356
match effective_rule {
5457
CoefficientCombineRule::Average => (coeff1 + coeff2) / 2.0,
55-
CoefficientCombineRule::Min => coeff1.min(coeff2),
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+
}
5664
CoefficientCombineRule::Multiply => coeff1 * coeff2,
5765
CoefficientCombineRule::Max => coeff1.max(coeff2),
66+
CoefficientCombineRule::ClampedSum => (coeff1 + coeff2).clamp(0.0, 1.0),
5867
}
5968
}
6069
}

src/geometry/interaction_groups.rs

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,20 @@
66
/// - **Memberships**: What groups does this collider belong to? (up to 32 groups)
77
/// - **Filter**: What groups can this collider interact with?
88
///
9-
/// Two colliders interact only if:
10-
/// 1. Collider A's memberships overlap with Collider B's filter, AND
11-
/// 2. Collider B's memberships overlap with Collider A's filter
9+
/// An interaction is allowed between two colliders `a` and `b` when two conditions
10+
/// are met simultaneously for [`InteractionTestMode::And`] or individually for [`InteractionTestMode::Or`]::
11+
/// - The groups membership of `a` has at least one bit set to `1` in common with the groups filter of `b`.
12+
/// - The groups membership of `b` has at least one bit set to `1` in common with the groups filter of `a`.
1213
///
14+
/// In other words, interactions are allowed between two colliders iff. the following condition is met
15+
/// for [`InteractionTestMode::And`]:
16+
/// ```ignore
17+
/// (self.memberships.bits() & rhs.filter.bits()) != 0 && (rhs.memberships.bits() & self.filter.bits()) != 0
18+
/// ```
19+
/// or for [`InteractionTestMode::Or`]:
20+
/// ```ignore
21+
/// (self.memberships.bits() & rhs.filter.bits()) != 0 || (rhs.memberships.bits() & self.filter.bits()) != 0
22+
/// ```
1323
/// # Common use cases
1424
///
1525
/// - **Player vs. Enemy bullets**: Players in group 1, enemies in group 2. Player bullets
@@ -18,18 +28,20 @@
1828
///
1929
/// # Example
2030
///
21-
/// ```
31+
/// ```ignore
2232
/// # use rapier3d::geometry::{InteractionGroups, Group};
2333
/// // Player collider: in group 1, collides with groups 2 and 3
2434
/// let player_groups = InteractionGroups::new(
25-
/// Group::GROUP_1, // I am in group 1
26-
/// Group::GROUP_2 | Group::GROUP_3 // I collide with groups 2 and 3
35+
/// Group::GROUP_1, // I am in group 1
36+
/// Group::GROUP_2, | Group::GROUP_3, // I collide with groups 2 and 3
37+
/// InteractionTestMode::And
2738
/// );
2839
///
2940
/// // Enemy collider: in group 2, collides with group 1
3041
/// let enemy_groups = InteractionGroups::new(
3142
/// Group::GROUP_2, // I am in group 2
32-
/// Group::GROUP_1 // I collide with group 1
43+
/// Group::GROUP_1, // I collide with group 1
44+
/// InteractionTestMode::And
3345
/// );
3446
///
3547
/// // These will collide because:
@@ -45,29 +57,49 @@ pub struct InteractionGroups {
4557
pub memberships: Group,
4658
/// Groups filter.
4759
pub filter: Group,
60+
/// Interaction test mode
61+
///
62+
/// In case of different test modes between two [`InteractionGroups`], [`InteractionTestMode::And`] is given priority.
63+
pub test_mode: InteractionTestMode,
64+
}
65+
66+
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
67+
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Default)]
68+
/// Specifies which method should be used to test interactions.
69+
///
70+
/// In case of different test modes between two [`InteractionGroups`], [`InteractionTestMode::And`] is given priority.
71+
pub enum InteractionTestMode {
72+
/// Use [`InteractionGroups::test_and`].
73+
#[default]
74+
And,
75+
/// Use [`InteractionGroups::test_or`], iff. the `rhs` is also [`InteractionTestMode::Or`].
76+
///
77+
/// If the `rhs` is not [`InteractionTestMode::Or`], use [`InteractionGroups::test_and`].
78+
Or,
4879
}
4980

5081
impl InteractionGroups {
5182
/// Initializes with the given interaction groups and interaction mask.
52-
pub const fn new(memberships: Group, filter: Group) -> Self {
83+
pub const fn new(memberships: Group, filter: Group, test_mode: InteractionTestMode) -> Self {
5384
Self {
5485
memberships,
5586
filter,
87+
test_mode,
5688
}
5789
}
5890

5991
/// Creates a filter that allows interactions with everything (default behavior).
6092
///
6193
/// The collider is in all groups and collides with all groups.
6294
pub const fn all() -> Self {
63-
Self::new(Group::ALL, Group::ALL)
95+
Self::new(Group::ALL, Group::ALL, InteractionTestMode::And)
6496
}
6597

6698
/// Creates a filter that prevents all interactions.
6799
///
68100
/// The collider won't collide with anything. Useful for temporarily disabled colliders.
69101
pub const fn none() -> Self {
70-
Self::new(Group::NONE, Group::NONE)
102+
Self::new(Group::NONE, Group::NONE, InteractionTestMode::And)
71103
}
72104

73105
/// Sets the group this filter is part of.
@@ -85,21 +117,46 @@ impl InteractionGroups {
85117
/// Check if interactions should be allowed based on the interaction memberships and filter.
86118
///
87119
/// An interaction is allowed iff. the memberships of `self` contain at least one bit set to 1 in common
88-
/// with the filter of `rhs`, and vice-versa.
120+
/// with the filter of `rhs`, **and** vice-versa.
89121
#[inline]
90-
pub const fn test(self, rhs: Self) -> bool {
122+
pub const fn test_and(self, rhs: Self) -> bool {
91123
// NOTE: since const ops is not stable, we have to convert `Group` into u32
92124
// to use & operator in const context.
93125
(self.memberships.bits() & rhs.filter.bits()) != 0
94126
&& (rhs.memberships.bits() & self.filter.bits()) != 0
95127
}
128+
129+
/// Check if interactions should be allowed based on the interaction memberships and filter.
130+
///
131+
/// An interaction is allowed iff. the groups of `self` contain at least one bit set to 1 in common
132+
/// with the mask of `rhs`, **or** vice-versa.
133+
#[inline]
134+
pub const fn test_or(self, rhs: Self) -> bool {
135+
// NOTE: since const ops is not stable, we have to convert `Group` into u32
136+
// to use & operator in const context.
137+
(self.memberships.bits() & rhs.filter.bits()) != 0
138+
|| (rhs.memberships.bits() & self.filter.bits()) != 0
139+
}
140+
141+
/// Check if interactions should be allowed based on the interaction memberships and filter.
142+
///
143+
/// See [`InteractionTestMode`] for more info.
144+
#[inline]
145+
pub const fn test(self, rhs: Self) -> bool {
146+
match (self.test_mode, rhs.test_mode) {
147+
(InteractionTestMode::And, _) => self.test_and(rhs),
148+
(InteractionTestMode::Or, InteractionTestMode::And) => self.test_and(rhs),
149+
(InteractionTestMode::Or, InteractionTestMode::Or) => self.test_or(rhs),
150+
}
151+
}
96152
}
97153

98154
impl Default for InteractionGroups {
99155
fn default() -> Self {
100156
Self {
101157
memberships: Group::GROUP_1,
102158
filter: Group::ALL,
159+
test_mode: InteractionTestMode::And,
103160
}
104161
}
105162
}

src/geometry/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub use self::contact_pair::{
1212
pub use self::interaction_graph::{
1313
ColliderGraphIndex, InteractionGraph, RigidBodyGraphIndex, TemporaryInteractionIndex,
1414
};
15-
pub use self::interaction_groups::{Group, InteractionGroups};
15+
pub use self::interaction_groups::{Group, InteractionGroups, InteractionTestMode};
1616
pub use self::mesh_converter::{MeshConverter, MeshConverterError};
1717
pub use self::narrow_phase::NarrowPhase;
1818

0 commit comments

Comments
 (0)