Implement interaction groups test mode and add the ClampedSum cofficient combine rule (#741)
This commit is contained in:
@@ -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::ClampedSum` - 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
/// - **ClampedSum**: `sum(friction1, friction2).clamp(0, 1)` - Sum of both frictions, clamped to range 0, 1.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```
|
||||
@@ -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: ClampedSum > Max > Multiply > Min > Average
|
||||
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
|
||||
pub enum CoefficientCombineRule {
|
||||
@@ -39,6 +40,8 @@ pub enum CoefficientCombineRule {
|
||||
Multiply = 2,
|
||||
/// Use the larger value ("sticky/bouncy wins").
|
||||
Max = 3,
|
||||
/// The clamped sum of the two coefficients.
|
||||
ClampedSum = 4,
|
||||
}
|
||||
|
||||
impl CoefficientCombineRule {
|
||||
@@ -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::ClampedSum => (coeff1 + coeff2).clamp(0.0, 1.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -45,14 +57,34 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,14 +92,14 @@ impl InteractionGroups {
|
||||
///
|
||||
/// 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.
|
||||
@@ -85,14 +117,38 @@ 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 {
|
||||
@@ -100,6 +156,7 @@ impl Default for InteractionGroups {
|
||||
Self {
|
||||
memberships: Group::GROUP_1,
|
||||
filter: Group::ALL,
|
||||
test_mode: InteractionTestMode::And,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user