Skip to main content

bevy_rapier2d/pipeline/
physics_hooks.rs

1use bevy::{ecs::system::SystemParam, prelude::*};
2use rapier::{
3    geometry::{ContactManifold, SoftPairContacts, SolverContact, SolverContacts},
4    pipeline::{ContactModificationContext, ModifiableContacts, PairFilterContext},
5    prelude::{PhysicsHooks, SolverFlags},
6};
7
8use crate::math::{Real, Vect};
9
10/// Read-only access to the properties of a collision pair filter context.
11pub struct PairFilterContextView<'a> {
12    /// The raw context from Rapier.
13    pub raw: &'a PairFilterContext<'a>,
14}
15
16impl PairFilterContextView<'_> {
17    /// The entity of the first collider involved in the potential collision.
18    pub fn collider1(&self) -> Entity {
19        let co1 = &self.raw.colliders[self.raw.collider1];
20        Entity::from_bits(co1.user_data as u64)
21    }
22
23    /// The entity of the second collider involved in the potential collision.
24    pub fn collider2(&self) -> Entity {
25        let co2 = &self.raw.colliders[self.raw.collider2];
26        Entity::from_bits(co2.user_data as u64)
27    }
28
29    /// The entity of the first rigid-body (if `self.collider1()` is attached to a rigid-body)
30    /// involved in the potential collision.
31    pub fn rigid_body1(&self) -> Option<Entity> {
32        self.raw.rigid_body1.map(|h| {
33            let co1 = &self.raw.bodies[h];
34            Entity::from_bits(co1.user_data as u64)
35        })
36    }
37
38    /// The entity of the second rigid-body (if `self.collider1()` is attached to a rigid-body)
39    /// involved in the potential collision.
40    pub fn rigid_body2(&self) -> Option<Entity> {
41        self.raw.rigid_body2.map(|h| {
42            let co2 = &self.raw.bodies[h];
43            Entity::from_bits(co2.user_data as u64)
44        })
45    }
46}
47
48/// Read-write access to the properties of a contact modification context.
49pub struct ContactModificationContextView<'a, 'b> {
50    /// The raw context from Rapier.
51    pub raw: &'a mut ContactModificationContext<'b>,
52}
53
54impl ContactModificationContextView<'_, '_> {
55    /// The entity of the first collider involved in the potential collision.
56    pub fn collider1(&self) -> Entity {
57        let co1 = &self.raw.colliders[self.raw.collider1];
58        Entity::from_bits(co1.user_data as u64)
59    }
60
61    /// The entity of the second collider involved in the potential collision.
62    pub fn collider2(&self) -> Entity {
63        let co2 = &self.raw.colliders[self.raw.collider2];
64        Entity::from_bits(co2.user_data as u64)
65    }
66
67    /// The entity of the first rigid-body (if `self.collider1()` is attached to a rigid-body)
68    /// involved in the potential collision.
69    pub fn rigid_body1(&self) -> Option<Entity> {
70        self.raw.rigid_body1.map(|h| {
71            let co1 = &self.raw.bodies[h];
72            Entity::from_bits(co1.user_data as u64)
73        })
74    }
75
76    /// The entity of the second rigid-body (if `self.collider1()` is attached to a rigid-body)
77    /// involved in the potential collision.
78    pub fn rigid_body2(&self) -> Option<Entity> {
79        self.raw.rigid_body2.map(|h| {
80            let co2 = &self.raw.bodies[h];
81            Entity::from_bits(co2.user_data as u64)
82        })
83    }
84}
85
86impl ContactModificationContextView<'_, '_> {
87    /// Is this a pair with contact manifolds (every pair except two soft surfaces)?
88    ///
89    /// All the manifold accessors of this view (e.g. [`Self::normal`], [`Self::friction`],
90    /// [`Self::solver_contacts`]) are only meaningful for such pairs.
91    pub fn is_rigid(&self) -> bool {
92        matches!(self.raw.contacts, ModifiableContacts::Rigid(_))
93    }
94
95    /// Is this a pair of two soft surfaces?
96    ///
97    /// The contacts of such pairs are contact candidates rather than a manifold. They can be
98    /// accessed through [`Self::soft`] and [`Self::soft_mut`]; the manifold accessors of this
99    /// view return `None` (or do nothing) for them.
100    pub fn is_soft(&self) -> bool {
101        matches!(self.raw.contacts, ModifiableContacts::Soft(_))
102    }
103
104    /// The contact candidates of a pair of two soft surfaces (`None` for other pairs).
105    pub fn soft(&self) -> Option<&SoftPairContacts> {
106        self.raw.soft()
107    }
108
109    /// Mutable access to the contact candidates of a pair of two soft surfaces (`None` for other
110    /// pairs).
111    ///
112    /// A candidate's `dir` and `dist` can be modified, or its `enabled` flag cleared to drop it;
113    /// [`SoftPairContacts::disable_all`] ignores the whole pair in the constraints solver.
114    pub fn soft_mut(&mut self) -> Option<&mut SoftPairContacts> {
115        self.raw.soft_mut()
116    }
117
118    /// The contact manifold being modified (`None` for a pair of two soft surfaces).
119    pub fn manifold(&self) -> Option<&ContactManifold> {
120        self.raw.rigid().map(|m| m.manifold)
121    }
122
123    /// The world-space contact normal of the manifold (`None` for a pair of two soft surfaces).
124    pub fn normal(&self) -> Option<Vect> {
125        self.raw.rigid().map(|m| *m.normal)
126    }
127
128    /// Sets the world-space contact normal of the manifold (ignored for a pair of two soft surfaces).
129    pub fn set_normal(&mut self, normal: Vect) {
130        if let Some(m) = self.raw.rigid_mut() {
131            *m.normal = normal;
132        }
133    }
134
135    /// The friction coefficient applied to every solver contact of the manifold (`None` for a
136    /// pair of two soft surfaces).
137    pub fn friction(&self) -> Option<Real> {
138        self.raw.rigid().map(|m| *m.friction)
139    }
140
141    /// Sets the friction coefficient applied to every solver contact of the manifold (ignored for a
142    /// pair of two soft surfaces).
143    pub fn set_friction(&mut self, friction: Real) {
144        if let Some(m) = self.raw.rigid_mut() {
145            *m.friction = friction;
146        }
147    }
148
149    /// The restitution coefficient applied to every solver contact of the manifold (`None` for a
150    /// pair of two soft surfaces).
151    pub fn restitution(&self) -> Option<Real> {
152        self.raw.rigid().map(|m| *m.restitution)
153    }
154
155    /// Sets the restitution coefficient applied to every solver contact of the manifold (ignored
156    /// for a pair of two soft surfaces).
157    pub fn set_restitution(&mut self, restitution: Real) {
158        if let Some(m) = self.raw.rigid_mut() {
159            *m.restitution = restitution;
160        }
161    }
162
163    /// The user-defined data of the manifold, persistent across timesteps while the manifold
164    /// exists (`None` for a pair of two soft surfaces).
165    pub fn user_data(&self) -> Option<u32> {
166        self.raw.rigid().map(|m| *m.user_data)
167    }
168
169    /// Sets the user-defined data of the manifold (ignored for a pair of two soft surfaces).
170    pub fn set_user_data(&mut self, user_data: u32) {
171        if let Some(m) = self.raw.rigid_mut() {
172            *m.user_data = user_data;
173        }
174    }
175
176    /// The solver contacts of the manifold (`None` for a pair of two soft surfaces).
177    ///
178    /// While inside the hook, each solver contact's `anchor1`/`anchor2` hold the world-space
179    /// contact points on each body and `dist` their separation.
180    pub fn solver_contacts(&self) -> Option<&[SolverContact]> {
181        self.raw.rigid().map(|m| &m.solver_contacts[..])
182    }
183
184    /// Mutable access to the solver contacts of the manifold (`None` for a pair of two soft
185    /// surfaces).
186    ///
187    /// Contacts can be modified (e.g. their `tangent_velocity` to simulate conveyor belts),
188    /// removed, or all cleared to ignore this manifold in the constraints solver.
189    pub fn solver_contacts_mut(&mut self) -> Option<&mut SolverContacts> {
190        self.raw.rigid_mut().map(|m| &mut *m.solver_contacts)
191    }
192
193    /// Updates the contacts to emulate a one-way platform.
194    ///
195    /// Contacts are only allowed if the contact normal, expressed in the local space of the first
196    /// collider, is within `allowed_angle` (in radians) of `allowed_local_n1`. This must be called
197    /// at each timestep for each manifold involving the platform, and the manifold's user data
198    /// must not be modified elsewhere since it is used to track the contact state. Pairs of two
199    /// soft surfaces are left untouched.
200    pub fn update_as_oneway_platform(&mut self, allowed_local_n1: Vect, allowed_angle: Real) {
201        self.raw
202            .update_as_oneway_platform(allowed_local_n1, allowed_angle);
203    }
204}
205
206/// User-defined functions called by the physics engines during one timestep in order to customize its behavior.
207///
208/// Note that the default implementations of the filtering methods match Rapier's
209/// [`PhysicsHooks`] defaults: [`Self::filter_contact_pair`] returns
210/// `Some(SolverFlags::COMPUTE_RIGID_IMPULSES)` and [`Self::filter_intersection_pair`] returns
211/// `true`, so enabling a hook on a collider without overriding the corresponding method does not
212/// change which pairs interact.
213pub trait BevyPhysicsHooks: SystemParam + Send + Sync {
214    /// Applies the contact pair filter.
215    ///
216    /// Note that this method will only be called if at least one of the colliders
217    /// involved in the contact contains the `ActiveHooks::FILTER_CONTACT_PAIRS` flags
218    /// in its physics hooks flags.
219    ///
220    /// User-defined filter for potential contact pairs detected by the broad-phase.
221    /// This can be used to apply custom logic in order to decide whether two colliders
222    /// should have their contact computed by the narrow-phase, and if these contact
223    /// should be solved by the constraints solver
224    ///
225    /// This filter doesn't replace the built-in filtering: it is only called for the pairs that
226    /// passed it. Rapier first discards the pairs of colliders attached to the same rigid-body or
227    /// to multibody links (or impulse joints) with contacts disabled between them, then the pairs
228    /// rejected by the [`ActiveCollisionTypes`] of both colliders (e.g. two non-dynamic bodies by
229    /// default), then the pairs rejected by their [`CollisionGroups`]. The [`SolverGroups`] are
230    /// applied to the returned solver flags afterwards.
231    ///
232    /// [`ActiveCollisionTypes`]: crate::geometry::ActiveCollisionTypes
233    /// [`CollisionGroups`]: crate::geometry::CollisionGroups
234    /// [`SolverGroups`]: crate::geometry::SolverGroups
235    ///
236    /// If this returns `None`, then the narrow-phase will ignore this contact pair and
237    /// not compute any contact manifolds for it.
238    /// If this returns `Some`, then the narrow-phase will compute contact manifolds for
239    /// this pair of colliders, and configure them with the returned solver flags. For
240    /// example, if this returns `Some(SolverFlags::COMPUTE_RIGID_IMPULSES)` then the contacts
241    /// will be taken into account by the constraints solver. If this returns
242    /// `Some(SolverFlags::empty())` then the constraints solver will ignore these
243    /// contacts.
244    ///
245    /// The default implementation returns `Some(SolverFlags::COMPUTE_RIGID_IMPULSES)`.
246    fn filter_contact_pair(&self, _context: PairFilterContextView) -> Option<SolverFlags> {
247        Some(SolverFlags::COMPUTE_RIGID_IMPULSES)
248    }
249
250    /// Applies the intersection pair filter.
251    ///
252    /// Note that this method will only be called if at least one of the colliders
253    /// involved in the contact contains the `ActiveHooks::FILTER_INTERSECTION_PAIR` flags
254    /// in its physics hooks flags.
255    ///
256    /// User-defined filter for potential intersection pairs detected by the broad-phase.
257    ///
258    /// This can be used to apply custom logic in order to decide whether two colliders
259    /// should have their intersection computed by the narrow-phase.
260    ///
261    /// This filter doesn't replace the built-in filtering: it is only called for the pairs that
262    /// passed it. Rapier first discards the pairs rejected by the [`ActiveCollisionTypes`] of both
263    /// colliders (e.g. two non-dynamic bodies by default), then the pairs rejected by their
264    /// [`CollisionGroups`].
265    ///
266    /// [`ActiveCollisionTypes`]: crate::geometry::ActiveCollisionTypes
267    /// [`CollisionGroups`]: crate::geometry::CollisionGroups
268    ///
269    /// If this returns `false`, then the narrow-phase will ignore this pair and
270    /// not compute any intersection information for it.
271    /// If this return `true` then the narrow-phase will compute intersection
272    /// information for this pair.
273    ///
274    /// The default implementation returns `true`.
275    fn filter_intersection_pair(&self, _context: PairFilterContextView) -> bool {
276        true
277    }
278
279    /// Modifies the set of contacts seen by the constraints solver.
280    ///
281    /// Note that this method will only be called if at least one of the colliders
282    /// involved in the contact contains the `ActiveHooks::MODIFY_SOLVER_CONTACTS` flags
283    /// in its physics hooks flags.
284    ///
285    /// It is called on each contact manifold (with at least one solver contact) of such pairs,
286    /// after the solver contacts were computed from the manifold's contact points. This can be
287    /// used to modify the set of solver contacts seen by the constraints solver: contacts can be
288    /// removed and modified.
289    ///
290    /// Note that if all the contacts have to be ignored by the constraint solver, you may simply
291    /// clear them with `context.solver_contacts_mut()`.
292    ///
293    /// Modifying the solver contacts allow you to achieve various effects, including:
294    /// - Simulating conveyor belts by setting the `tangent_velocity` of a solver contact.
295    /// - Simulating shapes with multiply materials by modifying the friction and restitution
296    ///   coefficient depending of the features in contacts.
297    /// - Simulating one-way platforms depending on the contact normal.
298    ///
299    /// Each contact manifold is given a `u32` user-defined data that is persistent between
300    /// timesteps (as long as the contact manifold exists). This user-defined data is initialized
301    /// as 0 and can be modified with [`ContactModificationContextView::set_user_data`].
302    ///
303    /// The world-space contact normal, friction and restitution can be modified with
304    /// [`ContactModificationContextView::set_normal`],
305    /// [`ContactModificationContextView::set_friction`] and
306    /// [`ContactModificationContextView::set_restitution`].
307    fn modify_solver_contacts(&self, _context: ContactModificationContextView) {}
308}
309
310impl<T> BevyPhysicsHooks for T
311where
312    T: 'static + PhysicsHooks + SystemParam + Send + Sync,
313    for<'w, 's> T: SystemParam<Item<'w, 's> = T>,
314{
315    fn filter_contact_pair(&self, context: PairFilterContextView) -> Option<SolverFlags> {
316        PhysicsHooks::filter_contact_pair(self, context.raw)
317    }
318
319    fn filter_intersection_pair(&self, context: PairFilterContextView) -> bool {
320        PhysicsHooks::filter_intersection_pair(self, context.raw)
321    }
322
323    fn modify_solver_contacts(&self, context: ContactModificationContextView) {
324        PhysicsHooks::modify_solver_contacts(self, context.raw)
325    }
326}
327
328/// Adapts a type implementing `BevyPhysicsHooks` so that it implements `PhysicsHooks`.
329pub(crate) struct BevyPhysicsHooksAdapter<Hooks>
330where
331    Hooks: BevyPhysicsHooks,
332{
333    hooks: Hooks,
334}
335
336impl<Hooks> BevyPhysicsHooksAdapter<Hooks>
337where
338    Hooks: BevyPhysicsHooks,
339{
340    pub(crate) fn new(hooks: Hooks) -> Self {
341        Self { hooks }
342    }
343}
344
345impl<Hooks> PhysicsHooks for BevyPhysicsHooksAdapter<Hooks>
346where
347    Hooks: BevyPhysicsHooks,
348{
349    fn filter_contact_pair(&self, context: &PairFilterContext) -> Option<SolverFlags> {
350        let context_view = PairFilterContextView { raw: context };
351        self.hooks.filter_contact_pair(context_view)
352    }
353
354    fn filter_intersection_pair(&self, context: &PairFilterContext) -> bool {
355        let context_view = PairFilterContextView { raw: context };
356        self.hooks.filter_intersection_pair(context_view)
357    }
358
359    fn modify_solver_contacts(&self, context: &mut ContactModificationContext) {
360        let context_view = ContactModificationContextView { raw: context };
361        self.hooks.modify_solver_contacts(context_view)
362    }
363}
364
365#[cfg(test)]
366mod test {
367    use crate::math::{Real, Vect};
368    use crate::prelude::*;
369    use bevy::{
370        ecs::system::SystemParam,
371        prelude::*,
372        time::{TimePlugin, TimeUpdateStrategy},
373    };
374    use std::sync::{
375        atomic::{AtomicUsize, Ordering},
376        Arc,
377    };
378
379    const FRICTION: Real = 0.123;
380    const RESTITUTION: Real = 0.0;
381
382    #[derive(Resource, Default)]
383    struct HookCalls(Arc<AtomicUsize>);
384
385    #[derive(SystemParam)]
386    struct ModifyingHooks<'w> {
387        calls: Res<'w, HookCalls>,
388    }
389
390    impl BevyPhysicsHooks for ModifyingHooks<'_> {
391        fn modify_solver_contacts(&self, mut context: ContactModificationContextView) {
392            assert!(context.is_rigid());
393            assert!(!context.is_soft());
394            assert!(context.manifold().is_some());
395            assert!(context.solver_contacts().is_some());
396            let n = context.normal().unwrap();
397            context.set_normal(if n.y > 0.0 { Vect::Y } else { -Vect::Y });
398            context.set_friction(FRICTION);
399            context.set_restitution(RESTITUTION);
400            context.set_user_data(42);
401            self.calls.0.fetch_add(1, Ordering::SeqCst);
402        }
403    }
404
405    #[cfg(feature = "dim3")]
406    fn cuboid(hx: Real, hy: Real, hz: Real) -> Collider {
407        Collider::cuboid(hx, hy, hz)
408    }
409    #[cfg(feature = "dim2")]
410    fn cuboid(hx: Real, hy: Real, _hz: Real) -> Collider {
411        Collider::cuboid(hx, hy)
412    }
413
414    #[test]
415    fn hook_modifies_friction_and_normal() {
416        let mut app = App::new();
417        app.add_plugins((
418            TransformPlugin,
419            TimePlugin,
420            RapierPhysicsPlugin::<ModifyingHooks>::default(),
421        ))
422        .init_resource::<HookCalls>()
423        .insert_resource(TimeUpdateStrategy::ManualDuration(
424            std::time::Duration::from_secs_f32(1f32 / 60f32),
425        ));
426        let ground = app
427            .world_mut()
428            .spawn((Transform::from_xyz(0.0, -1.0, 0.0), cuboid(4.0, 1.0, 4.0)))
429            .id();
430        let body = app
431            .world_mut()
432            .spawn((
433                Transform::from_xyz(0.0, 0.6, 0.0),
434                RigidBody::Dynamic,
435                cuboid(0.5, 0.5, 0.5),
436                ActiveHooks::MODIFY_SOLVER_CONTACTS,
437            ))
438            .id();
439        app.finish();
440        for _ in 0..60 {
441            app.update();
442        }
443
444        assert!(app.world().resource::<HookCalls>().0.load(Ordering::SeqCst) > 0);
445
446        let mut query = app.world_mut().query::<(
447            &RapierContextSimulation,
448            &RapierContextColliders,
449            &RapierRigidBodySet,
450        )>();
451        let (simulation, colliders, bodies) = query.single(app.world()).unwrap();
452        let pair = simulation
453            .contact_pair(colliders, bodies, ground, body)
454            .unwrap();
455        assert!(pair.has_any_active_contact());
456        for manifold in pair.manifolds() {
457            assert_eq!(manifold.friction(), FRICTION);
458            assert_eq!(manifold.restitution(), RESTITUTION);
459            assert_eq!(manifold.user_data(), 42);
460            assert_eq!(manifold.normal().abs(), Vect::Y);
461        }
462    }
463}