Skip to main content

Advanced collision-detection

Collision-detection is a two-steps process. First the BroadPhase detects pairs of colliders that are potentially in contact or intersecting. Second, the NarrowPhase processes all these pairs in order to compute contacts points and generate collision events. Based on these points, the constraints solver computes forces that may generate contact force events.

All the pairs detected by the broad-phase are stored into two graph structures:

  • The contact graph stores all the potential contact pairs (between two non-sensor colliders) as well as the contact points generated by the narrow-phase.
  • The intersection graph stores all the potential intersection pairs (between a sensor collider and another collider) as well as the result of the boolean intersection test executed by the narrow-phase.

These two graphs are part of the NarrowPhase structure and are automatically updated by the PhysicsPipeline or the CollisionPipeline. Each node of these graph contains a ColliderHandle and there is one graph edge per pair detected by the broad-phase. The NarrowPhase is part of the RapierContextSimulation component, and these graphs are generally read through the methods of RapierContext that identify the colliders by their entity.

Collision and contact force events​

The narrow-phase can generate collision events between two colliders. Each collision event is given optional flags:

  • CollisionEventFlags::SENSOR is set if at least one of the colliders involved in the collision is a sensor.
  • CollisionEventFlags::REMOVED is set if a collision stopped because at least one of the colliders involved in the collision was removed from the physics scene.

In addition, after forces are computed by the constraints solver, contact force events may be generated between two colliders subject to non-zero contact forces. Generally, the user isn’t interested in contact force events unless the force magnitudes exceed some threshold. In order to skip low-force events, the engine will compute the sum of the magnitude of all the contacts between the two colliders and only trigger a contact force event if that magnitude is larger than the threshold set with the ContactForceEventThreshold component (defaults to 0) for any of the two colliders with the ActiveEvents::CONTACT_FORCE_EVENTS flag enabled.

warning

Collision events (resp. contact force events) are only generated between two colliders if at least one of them has the ActiveEvents::COLLISION_EVENTS flag (resp. ActiveEvents::CONTACT_FORCE_EVENTS flags) in its active events.

These events are sent as the CollisionEvent and ContactForceEvent Bevy messages, identifying the colliders involved by their entity. They can be read with a MessageReader. In addition to the force-related fields, ContactForceEvent::started indicates whether this is the first step during which the contact force exceeds the threshold (it is false during the next steps as long as the force remains above that threshold). The tear events of the soft-bodies are sent as SoftBodyTearEvent messages:

/* A system that displays the events. */
fn display_events(
mut collision_events: MessageReader<CollisionEvent>,
mut contact_force_events: MessageReader<ContactForceEvent>,
) {
for collision_event in collision_events.read() {
println!("Received collision event: {:?}", collision_event);
}

for contact_force_event in contact_force_events.read() {
// `started` is `true` only during the first step the contact force exceeds
// the threshold, and `false` during the next steps where it remains above it.
if contact_force_event.started {
println!("Received contact force event: {:?}", contact_force_event);
}
}
}

These messages only identify the colliders involved. The contact geometry can be read from the contact graph afterwards, but there are some cases when the contact information is no longer available at the end of the timestep (e.g. when running multi-step CCD and the contact start during one substep and stops at a substep right after). If you need to access the contact information at the exact time a contact event happens, you may install your own implementation of Rapier's EventHandler trait with RapierContextSimulation::set_event_handler. That handler is called in addition to (not instead of) the sending of the Bevy messages. Because Rapier can be parallelized, this event handler must also implement Send + Sync:

use bevy_rapier2d::rapier::dynamics::{RigidBodySet, SoftBodySet, SoftBodyTearEvent};
use bevy_rapier2d::rapier::geometry::{
ColliderSet, CollisionEvent as RapierCollisionEvent, ContactPair,
};
use bevy_rapier2d::rapier::pipeline::EventHandler;

/* An event handler counting the contact points at the time the collisions start. */
#[derive(Clone, Default)]
struct ContactCounter {
num_contacts: Arc<AtomicUsize>,
}

