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.
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 ColliderBuilder::contact_force_event_threshold
or
Collider::set_contact_force_event_threshold
(defaults to 0) for any of the two colliders with the ActiveEvents::CONTACT_FORCE_EVENTS
flag enabled.
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.
In order to handle these events, it is necessary to collect them using a structure implementing the EventHandler
trait.
Because Rapier can be parallelized, this event handler must also implement Send + Sync
.
One such structure provided by Rapier is the rapier::pipeline::ChannelEventCollector
. This event collector contains
channels from the crossbeam crate. These channels will be populated with events
during each call to PhysicsPipeline::step
:
// Initialize the event collector.
let (collision_send, collision_recv) = crossbeam::channel::unbounded();
let (contact_force_send, contact_force_recv) = crossbeam::channel::unbounded();
let event_handler = ChannelEventCollector::new(collision_send, contact_force_send);
physics_pipeline.step(
&gravity,
&integration_parameters,
&mut island_manager,
&mut broad_phase,
&mut narrow_phase,
&mut rigid_body_set,
&mut collider_set,
&mut impulse_joint_set,
&mut multibody_joint_set,
&mut ccd_solver,
Some(&mut query_pipeline),
&physics_hooks,
&event_handler,
);
while let Ok(collision_event) = collision_recv.try_recv() {
// Handle the collision event.
println!("Received collision event: {:?}", collision_event);
}
while let Ok(contact_force_event) = contact_force_recv.try_recv() {
// Handle the contact force event.
println!("Received contact force event: {:?}", contact_force_event);
}
Note however that if you need to access the contact information at the exact time a contact event happens, you may
provide your own EventHandler
implementation to access the contact pair given to EventHandler::handle_collision_event
.
You may just query the NarrowPhase
instead (after the timestep completed), 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 steps at a substeps right after).
Collision events identify the involved colliders by their handle. It is possible to retrieve the handle of
the rigid-body a collider is attached to: collider_set.get(collider_handle).unwrap().parent()
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:
- Each contact pair may contain multiple contact manifolds. Each contact manifold represents a set of contacts sharing the same contact normal.
- Each contact manifold contains the list of geometric contacts detected by the narrow-phase.
- 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 are expressed in world-space.
Because the solver contacts can be modified by the user and are expressed in world-space, 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 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:
- the
ContactPair::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
for each manifold inContactPair::manifolds
to determine if the colliders are really geometrically touching (independently from contact-modification).
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.
/* Find the contact pair, if it exists, between two colliders. */
if let Some(contact_pair) = narrow_phase.contact_pair(collider_handle1, collider_handle2) {
// 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.
}
// 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.data.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.data.impulse);
println!(
"Found friction impulse: {}",
contact_point.data.tangent_impulse
);
}
// Read the solver contacts.
for solver_contact in &manifold.data.solver_contacts {
// Keep in mind that all the solver contact data are expressed in world-space.
println!("Found solver contact point: {:?}", solver_contact.point);
// The solver contact distance is negative if there is a penetration.
println!("Found solver contact distance: {:?}", solver_contact.dist);
}
}
}
/* Iterate through all the contact pairs involving a specific collider. */
for contact_pair in narrow_phase.contact_pairs_with(collider_handle1) {
let other_collider = if contact_pair.collider1 == collider_handle1 {
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
.
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 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:
- At least one of the collider is a sensor.
- 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:
/* Find the intersection pair, if it exists, between two colliders. */
if narrow_phase.intersection_pair(collider_handle1, collider_handle2) == Some(true) {
println!(
"The colliders {:?} and {:?} are intersecting!",
collider_handle1, collider_handle2
);
}
/* Iterate through all the intersection pairs involving a specific collider. */
for (collider1, collider2, intersecting) in
narrow_phase.intersection_pairs_with(collider_handle1)
{
if intersecting {
println!(
"The colliders {:?} and {:?} are intersecting!",
collider1, collider2
);
}
}
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 an argument of the PhysicsPipeline::step
and CollisionPipeline::step
methods.
All physics hooks are grouped into the PhysicsHooks
trait that defines one method per kind of hook.
If no physics hooks are needed by your simulation, it is possible to use &()
as the physics hooks argument. ()
is
a physics hooks that does nothing particular.
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, thenPhysicsHooks::filter_contact_pair
will be called. If this filter returnsNone
then no contact computation will happen for this pair of colliders. If it returnsSome
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, thenPhysicsHooks::filter_intersection_pair
will be called. If this filter returnsfalse
then no intersection computation will happen for this pair of colliders. If it returnstrue
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 theSolverFlags::COMPUTE_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.
Right now there is no solver flags other than SolverFlags::COMPUTE_IMPULSES
. Other flags may be added in the future.
struct MyPhysicsHooks;
impl PhysicsHooks for MyPhysicsHooks {
fn filter_contact_pair(&self, context: &PairFilterContext) -> Option<SolverFlags> {
// This is a silly example of contact pair filter that:
// - Enables contact and force computation if both colliders have even user-data.
// - Enables contact computation but not force computation if both colliders have equal user-data.
// - Disables contact computation otherwise.
let user_data1 = context.colliders[context.collider1].user_data;
let user_data2 = context.colliders[context.collider2].user_data;
if user_data1 % 2 == 0 && user_data2 % 2 == 0 {
Some(SolverFlags::COMPUTE_IMPULSES)
} else if user_data1 == user_data2 {
Some(SolverFlags::empty())
} else {
None
}
}
fn filter_intersection_pair(&self, context: &PairFilterContext) -> bool {
// This is a silly example of intersection pair filter that
// enables the intersection test if both colliders have odd
// user-data.
let user_data1 = context.colliders[context.collider1].user_data;
let user_data2 = context.colliders[context.collider2].user_data;
user_data1 % 2 == 1 && user_data2 % 2 == 1
}
}
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 with non-uniform friction or non-uniform restitution coefficients, i.e., friction or restitution coefficients that depend on 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.
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 openning 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, including the contact normal, contact friction/restitution
coefficients, contact penetration depth, and warmstart impulses. 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).
struct MyPhysicsHooks;
impl PhysicsHooks for MyPhysicsHooks {
fn modify_solver_contacts(&self, context: &mut ContactModificationContext) {
// 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 coefficients to 0.3
// - Set the restitution coefficients to 0.4
// - Set the tangent velocities to X * 10.0
*context.normal = -*context.normal;
if !context.solver_contacts.is_empty() {
context.solver_contacts.swap_remove(0);
}
for solver_contact in &mut *context.solver_contacts {
solver_contact.friction = 0.3;
solver_contact.restitution = 0.4;
solver_contact.tangent_velocity.x = 10.0;
}
// Use the persistent user-data to count the number of times
// contact modification was called for this contact manifold
// since its creation.
*context.user_data += 1;
println!(
"Contact manifold has been modified {} times since its creation.",
*context.user_data
);
}
}
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.