diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ac5c86cb..a9504e5d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/examples2d/collision_groups2.rs b/examples2d/collision_groups2.rs index 5b6c3f267..88e9c0c7f 100644 --- a/examples2d/collision_groups2.rs +++ b/examples2d/collision_groups2.rs @@ -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. diff --git a/examples3d/collision_groups3.rs b/examples3d/collision_groups3.rs index e3f70b43d..7cca88299 100644 --- a/examples3d/collision_groups3.rs +++ b/examples3d/collision_groups3.rs @@ -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. diff --git a/examples3d/vehicle_joints3.rs b/examples3d/vehicle_joints3.rs index 7c4b1c48c..859863e3b 100644 --- a/examples3d/vehicle_joints3.rs +++ b/examples3d/vehicle_joints3.rs @@ -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); diff --git a/src/dynamics/coefficient_combine_rule.rs b/src/dynamics/coefficient_combine_rule.rs index b6f6373db..b0286c77c 100644 --- a/src/dynamics/coefficient_combine_rule.rs +++ b/src/dynamics/coefficient_combine_rule.rs @@ -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), } } } diff --git a/src/geometry/interaction_groups.rs b/src/geometry/interaction_groups.rs index e27a2b34d..e43637986 100644 --- a/src/geometry/interaction_groups.rs +++ b/src/geometry/interaction_groups.rs @@ -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, } } } diff --git a/src/geometry/mod.rs b/src/geometry/mod.rs index 00d86fc15..6547ad935 100644 --- a/src/geometry/mod.rs +++ b/src/geometry/mod.rs @@ -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;