impl EventHandler for ContactCounter {
fn handle_collision_event(
&self,
_bodies: &RigidBodySet,
colliders: &ColliderSet,
event: RapierCollisionEvent,
contact_pair: Option<&ContactPair>,
) {
if event.started() {
// The colliders are identified by their Rapier handles. Their entity can be
// retrieved with `RapierContextColliders::entity_from_collider`.
let entity1 =
RapierContextColliders::entity_from_collider(&colliders[event.collider1()]);
// The contact pair is `None` if one of the colliders is a sensor.
if let Some(contact_pair) = contact_pair {
let num_points: usize = contact_pair
.manifolds()
.iter()
.map(|m| m.points.len())
.sum();
self.num_contacts.fetch_add(num_points, Ordering::Relaxed);
println!(
"Entity {:?} started touching with {} contact points.",
entity1, num_points
);
}
}
}

fn handle_contact_force_event(
&self,
_dt: f32,
_bodies: &RigidBodySet,
_colliders: &ColliderSet,
_contact_pair: &ContactPair,
_total_force_magnitude: f32,
) {
}

fn handle_soft_body_tear_event(&self, _soft_bodies: &SoftBodySet, _event: &SoftBodyTearEvent) {}
}

fn setup_event_handler(mut rapier_context: WriteRapierContext) {
let mut rapier_context = rapier_context.single_mut().unwrap();
// The handler is called in addition to (not instead of) the Bevy messages.
rapier_context
.simulation
.set_event_handler(ContactCounter::default());
}

The contact graph​

The contact graph can be read in order to determine whether two specific non-sensor colliders are in contact, or to determine all the non-sensor colliders in contact with one particular non-sensor collider. Contact points and contact normals will also be provided when a contact exists.

The contact geometry (contact points, contact normal, penetration depth, etc.) can be read from the contact manifolds stored in a contact pair:

  1. Each contact pair may contain multiple contact manifolds. Each contact manifold represents a set of contacts sharing the same contact normal.
  2. Each contact manifold contains the list of geometric contacts detected by the narrow-phase.
  3. Each contact manifold also contains a list of contacts that were processed by the constraints solver for force calculation (aka. the solver contacts). These solver contacts are a subset of the contacts detected by the narrow-phase, expressed in a way that is more efficient for the constraints solver to process. These solver contacts can be modified or deleted by the user using contact modification.

All the geometric contact data are expressed in the local-space of the colliders. The solver contacts hold one anchor per body surface, expressed in the local-space of the body that surface belongs to (so they ride rigidly with it); SolverContactView::world_point1 and SolverContactView::world_point2 resolve them back to world-space through the bodies' current poses. Inside a contact-modification hook they are world-space instead, since the hook runs before they are localized.

info

Because the solver contacts can be modified by the user, they are transients by nature: they are recomputed at each frame from the geometric contacts. Because of their transient nature, the constraint solver will store the forces it computes inside of the geometric contacts TrackedContact::data::impulse field (readable with ContactView::impulse) instead of the solver contacts themselves.

Keep in mind that the contact graph contains one graph edge per pair detected by the broad-phase. So the fact that a contact pair can be found in the graph doesn't mean that the corresponding colliders are actually in contact (they may just be very close to one another, without touching). It is necessary to check either:

  • theContactPair::has_any_active_contact if you need to know if there exist at least one solver contact between the colliders.
  • the length of ContactManifold:points (ContactManifoldView::num_points) for each manifold in ContactPair::manifolds to determine if the colliders are really geometrically touching (independently from contact-modification).
info

There will always be only up to one contact manifold between two colliders with convex primitive shapes. If one collider has a shape composed of several pieces (trimesh, polyline, heightfield, or compound shape) then there will be multiple contact manifolds, one for each piece that may result in an actual contact.

fn display_contact_info(rapier_context: ReadRapierContext, custom_info: Res<CustomInfo>) {
let rapier_context = rapier_context.single().unwrap();
let entity1 = custom_info.entity1; // A first entity with a collider attached.
let entity2 = custom_info.entity2; // A second entity with a collider attached.

/* Find the contact pair, if it exists, between two colliders. */
if let Some(contact_pair) = rapier_context.contact_pair(entity1, entity2) {
// The contact pair exists meaning that the broad-phase identified a potential contact.
if contact_pair.has_any_active_contact() {
// The contact pair has active contacts, meaning that it
// contains contacts for which contact forces were computed.
}

// The sum of the contact impulses applied to the first collider during the last timestep.
println!("Total contact impulse: {}", contact_pair.total_impulse());

// We may also read the contact manifolds to access the contact geometry.
for manifold in contact_pair.manifolds() {
println!("Local-space contact normal: {}", manifold.local_n1());
println!("Local-space contact normal: {}", manifold.local_n2());
println!("World-space contact normal: {}", manifold.normal());

// Read the geometric contacts.
for contact_point in manifold.points() {
// Keep in mind that all the geometric contact data are expressed in the local-space of the colliders.
println!(
"Found local contact point 1: {:?}",
contact_point.local_p1()
);
println!("Found contact distance: {:?}", contact_point.dist()); // Negative if there is a penetration.
println!("Found contact impulse: {}", contact_point.impulse());
println!(
"Found friction impulse: {}",
contact_point.tangent_impulse()
);
}

// Read the solver contacts.
for solver_contact in manifold.solver_contacts() {
// The world-space contact points on each body's surface.
let (point1, point2) =
(solver_contact.world_point1(), solver_contact.world_point2());
println!("Found solver contact points: {point1:?}, {point2:?}");
// The solver contact distance is negative if there is a penetration.
println!("Found solver contact distance: {:?}", solver_contact.dist());
}
}
}
}
fn display_contact_info_all_from_1_entity(
rapier_context: ReadRapierContext,
custom_info: Res<CustomInfo>,
) {
let rapier_context = rapier_context.single().unwrap();
let entity = custom_info.entity2; // An entity with a collider attached.

/* Iterate through all the contact pairs involving a specific collider. */
for contact_pair in rapier_context.contact_pairs_with(entity) {
let other_collider = if contact_pair.collider1() == Some(entity) {
contact_pair.collider2()
} else {
contact_pair.collider1()
};

// Process the contact pair in a way similar to what we did in
// the previous example.
}
}

