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}