Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## Unreleased

- `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)
- `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)`)
- `CoefficientCombineRule::Min` - now makes sure it uses a non zero value as result by using `coeff1.min(coeff2).abs()`
- `InteractionTestMode`: Specifies which method should be used to test interactions. Supports `AND` and `OR`.
- `CoefficientCombineRule::Sum` - Adds the two coefficients and does a clamp to have at most 1.

## v0.30.1 (17 Oct. 2025)

- Kinematic rigid-bodies will no longer fall asleep if they have a nonzero velocity, even if that velocity is very
Expand Down
6 changes: 4 additions & 2 deletions examples2d/collision_groups2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ pub fn init_world(testbed: &mut Testbed) {
/*
* Setup groups
*/
const GREEN_GROUP: InteractionGroups = InteractionGroups::new(Group::GROUP_1, Group::GROUP_1);
const BLUE_GROUP: InteractionGroups = InteractionGroups::new(Group::GROUP_2, Group::GROUP_2);
const GREEN_GROUP: InteractionGroups =
InteractionGroups::new(Group::GROUP_1, Group::GROUP_1, InteractionTestMode::And);
const BLUE_GROUP: InteractionGroups =
InteractionGroups::new(Group::GROUP_2, Group::GROUP_2, InteractionTestMode::And);

/*
* A green floor that will collide with the GREEN group only.
Expand Down
6 changes: 4 additions & 2 deletions examples3d/collision_groups3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ pub fn init_world(testbed: &mut Testbed) {
/*
* Setup groups
*/
const GREEN_GROUP: InteractionGroups = InteractionGroups::new(Group::GROUP_1, Group::GROUP_1);
const BLUE_GROUP: InteractionGroups = InteractionGroups::new(Group::GROUP_2, Group::GROUP_2);
const GREEN_GROUP: InteractionGroups =
InteractionGroups::new(Group::GROUP_1, Group::GROUP_1, InteractionTestMode::And);
const BLUE_GROUP: InteractionGroups =
InteractionGroups::new(Group::GROUP_2, Group::GROUP_2, InteractionTestMode::And);

/*
* A green floor that will collide with the GREEN group only.
Expand Down
12 changes: 10 additions & 2 deletions examples3d/vehicle_joints3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ pub fn init_world(testbed: &mut Testbed) {

let body_co = ColliderBuilder::cuboid(0.65, 0.3, 0.9)
.density(100.0)
.collision_groups(InteractionGroups::new(CAR_GROUP, !CAR_GROUP));
.collision_groups(InteractionGroups::new(
CAR_GROUP,
!CAR_GROUP,
InteractionTestMode::And,
));
let body_rb = RigidBodyBuilder::dynamic()
.pose(body_position.into())
.build();
Expand Down Expand Up @@ -85,7 +89,11 @@ pub fn init_world(testbed: &mut Testbed) {
// is mathematically simpler than a cylinder and cheaper to compute for collision-detection.
let wheel_co = ColliderBuilder::ball(wheel_radius)
.density(100.0)
.collision_groups(InteractionGroups::new(CAR_GROUP, !CAR_GROUP))
.collision_groups(InteractionGroups::new(
CAR_GROUP,
!CAR_GROUP,
InteractionTestMode::And,
))
.friction(1.0);
let wheel_rb = RigidBodyBuilder::dynamic().pose(wheel_center.into());
let wheel_handle = bodies.insert(wheel_rb);
Expand Down
15 changes: 12 additions & 3 deletions src/dynamics/coefficient_combine_rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ use crate::math::Real;
/// **Most games use Average (the default)** and never change this.
///
/// - **Average** (default): `(friction1 + friction2) / 2` - Balanced, intuitive
/// - **Min**: `min(friction1, friction2)` - "Slippery wins" (ice on any surface = ice)
/// - **Min**: `min(friction1, friction2).abs()` - "Slippery wins" (ice on any surface = ice)
/// - **Multiply**: `friction1 × friction2` - Both must be high for high friction
/// - **Max**: `max(friction1, friction2)` - "Sticky wins" (rubber on any surface = rubber)
/// - **Sum**: `sum(friction1, friction2).clamp(0, 1)` - Sum of both frictions, clamped to range 0, 1.
///
/// ## Example
/// ```
Expand All @@ -26,7 +27,7 @@ use crate::math::Real;
/// ```
///
/// ## Priority System
/// If colliders disagree on rules, the "higher" one wins: Max > Multiply > Min > Average
/// If colliders disagree on rules, the "higher" one wins: Sum > Max > Multiply > Min > Average
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
pub enum CoefficientCombineRule {
Expand All @@ -39,6 +40,8 @@ pub enum CoefficientCombineRule {
Multiply = 2,
/// Use the larger value ("sticky/bouncy wins").
Max = 3,
/// The sum of the two coefficients.
Sum = 4,
}

impl CoefficientCombineRule {
Expand All @@ -52,9 +55,15 @@ impl CoefficientCombineRule {

match effective_rule {
CoefficientCombineRule::Average => (coeff1 + coeff2) / 2.0,
CoefficientCombineRule::Min => coeff1.min(coeff2),
CoefficientCombineRule::Min => {
// Even though coeffs are meant to be positive, godot use-case has negative values.
// We're following their logic here.
// Context: https://github.com/dimforge/rapier/pull/741#discussion_r1862402948
coeff1.min(coeff2).abs()
}
CoefficientCombineRule::Multiply => coeff1 * coeff2,
CoefficientCombineRule::Max => coeff1.max(coeff2),
CoefficientCombineRule::Sum => (coeff1 + coeff2).clamp(0.0, 1.0),
}
}
}
81 changes: 69 additions & 12 deletions src/geometry/interaction_groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,20 @@
/// - **Memberships**: What groups does this collider belong to? (up to 32 groups)
/// - **Filter**: What groups can this collider interact with?
///
/// Two colliders interact only if:
/// 1. Collider A's memberships overlap with Collider B's filter, AND
/// 2. Collider B's memberships overlap with Collider A's filter
/// An interaction is allowed between two colliders `a` and `b` when two conditions
/// are met simultaneously for [`InteractionTestMode::And`] or individually for [`InteractionTestMode::Or`]::
/// - The groups membership of `a` has at least one bit set to `1` in common with the groups filter of `b`.
/// - The groups membership of `b` has at least one bit set to `1` in common with the groups filter of `a`.
///
/// In other words, interactions are allowed between two colliders iff. the following condition is met
/// for [`InteractionTestMode::And`]:
/// ```ignore
/// (self.memberships.bits() & rhs.filter.bits()) != 0 && (rhs.memberships.bits() & self.filter.bits()) != 0
/// ```
/// or for [`InteractionTestMode::Or`]:
/// ```ignore
/// (self.memberships.bits() & rhs.filter.bits()) != 0 || (rhs.memberships.bits() & self.filter.bits()) != 0
/// ```
/// # Common use cases
///
/// - **Player vs. Enemy bullets**: Players in group 1, enemies in group 2. Player bullets
Expand All @@ -18,18 +28,20 @@
///
/// # Example
///
/// ```
/// ```ignore
/// # use rapier3d::geometry::{InteractionGroups, Group};
/// // Player collider: in group 1, collides with groups 2 and 3
/// let player_groups = InteractionGroups::new(
/// Group::GROUP_1, // I am in group 1
/// Group::GROUP_2 | Group::GROUP_3 // I collide with groups 2 and 3
/// Group::GROUP_1, // I am in group 1
/// Group::GROUP_2, | Group::GROUP_3, // I collide with groups 2 and 3
/// InteractionTestMode::And
/// );
///
/// // Enemy collider: in group 2, collides with group 1
/// let enemy_groups = InteractionGroups::new(
/// Group::GROUP_2, // I am in group 2
/// Group::GROUP_1 // I collide with group 1
/// Group::GROUP_1, // I collide with group 1
/// InteractionTestMode::And
/// );
///
/// // These will collide because:
Expand All @@ -45,29 +57,49 @@ pub struct InteractionGroups {
pub memberships: Group,
/// Groups filter.
pub filter: Group,
/// Interaction test mode
///
/// In case of different test modes between two [`InteractionGroups`], [`InteractionTestMode::And`] is given priority.
pub test_mode: InteractionTestMode,
}

#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Default)]
/// Specifies which method should be used to test interactions.
///
/// In case of different test modes between two [`InteractionGroups`], [`InteractionTestMode::And`] is given priority.
pub enum InteractionTestMode {
/// Use [`InteractionGroups::test_and`].
#[default]
And,
/// Use [`InteractionGroups::test_or`], iff. the `rhs` is also [`InteractionTestMode::Or`].
///
/// If the `rhs` is not [`InteractionTestMode::Or`], use [`InteractionGroups::test_and`].
Or,
}

impl InteractionGroups {
/// Initializes with the given interaction groups and interaction mask.
pub const fn new(memberships: Group, filter: Group) -> Self {
pub const fn new(memberships: Group, filter: Group, test_mode: InteractionTestMode) -> Self {
Self {
memberships,
filter,
test_mode,
}
}

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

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

/// Sets the group this filter is part of.
Expand All @@ -85,21 +117,46 @@ impl InteractionGroups {
/// Check if interactions should be allowed based on the interaction memberships and filter.
///
/// An interaction is allowed iff. the memberships of `self` contain at least one bit set to 1 in common
/// with the filter of `rhs`, and vice-versa.
/// with the filter of `rhs`, **and** vice-versa.
#[inline]
pub const fn test(self, rhs: Self) -> bool {
pub const fn test_and(self, rhs: Self) -> bool {
// NOTE: since const ops is not stable, we have to convert `Group` into u32
// to use & operator in const context.
(self.memberships.bits() & rhs.filter.bits()) != 0
&& (rhs.memberships.bits() & self.filter.bits()) != 0
}

/// Check if interactions should be allowed based on the interaction memberships and filter.
///
/// An interaction is allowed iff. the groups of `self` contain at least one bit set to 1 in common
/// with the mask of `rhs`, **or** vice-versa.
#[inline]
pub const fn test_or(self, rhs: Self) -> bool {
// NOTE: since const ops is not stable, we have to convert `Group` into u32
// to use & operator in const context.
(self.memberships.bits() & rhs.filter.bits()) != 0
|| (rhs.memberships.bits() & self.filter.bits()) != 0
}

/// Check if interactions should be allowed based on the interaction memberships and filter.
///
/// See [`InteractionTestMode`] for more info.
#[inline]
pub const fn test(self, rhs: Self) -> bool {
match (self.test_mode, rhs.test_mode) {
(InteractionTestMode::And, _) => self.test_and(rhs),
(InteractionTestMode::Or, InteractionTestMode::And) => self.test_and(rhs),
(InteractionTestMode::Or, InteractionTestMode::Or) => self.test_or(rhs),
}
}
}

impl Default for InteractionGroups {
fn default() -> Self {
Self {
memberships: Group::GROUP_1,
filter: Group::ALL,
test_mode: InteractionTestMode::And,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/geometry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub use self::contact_pair::{
pub use self::interaction_graph::{
ColliderGraphIndex, InteractionGraph, RigidBodyGraphIndex, TemporaryInteractionIndex,
};
pub use self::interaction_groups::{Group, InteractionGroups};
pub use self::interaction_groups::{Group, InteractionGroups, InteractionTestMode};
pub use self::mesh_converter::{MeshConverter, MeshConverterError};
pub use self::narrow_phase::NarrowPhase;

Expand Down