Finally, keep in mind that the contacts and contact manifolds field names frequently end with a digit 1 or 2. For example contact_pair.manifolds[0].local_n1 and contact_pair.manifolds[0].local_n2. Fields ending with the digit 1 relate to the collider identified by contact_pair.collider1. Fields ending with the digit 2 relate to the collider identified by contact_pair.collider2.

In other words local_n1 is the contact normal expressed in the local space of the collider collider_pair.collider1, it points towards the exterior of the shape of collider_pair.collider1. On the other hand, local_n2 is expressed in the local space of the collider collider_pair.collider2 and points towards the exterior of the shape of collider_pair.collider2.

warning

The contact pair returned by narrow_phase.contact_pair(handle1, handle2) does not necessarily have contact_pair.collider1 == handle1 && contact_pair.collider2 == handle2. It could be swapped: contact_pair.collider1 == handle2 && contact_pair.collider2 == handle1.

So keep that in mind when reading the contact information because it's contact_pair.collider1 and contact_pair.collider2 that determine to what collider the digits 1 and 2 relate in the contacts and contact manifolds fields. The same applies to RapierContext::contact_pair(entity1, entity2): compare contact_pair.collider1() with your entities to know which one is the first collider.

The intersection graph​

The intersection graph can be read in order to determine whether two specific colliders (assuming at least one of them is a sensor) are intersecting, or to determine all the colliders intersecting one particular collider (assuming at least one collider of each pair is a sensor). The intersection graph contains one graph edge for each pair of colliders such that:

  1. At least one of the collider is a sensor.
  2. And they are close enough so the broad-phase considers they have a chance to be intersecting.

Each such edge contains one boolean indicating if the colliders are actually intersecting or not:

fn display_intersection_info(rapier_context: ReadRapierContext, custom_info: Res<CustomInfo>) {
let rapier_context = rapier_context.single().unwrap();
let entity1 = custom_info.entity1; // A first entity with a collider attached.
let entity2 = custom_info.entity2; // A second entity with a collider attached.

/* Find the intersection pair, if it exists, between two colliders. */
if rapier_context.intersection_pair(entity1, entity2) == Some(true) {
println!(
"The entities {:?} and {:?} have intersecting colliders!",
entity1, entity2
);
}
}
fn display_intersection_info_all_from_1_entity(
rapier_context: ReadRapierContext,
custom_info: Res<CustomInfo>,
) {
let rapier_context = rapier_context.single().unwrap();
let entity = custom_info.entity2; // An entity with a collider attached.

/* Iterate through all the intersection pairs involving a specific collider. */
for (collider1, collider2, intersecting) in rapier_context.intersection_pairs_with(entity) {
if intersecting {
println!(
"The entities {:?} and {:?} have intersecting colliders!",
collider1, collider2
);
}
}
}
warning

Keep in mind that intersection tests are performed between two colliders only if at least one of the colliders is a sensor. If they are both non-sensor colliders then they will be involved in the contact graph instead of the intersection graph.

Physics hooks​

Physics hooks are user-defined callbacks used to change the behavior of the physics simulation. In particular, they can be used to filter contacts (in a more flexible way than collision groups and solver groups) and to modify contacts before they are processed by the constraints solver.

Physics hooks are given as a type argument to RapierPhysicsPlugin. The hooks type must implement BevyPhysicsHooks trait. The trait requires SystemParam trait to also be implemented which is useful for example to access components attached to the entities to guide contact filtering. For physics hooks to work, the following steps must be taken:

  • The RapierPhysicsPlugin must be parametrized by the custom physics hooks type
  • The custom physics hooks must implement the BevyPhysicsHooks trait
  • The custom physics hooks must implement SystemParam trait for example using derive macro

The hooks are given a PairFilterContextView (for the filtering hooks) or a ContactModificationContextView (for the contact modification hook). They give access to the entities of the colliders involved (collider1(), collider2()) and of their rigid-bodies (rigid_body1(), rigid_body2()), as well as to the raw Rapier context (the raw field). The methods of BevyPhysicsHooks that are not implemented keep the default behavior of Rapier: filter_contact_pair returns Some(SolverFlags::COMPUTE_RIGID_IMPULSES) and filter_intersection_pair returns true. So enabling a hook in the active hooks of a collider without implementing the corresponding method doesn't change which pairs interact.

note

If you don't need any physics hooks, NoUserData should be passed as a plugin type parameter: RapierPhysicsPlugin::<NoUserData>::default().

Contact and intersection filtering​

Sometimes, collision groups and solver groups are not flexible enough to achieve the desired behavior. In that case, the contact filtering hooks let you apply custom rules to filter contact pairs and intersection pairs:

  • For each potential contact pair (between two non-sensor colliders) detected by the broad-phase, if at least one of the colliders involved in the pair has the bit ActiveHooks::FILTER_CONTACT_PAIRS enabled in its active hooks, then PhysicsHooks::filter_contact_pair will be called. If this filter returns None then no contact computation will happen for this pair of colliders. If it returns Some then the narrow-phase will compute contact points.
  • For each potential intersection pair (between a sensor colliders and another collider) detected by the broad-phase, if at least one of the colliders involved in the pair has the bit ActiveHooks::FILTER_INTERSECTION_PAIR enabled in its active hooks, then PhysicsHooks::filter_intersection_pair will be called. If this filter returns false then no intersection computation will happen for this pair of colliders. If it returns true then the narrow-phase will test whether or not they are intersecting.

When PhysicsHooks::filter_contact_pair returns Some(flags) it needs to provide a set of solver flags for this contact pair. These solver flags indicate what happen with the contacts of this contact pair afterwards:

  • If the returned Some(flags) contains the SolverFlags::COMPUTE_RIGID_IMPULSES bit, then the constraints solver will compute forces for these contacts. If this bit is not included in the returned flags, then no contact force will be computed for this pair of colliders.
note

Right now there is no solver flags other than SolverFlags::COMPUTE_RIGID_IMPULSES. Other flags may be added in the future.

fn main() {
App::new()
.add_plugins(DefaultPlugins)
// Make sure the Rapier plugin is parametrized by our custom user-data type.
.add_plugins(RapierPhysicsPlugin::<SameUserDataFilter>::default())
.add_systems(Startup, setup_physics)
.run();
}

#[derive(Component, PartialEq, Eq, Clone, Copy)]
enum CustomFilterTag {
GroupA,
GroupB,
}

// A custom filter that allows contacts/intersections only between rigid-bodies
// with the same CustomFilterTag component value.
// Note that using collision groups would be a more efficient way of doing
// this, but we use custom filters instead for demonstration purpose.
#[derive(SystemParam)]
struct SameUserDataFilter<'w, 's> {
tags: Query<'w, 's, &'static CustomFilterTag>,
}
impl BevyPhysicsHooks for SameUserDataFilter<'_, '_> {
fn filter_contact_pair(&self, context: PairFilterContextView) -> Option<SolverFlags> {
if self.tags.get(context.collider1()).ok().copied()
== self.tags.get(context.collider2()).ok().copied()
{
Some(SolverFlags::COMPUTE_RIGID_IMPULSES)
} else {
None
}
}

fn filter_intersection_pair(&self, context: PairFilterContextView) -> bool {
self.tags.get(context.collider1()).ok().copied()
== self.tags.get(context.collider2()).ok().copied()
}
}

fn setup_physics(mut commands: Commands) {
// Add colliders with a `CustomFilterTag` component. Only colliders
// with the same `CustomFilterTag` variant will collider thanks to
// our custom physics hooks:
commands.spawn((
Collider::ball(0.5),
ActiveHooks::FILTER_CONTACT_PAIRS | ActiveHooks::FILTER_INTERSECTION_PAIR,
CustomFilterTag::GroupA,
));

// TODO: add other colliders in a similar way.
}

Keep in mind that these filters don't replace the built-in filtering of Rapier: they are only called for the pairs that passed it. The pairs of colliders attached to the same rigid-body, or to rigid-bodies linked by a joint with contacts disabled, are discarded first, then the pairs rejected by the active collision types of both colliders (e.g. between two non-dynamic rigid-bodies by default), then the pairs rejected by their collision groups. The solver groups are applied to the solver flags returned by filter_contact_pair afterwards.

Contact modification​

It is possible to modify contacts after they have been computed by the narrow-phase. Contact-modification can have multiple advanced usages, for example:

  • The simulation of conveyor belts by modifying the tangent_velocity of solver contacts.
  • The simulation of one-way-platforms by deleting some contacts depending on the contact normal.
  • The simulation of colliders whose friction or restitution depends on where they are touched, by setting the coefficients from the contact points' location.

The PhysicsHooks::modify_solver_contacts methods is called on each contact manifold between two colliders where at least one of them has the ActiveHooks::MODIFY_SOLVER_CONTACTS flag enabled in its active hooks.

warning

Contact modification can be used to remove some (or all) solver contacts from a contact manifold. However, it cannot be used to add new contacts manually. If this is something that could useful to you, please consider opening an issue to let us know about your use-case so we can see if this is worth adding.

Contact-modification lets you change most characteristics of a contact: the contact normal, the contact points and their penetration depth, and the tangent velocity. The friction and restitution coefficients are combined once per manifold, so they are set for the whole manifold (context.set_friction / context.set_restitution) rather than per contact. None of these modifications are persistent (they are overwritten during the next timestep). There is one exception though: you can modify a user_data associated to each ContactManifold. This user_data will persist throughout timesteps as long as the ContactManifold remains alive (i.e. as long as some contacts exist between the touching parts of the colliders shapes). This can be useful to apply modification rules that depend on previous states of the contact (like whether or not this contact manifold existed during previous timesteps).

The ContactModificationContextView given to BevyPhysicsHooks::modify_solver_contacts exposes the contact manifold being modified through getters and setters: normal/set_normal, friction/set_friction, restitution/set_restitution, user_data/set_user_data, and solver_contacts/solver_contacts_mut. The contacts between two soft surfaces are contact candidates rather than a contact manifold: in that case (see ContactModificationContextView::is_soft), these getters return None and these setters do nothing, and the candidates can be modified with ContactModificationContextView::soft_mut instead. Finally, ContactModificationContextView::update_as_oneway_platform implements the removal of the contacts required for one-way-platforms.

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(RapierPhysicsPlugin::<MyPhysicsHooks>::default())
.add_systems(Startup, setup_physics)
.run();
}

#[derive(SystemParam)]
struct MyPhysicsHooks;

impl BevyPhysicsHooks for MyPhysicsHooks {
fn modify_solver_contacts(&self, mut context: ContactModificationContextView) {
// This is a silly example of contact modifier that does silly things
// for illustration purpose:
// - Flip all the contact normals.
// - Delete the first contact.
// - Set the friction coefficient to 0.3
// - Set the restitution coefficient to 0.4
// - Set the tangent velocities to X * 10.0
// The contacts of two soft surfaces are candidates rather than a manifold:
// only the manifolds of rigid pairs are modified here.
let Some(normal) = context.normal() else {
return;
};
context.set_normal(-normal);

if let Some(solver_contacts) = context.solver_contacts_mut() {
if !solver_contacts.is_empty() {
solver_contacts.swap_remove(0);
}

for solver_contact in solver_contacts.iter_mut() {
solver_contact.tangent_velocity.x = 10.0;
}
}

// Friction and restitution are combined once per manifold, so they are set
// for the whole manifold rather than per solver contact.
context.set_friction(0.3);
context.set_restitution(0.4);

// Use the persistent user-data to count the number of times
// contact modification was called for this contact manifold
// since its creation.
let num_calls = context.user_data().unwrap_or(0) + 1;
context.set_user_data(num_calls);
println!(
"Contact manifold has been modified {} times since its creation.",
num_calls
);
}
}

fn setup_physics(mut commands: Commands) {
// Add colliders
commands.spawn((Collider::ball(0.5), ActiveHooks::MODIFY_SOLVER_CONTACTS));

// TODO: add other colliders in a similar way.
}

Continuous Collision Detection​

Continuous Collision Detection (CCD) is used to make sure that fast-moving objects don't miss any contacts (a problem usually called tunneling). See the rigid-body CCD section for details.