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 NarrowPhase structure and are automatically updated by the PhysicsPipeline or the CollisionPipelineR3World and are automatically updated by r3Step or r3DetectCollisionsColliderHandleR3ColliderHandleNarrowPhase is part of
the RapierContextSimulation component, and these graphs are generally read through the methods of RapierContext
that identify the colliders by their entity.NarrowPhase of a physics world is given by its PhysicsWorld.narrow_phase property.
Collision and contact force events
The narrow-phase can generate collision events between two colliders. Each collision event is given optional flags:
CollisionEventFlags::SENSORCollisionEventFlags::SENSORR3_COLLISION_EVENT_SENSOR is set if at least one of the colliders involved in the collision is a sensor.CollisionEventFlags.SENSORCollisionEventFlags::REMOVEDCollisionEventFlags::REMOVEDR3_COLLISION_EVENT_REMOVED is set if a collision stopped because at least one of the colliders involved in the collision was removed from the physics scene.CollisionEventFlags.REMOVED
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_thresholdContactForceEventThreshold componentcontactForceEventThreshold field of R3ColliderDesc or r3Collider_SetContactForceEventThresholdColliderBuilder.contact_force_event_threshold or the Collider.contact_force_event_threshold propertyActiveEvents::CONTACT_FORCE_EVENTSActiveEvents::CONTACT_FORCE_EVENTSR3_CONTACT_FORCE_EVENTSActiveEvents.CONTACT_FORCE_EVENTS
Collision events (resp. contact force events) are only generated between two colliders if at least one of them has the
ActiveEvents::COLLISION_EVENTSActiveEvents::COLLISION_EVENTSR3_COLLISION_EVENTSActiveEvents.COLLISION_EVENTSActiveEvents::CONTACT_FORCE_EVENTSActiveEvents::CONTACT_FORCE_EVENTSR3_CONTACT_FORCE_EVENTSActiveEvents.CONTACT_FORCE_EVENTS
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
one channel per kind of event (the collision events, the contact force events, and the tear events of the
soft-bodies), taken from the std::sync::mpsc module. These channels will be populated with
events during each call to PhysicsWorld::step_with_events (or PhysicsPipeline::step). Note that a channel whose
receiver was dropped simply discards the events of its kind:
// Initialize the event collector.
let (collision_send, collision_recv) = std::sync::mpsc::channel();
let (contact_force_send, contact_force_recv) = std::sync::mpsc::channel();
let (soft_body_tear_send, soft_body_tear_recv) = std::sync::mpsc::channel();
let event_handler =
ChannelEventCollector::new(collision_send, contact_force_send, soft_body_tear_send);
world.step_with_events(&(), &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);
}
while let Ok(tear_event) = soft_body_tear_recv.try_recv() {
// Handle the soft-body tear event.
println!("Received soft-body tear event: {:?}", tear_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: world.colliders[collider_handle].parent()
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());
}
These events are collected by an R3EventCollector, created by r3NewEventCollector and freed by
r3FreeEventCollector. It is given to r3Step (or r3DetectCollisions), which adds to it the collision events, the
contact force events, and the tear events of the soft-bodies generated during that step. The
collision events and contact force events are copied into buffers given by the application with
r3EventCollector_CollisionEvents and r3EventCollector_ContactForceEvents: calling them with a NULL buffer and a
zero capacity gives the number of events, then a second call with a buffer large enough copies them. The tear events
are read one by one with r3EventCollector_TearEvent, which returns a copy that must be freed with
r3FreeSoftBodyTearEvent. Note that reading the events doesn't remove them from the collector: they accumulate from
one step to the next until r3EventCollector_Clear is called:
// Initialize the event collector.
R2EventCollector *events = r2NewEventCollector();
r2Step(world, NULL, events);
// Get the number of collision events, then copy them.
size_t count = r2EventCollector_CollisionEvents(events, NULL, 0);
R2CollisionEvent *collision_events = malloc(count * sizeof(*collision_events));
count = r2EventCollector_CollisionEvents(events, collision_events, count);
for (size_t i = 0; i < count; i++) {
// Handle the collision event.
R2CollisionEvent event = collision_events[i];
printf("Received collision event: colliders %u and %u, started: %u, flags: %u\n",
event.collider1.index, event.collider2.index, event.started, event.flags);
}
free(collision_events);
count = r2EventCollector_ContactForceEvents(events, NULL, 0);
R2ContactForceEvent *contact_force_events = malloc(count * sizeof(*contact_force_events));
count = r2EventCollector_ContactForceEvents(events, contact_force_events, count);
for (size_t i = 0; i < count; i++) {
// Handle the contact force event.
R2ContactForceEvent event = contact_force_events[i];
printf("Received contact force event: colliders %u and %u, force magnitude: %f\n",
event.collider1.index, event.collider2.index, (double)event.total_force_magnitude);
}
free(contact_force_events);
count = r2EventCollector_TearEventCount(events);
for (size_t i = 0; i < count; i++) {
// Handle the soft-body tear event. It is a copy that must be freed.
R2SoftBodyTearEvent *tear_event = r2EventCollector_TearEvent(events, i);
printf("Received soft-body tear event: soft-body %u\n",
r2SoftBodyTearEvent_SoftBody(tear_event).index);
r2FreeSoftBodyTearEvent(tear_event);
}
// The events accumulate until the collector is cleared.
r2EventCollector_Clear(events);
Each R3CollisionEvent indicates whether the collision started or stopped, and its flags combine the
R3_COLLISION_EVENT_SENSOR and R3_COLLISION_EVENT_REMOVED bits described above. In addition to the force-related
fields, the started field of an R3ContactForceEvent indicates whether this is the first step during which the
contact force exceeds the threshold (it is 0 during the next steps as long as the force remains above that
threshold).
These events 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 give an R3EventCallbacks to the event collector with
r3EventCollector_SetCallbacks (taking effect from the next step). Its callbacks are called during the step with
each event, right after it was added to the collector: they complement (and don't replace) the collection of the
events. The collision event callback is also given the geometric contacts of the pair at that time (in the collider
order of the event, and none for a sensor), which are only valid during that call. Just like the physics hooks, they only have a read-only
access to the world, and must be thread-safe if the library was built with parallelism enabled.
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: r3Collider_Parent(collider_handle).
In order to handle these events, it is necessary to collect them with an event handler assigned to the
PhysicsWorld.event_handler property. One such event handler provided by Rapier is the ChannelEventCollector. It
contains one queue per kind of event (the collision events, the contact force events, and the tear events of the
soft-bodies), which are populated with events during each call to PhysicsWorld.step. Its
drain_collision_events, drain_contact_force_events, and drain_soft_body_tear_events methods give the list of the
events of their kind and empty the corresponding queue. Note that the events accumulate from one step to the next
until they are drained (or until ChannelEventCollector.clear is called):
# Initialize the event collector.
event_handler = rp.ChannelEventCollector()
world.event_handler = event_handler
world.step()
for collision_event in event_handler.drain_collision_events():
# Handle the collision event.
print("Received collision event:", collision_event)
for contact_force_event in event_handler.drain_contact_force_events():
# Handle the contact force event.
print("Received contact force event:", contact_force_event)
for tear_event in event_handler.drain_soft_body_tear_events():
# Handle the soft-body tear event.
print("Received soft-body tear event:", tear_event)
Each CollisionEvent indicates whether the collision started or stopped, and its flags combine the
CollisionEventFlags.SENSOR and CollisionEventFlags.REMOVED bits described above (also given by its sensor and
removed properties). Each ContactForceEvent gives the sum of the contact forces applied between the two colliders
(total_force) and its magnitude (total_force_magnitude), as well as the magnitude and the direction of the largest
contact force (max_force_magnitude and max_force_direction).
These events 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 assign your own event handler instead: any object with the methods
of the EventHandler protocol (the kinds of events whose method isn't implemented are ignored). These methods are called
during the step, with a copy of the contact pair involved.
They are also given the rigid-body and collider sets of the world (bodies and colliders), which can be read but not
modified (a modification raises a RuntimeError). Since these methods may be called from the worker threads of the
step, read the world through these arguments rather than through the PhysicsWorld itself. Finally, an exception
raised by one of these methods is raised again by PhysicsWorld.step once the step is complete (see
PhysicsWorld.event_error_policy):
class MyEventHandler:
def handle_collision_event(self, bodies, colliders, event, contact_pair):
# The contact pair is a copy of the contacts at the time of the event
# (it is `None` if one of the colliders is a sensor).
if event.started and contact_pair is not None:
deepest_contact = contact_pair.find_deepest_contact()
print("Collision started with the contact:", deepest_contact)
# The sets of the world can be read (but not modified) from the event handler.
parent1 = colliders[event.collider1].parent
print("The first collider is attached to the rigid-body:", parent1)
def handle_contact_force_event(self, dt, bodies, colliders, contact_pair, total_force_magnitude):
print(
f"Contact force {total_force_magnitude} between",
contact_pair.collider1,
"and",
contact_pair.collider2,
)
def handle_soft_body_tear_event(self, soft_bodies, event):
# This method is optional.
print("Soft-body torn:", event)
world.event_handler = MyEventHandler()
world.step()
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: world.colliders[collider_handle].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 hold one
anchor per body surface, expressed in the local-space of the body that surface belongs to (so they ride rigidly with
it); ContactManifoldData::solver_contact_world_points resolvesSolverContactView::world_point1
and SolverContactView::world_point2 resolver3SolverContacts resolvesContactManifoldData.solver_contact_world_points resolves
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 fieldTrackedContact::data::impulse fieldimpulse field of R3ContactPoint)impulse property of ContactData)ContactView::impulse)
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_contactContactPair::has_any_active_contacthas_any_active_contactfield of theR3ContactPair if you need to know if there exist at least one solver contact between the colliders.ContactPair.has_any_active_contactproperty - the length of
ContactManifold:pointsContactManifold:pointsthe geometric contacts ( num_points)ContactManifold.points( for each manifold inContactManifoldView::num_points)ContactPair::manifoldsContactPair::manifoldsthe result of r3ContactManifolds to determine if the colliders are really geometrically touching (independently from contact-modification).ContactPair.manifolds
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) = world.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 {
// Solver contacts are anchored in the local-space of the body they touch, so
// they ride rigidly with it. Resolve them through the bodies' current poses to
// get the world-space contact point on each body's surface.
let (point1, point2) = manifold
.data
.solver_contact_world_points(solver_contact, &world.bodies);
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);
}
}
}
/* Iterate through all the contact pairs involving a specific collider. */
for contact_pair in world.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.
}
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.
}
}
The contact pair between two colliders is given by r3TryContactPair, which sets the found field of its result to
0 if the pair doesn't exist (whereas r3ContactPair reports it as the R3_NOT_FOUND error). The contact pairs
involving one particular collider are given by r3Collider_ContactPairs, and all the contact pairs of the world by
r3ContactPairs. In addition to has_any_active_contact, an R3ContactPair gives a summary of the contact impulses
applied between the two colliders during the last step (total_impulse, max_impulse, etc.) The contact manifolds of
a pair are given by r3ContactManifolds, their geometric contacts by r3ContactPoints (each tagged with the
manifold_index of its manifold), and the solver contacts of one manifold by r3SolverContacts:
/* Find the contact pair, if it exists, between two colliders. */
R2OptionalContactPair contact_pair = r2TryContactPair(collider_handle1, collider_handle2);
if (contact_pair.found) {
// The contact pair exists meaning that the broad-phase identified a potential contact.
if (contact_pair.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.
size_t num_manifolds = r2ContactManifolds(collider_handle1, collider_handle2, NULL, 0);
R2ContactManifold *manifolds = malloc(num_manifolds * sizeof(*manifolds));
num_manifolds = r2ContactManifolds(collider_handle1, collider_handle2, manifolds, num_manifolds);
// The geometric contacts of all the manifolds, each with the index of its manifold.
size_t num_points = r2ContactPoints(collider_handle1, collider_handle2, NULL, 0);
R2ContactPoint *points = malloc(num_points * sizeof(*points));
num_points = r2ContactPoints(collider_handle1, collider_handle2, points, num_points);
for (size_t i = 0; i < num_manifolds; i++) {
R2ContactManifold manifold = manifolds[i];
printf("Local-space contact normal: (%f, %f)\n", (double)manifold.local_n1.x, (double)manifold.local_n1.y);
printf("Local-space contact normal: (%f, %f)\n", (double)manifold.local_n2.x, (double)manifold.local_n2.y);
printf("World-space contact normal: (%f, %f)\n", (double)manifold.normal.x, (double)manifold.normal.y);
// Read the geometric contacts.
for (size_t j = 0; j < num_points; j++) {
if (points[j].manifold_index != i) {
continue;
}
// Keep in mind that all the geometric contact data are expressed in the local-space of the colliders.
R2ContactPoint contact_point = points[j];
printf("Found local contact point 1: (%f, %f)\n", (double)contact_point.local_p1.x,
(double)contact_point.local_p1.y);
printf("Found contact distance: %f\n", (double)contact_point.distance); // Negative if there is a penetration.
printf("Found contact impulse: %f\n", (double)contact_point.impulse);
printf("Found friction impulse: %f\n", (double)contact_point.tangent_impulse[0]);
}
// Read the solver contacts.
size_t num_solver_contacts = r2SolverContacts(collider_handle1, collider_handle2, i, NULL, 0);
R2SolverContact *solver_contacts = malloc(num_solver_contacts * sizeof(*solver_contacts));
num_solver_contacts =
r2SolverContacts(collider_handle1, collider_handle2, i, solver_contacts, num_solver_contacts);
for (size_t j = 0; j < num_solver_contacts; j++) {
// Solver contacts are anchored in the local-space of the body they touch, so
// they ride rigidly with it. `r2SolverContacts` resolves them through the bodies'
// current poses to give the world-space contact point on each body's surface.
R2SolverContact solver_contact = solver_contacts[j];
printf("Found solver contact points: (%f, %f), (%f, %f)\n", (double)solver_contact.point1.x,
(double)solver_contact.point1.y, (double)solver_contact.point2.x,
(double)solver_contact.point2.y);
// The solver contact distance is negative if there is a penetration.
printf("Found solver contact distance: %f\n", (double)solver_contact.distance);
}
free(solver_contacts);
}
free(points);
free(manifolds);
}
/* Iterate through all the contact pairs involving a specific collider. */
size_t num_pairs = r2Collider_ContactPairs(collider_handle1, NULL, 0);
R2ContactPair *pairs = malloc(num_pairs * sizeof(*pairs));
num_pairs = r2Collider_ContactPairs(collider_handle1, pairs, num_pairs);
for (size_t i = 0; i < num_pairs; i++) {
R2ColliderHandle other_collider =
same_collider(pairs[i].collider1, collider_handle1) ? pairs[i].collider2 : pairs[i].collider1;
// Process the contact pair in a way similar to what we did in
// the previous example.
(void)other_collider;
}
free(pairs);
The contact pair between two colliders is given by NarrowPhase.contact_pair, which returns None if the pair doesn't
exist. The contact pairs involving one particular collider are given by NarrowPhase.contact_pairs_with, and all the
contact pairs of the world by NarrowPhase.contact_pairs. Keep in mind that these methods give copies of the contact
pairs: they are not updated by the next timesteps. In addition to has_any_active_contact, a ContactPair gives a
summary of the contact impulses applied between the two colliders during the last step (total_impulse and
total_impulse_magnitude), and its deepest geometric contact (find_deepest_contact). Each element of
ContactPair.manifolds is a ContactManifold whose points are its geometric contacts (each one being a
ContactData), and whose data (a ContactManifoldData) contains its world-space contact normal as well as its
solver contacts:
# Find the contact pair, if it exists, between two colliders.
contact_pair = world.narrow_phase.contact_pair(collider_handle1, collider_handle2)
if contact_pair is not None:
# 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.
pass
# We may also read the contact manifolds to access the contact geometry.
for manifold in contact_pair.manifolds:
print("Local-space contact normal:", manifold.local_n1)
print("Local-space contact normal:", manifold.local_n2)
print("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.
print("Found local contact point 1:", contact_point.local_p1)
print("Found contact distance:", contact_point.dist) # Negative if there is a penetration.
print("Found contact impulse:", contact_point.impulse)
print("Found friction impulse:", contact_point.tangent_impulse)
# Read the solver contacts.
for solver_contact in manifold.data.solver_contacts:
# Solver contacts are anchored in the local-space of the body they touch, so
# they ride rigidly with it. Resolve them through the bodies' current poses to
# get the world-space contact point on each body's surface.
point1, point2 = manifold.data.solver_contact_world_points(solver_contact, world.rigid_bodies)
print("Found solver contact points:", point1, point2)
# The solver contact distance is negative if there is a penetration.
print("Found solver contact distance:", solver_contact.dist)
# Iterate through all the contact pairs involving a specific collider.
for contact_pair in world.narrow_phase.contact_pairs_with(collider_handle1):
if contact_pair.collider1 == collider_handle1:
other_collider = contact_pair.collider2
else:
other_collider = 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_n2local_n1 and local_n2 fields of an R3ContactManifold1 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)r3TryContactPair(handle1, handle2)contact_pair.collider1 == handle1 && contact_pair.collider2 == handle2contact_pair.collider1 == handle1 and contact_pair.collider2 == handle2contact_pair.collider1 == handle2 && contact_pair.collider2 == handle1contact_pair.collider1 == handle2 and 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.RapierContext::contact_pair(entity1, entity2): compare contact_pair.collider1() with your entities to
know which one is the first collider.r3ContactManifolds(handle1, handle2), r3ContactPoints(handle1, handle2), and r3SolverContacts(handle1, handle2): compare contact_pair.collider1 with your handles (i.e. their index and generation fields) 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:
- 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 world.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
world.narrow_phase.intersection_pairs_with(collider_handle1)
{
if intersecting {
println!(
"The colliders {:?} and {:?} are intersecting!",
collider1, collider2
);
}
}
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
);
}
}
}
The intersection pair between two colliders is given by r3TryIntersectionPair (r3IntersectionPair reports the
R3_NOT_FOUND error if that pair doesn't exist), the intersection pairs involving one particular collider by
r3Collider_IntersectionPairs, and all the intersection pairs of the world by r3IntersectionPairs. The boolean of
each edge is the intersecting field of the R3IntersectionPair. Unlike the contact pairs, the pair given by
r3TryIntersectionPair and r3IntersectionPair keeps the order of their arguments:
/* Find the intersection pair, if it exists, between two colliders. */
R2OptionalIntersectionPair intersection_pair = r2TryIntersectionPair(collider_handle1, collider_handle2);
if (intersection_pair.found && intersection_pair.pair.intersecting) {
printf("The colliders %u and %u are intersecting!\n", collider_handle1.index, collider_handle2.index);
}
/* Iterate through all the intersection pairs involving a specific collider. */
size_t num_intersections = r2Collider_IntersectionPairs(collider_handle1, NULL, 0);
R2IntersectionPair *intersections = malloc(num_intersections * sizeof(*intersections));
num_intersections = r2Collider_IntersectionPairs(collider_handle1, intersections, num_intersections);
for (size_t i = 0; i < num_intersections; i++) {
if (intersections[i].intersecting) {
printf("The colliders %u and %u are intersecting!\n", intersections[i].collider1.index,
intersections[i].collider2.index);
}
}
free(intersections);
The boolean of the edge between two colliders is given by NarrowPhase.intersection_pair, which returns None if that
pair doesn't exist. The intersection pairs involving one particular collider are given by
NarrowPhase.intersection_pairs_with, and all the intersection pairs of the world by NarrowPhase.intersection_pairs,
each one as a (collider1, collider2, intersecting) tuple:
# Find the intersection pair, if it exists, between two colliders.
if world.narrow_phase.intersection_pair(collider_handle1, collider_handle2):
print(f"The colliders {collider_handle1} and {collider_handle2} are intersecting!")
# Iterate through all the intersection pairs involving a specific collider.
for collider1, collider2, intersecting in world.narrow_phase.intersection_pairs_with(collider_handle1):
if intersecting:
print(f"The colliders {collider1} and {collider2} are intersecting!")
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 PhysicsWorld::step_with_events, 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.
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
RapierPhysicsPluginmust be parametrized by the custom physics hooks type - The custom physics hooks must implement the
BevyPhysicsHookstrait - The custom physics hooks must implement
SystemParamtrait for example usingderivemacro
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.
If you don't need any physics hooks, NoUserData should be passed as a plugin type parameter:
RapierPhysicsPlugin::<NoUserData>::default().
Physics hooks are given as an argument of r3Step and r3DetectCollisions. They are grouped into the
R3PhysicsHooks structure, which contains one callback per kind of hook, as well as a user_data pointer given as
the first argument of each of these callbacks. A NULL callback keeps the default behavior of Rapier, so a
zero-initialized R3PhysicsHooks doesn't change anything.
The callbacks are called during the step, possibly from several threads at once if the library was built with
parallelism enabled (in which case the callbacks and their user_data must be thread-safe). They are given an
R3ReadContext which gives a read-only access to the rigid-bodies and colliders (through the r3ReadRigidBody_* and
r3ReadCollider_* functions): any other access to the world being stepped reports the R3_WORLD_BUSY error, so any
modification of the world must be performed after the step. Finally, a callback must not keep its arguments after it
returns.
If no physics hooks are needed by your simulation, it is possible to use NULL as the physics hooks argument of
r3Step.
Physics hooks are given to the physics world by assigning them to its PhysicsWorld.physics_hooks property. They can
be any object with the methods of the PhysicsHooks protocol, which defines one method per kind of hook. Only the
methods of the hooks enabled in the active hooks of the colliders are called, and a
method that isn't implemented keeps the default behavior of Rapier, so the hooks you don't need can be omitted. These
methods are called during the step, and are given a context (a PairFilterContext or a ContactModificationContext)
which identifies the colliders involved (collider1 and collider2) and their rigid-bodies (rigid_body1 and
rigid_body2, which are None for a collider without parent), and gives a read-only access to the colliders and
rigid-bodies of the world (colliders and bodies, a modification raising a RuntimeError). This context is only
valid until the method returns. Since the hooks may be called from the worker threads of the step, read the world
through this context rather than through the PhysicsWorld itself. Finally, an exception raised by one of these methods is raised again by PhysicsWorld.step once the step is complete
(see PhysicsWorld.event_error_policy).
If no physics hooks are needed by your simulation, the PhysicsWorld.physics_hooks property can be left to None,
its default value.
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_PAIRSenabled in its active hooks, thenPhysicsHooks::filter_contact_pairwill be called. If this filter returnsNonethen no contact computation will happen for this pair of colliders. If it returnsSomethen 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_PAIRenabled in its active hooks, thenPhysicsHooks::filter_intersection_pairwill be called. If this filter returnsfalsethen no intersection computation will happen for this pair of colliders. If it returnstruethen 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_RIGID_IMPULSESbit, 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_RIGID_IMPULSES. Other flags may be added in the future.
- 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
R3_FILTER_CONTACT_PAIRSenabled in its active hooks, then thefilter_contact_paircallback will be called. If it returns-1then no contact computation will happen for this pair of colliders. Otherwise 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
R3_FILTER_INTERSECTION_PAIRenabled in its active hooks, then thefilter_intersection_paircallback will be called. If it returns0then no intersection computation will happen for this pair of colliders. If it returns a positive value then the narrow-phase will test whether or not they are intersecting.
When filter_contact_pair doesn't return -1, its return value also indicates what happen with the contacts of this
contact pair afterwards:
- If it returns
1, then the constraints solver will compute forces for these contacts. - If it returns
0, then the contact points are computed, but no contact force will be computed for this pair of colliders.
- 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_PAIRSenabled in its active hooks, thenPhysicsHooks.filter_contact_pairwill be called. If this filter returnsNonethen no contact computation will happen for this pair of colliders. If it returns solver flags (aSolverFlags) 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_PAIRenabled in its active hooks, thenPhysicsHooks.filter_intersection_pairwill be called. If this filter returnsFalsethen no intersection computation will happen for this pair of colliders. If it returnsTruethen the narrow-phase will test whether or not they are intersecting.
When PhysicsHooks.filter_contact_pair doesn't return None, the solver flags it returns indicate what happen with
the contacts of this contact pair afterwards:
- If the returned flags contain the
SolverFlags.COMPUTE_RIGID_IMPULSESbit, then the constraints solver will compute forces for these contacts. If this bit is not included in the returned flags (e.g. withSolverFlags.empty()), then no contact force will be computed for this pair of colliders.
Right now there is no solver flags other than SolverFlags.COMPUTE_RIGID_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_RIGID_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
}
}
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.
// 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.
static int32_t RAPIER_CALL filter_contact_pair(void *user_data, const R2ReadContext *read,
R2ColliderHandle collider1, R2ColliderHandle collider2,
R2RigidBodyHandle body1, R2RigidBodyHandle body2) {
(void)user_data;
(void)body1;
(void)body2;
uint64_t user_data1 = r2ReadCollider_UserData(read, collider1).low;
uint64_t user_data2 = r2ReadCollider_UserData(read, collider2).low;
if (user_data1 % 2 == 0 && user_data2 % 2 == 0) {
return 1; // Compute the contacts and the contact forces.
} else if (user_data1 == user_data2) {
return 0; // Compute the contacts, but not the contact forces.
} else {
return -1; // Don't compute any contact.
}
}
// This is a silly example of intersection pair filter that
// enables the intersection test if both colliders have odd
// user-data.
static int32_t RAPIER_CALL filter_intersection_pair(void *user_data, const R2ReadContext *read,
R2ColliderHandle collider1, R2ColliderHandle collider2,
R2RigidBodyHandle body1, R2RigidBodyHandle body2) {
(void)user_data;
(void)body1;
(void)body2;
uint64_t user_data1 = r2ReadCollider_UserData(read, collider1).low;
uint64_t user_data2 = r2ReadCollider_UserData(read, collider2).low;
return user_data1 % 2 == 1 && user_data2 % 2 == 1;
}
static void step_with_pair_filters(R2World *world) {
// NULL callbacks keep the default behavior.
R2PhysicsHooks hooks = {0};
hooks.user_data = NULL; // Given to every callback as its first argument.
hooks.filter_contact_pair = filter_contact_pair;
hooks.filter_intersection_pair = filter_intersection_pair;
r2Step(world, &hooks, NULL);
}
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 result of
filter_contact_pair afterwards.
class MyPhysicsHooks:
def filter_contact_pair(self, context):
# 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.
user_data1 = context.colliders[context.collider1].user_data
user_data2 = context.colliders[context.collider2].user_data
if user_data1 % 2 == 0 and user_data2 % 2 == 0:
return rp.SolverFlags.COMPUTE_RIGID_IMPULSES
elif user_data1 == user_data2:
return rp.SolverFlags.empty()
else:
return None
def filter_intersection_pair(self, context):
# This is a silly example of intersection pair filter that
# enables the intersection test if both colliders have odd
# user-data.
user_data1 = context.colliders[context.collider1].user_data
user_data2 = context.colliders[context.collider2].user_data
return user_data1 % 2 == 1 and user_data2 % 2 == 1
world.physics_hooks = MyPhysicsHooks()
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_velocityof 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 isPhysicsHooks::modify_solver_contacts methods ismodify_solver_contacts and modify_solver_contacts_context callbacks arePhysicsHooks.modify_solver_contacts method isActiveHooks::MODIFY_SOLVER_CONTACTSActiveHooks::MODIFY_SOLVER_CONTACTSR3_MODIFY_SOLVER_CONTACTSActiveHooks.MODIFY_SOLVER_CONTACTS
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.friction / context.restitutioncontext.set_friction /
context.set_restitutionfriction / restitution fields of the R3ContactModificationcontext.friction / context.restitutionuser_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 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 ModifiableContacts::Rigid(manifold) = &mut context.contacts else {
return;
};
*manifold.normal = -*manifold.normal;
if !manifold.solver_contacts.is_empty() {
manifold.solver_contacts.swap_remove(0);
}
// Friction and restitution are combined once per manifold, so they are set
// for the whole manifold rather than per solver contact.
*manifold.friction = 0.3;
*manifold.restitution = 0.4;
for solver_contact in &mut *manifold.solver_contacts {
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.
*manifold.user_data += 1;
println!(
"Contact manifold has been modified {} times since its creation.",
*manifold.user_data
);
}
}
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.
}
The contact modification is split into two callbacks of the R3PhysicsHooks, both called for each contact manifold:
modify_solver_contactsis given anR3ContactModificationholding the properties of the whole manifold: its world-space contactnormal, itsfrictionandrestitutioncoefficients, and its persistentuser_data. Setting itsenabledfield to0removes all the solver contacts of the manifold.modify_solver_contacts_context, called right after, is given anR3ContactModificationContextfor modifying the solver contacts themselves.r3ContactModificationContext_SetTangentVelocitysets the tangent velocity of every solver contact of the manifold (e.g. for conveyor belts), andr3ContactModificationContext_UpdateAsOnewayPlatformimplements the removal of the contacts required for one-way-platforms (it relies on theuser_dataof the manifold).
The contacts between two soft surfaces are contact candidates rather than a contact manifold: in that case,
modify_solver_contacts isn't called and the functions of the R3ContactModificationContext above do nothing.
// This is a silly example of contact modifier that does silly things
// for illustration purpose:
// - Flip all the contact normals.
// - 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 given to this callback.
static void RAPIER_CALL modify_manifold(void *user_data, const R2ReadContext *read,
R2ColliderHandle collider1, R2ColliderHandle collider2,
R2ContactModification *manifold) {
(void)user_data;
(void)read;
(void)collider1;
(void)collider2;
manifold->normal = r2VectorScale(manifold->normal, -1.0);
// Friction and restitution are combined once per manifold, so they are set
// for the whole manifold rather than per solver contact.
manifold->friction = 0.3;
manifold->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.
manifold->user_data += 1;
printf("Contact manifold has been modified %u times since its creation.\n", manifold->user_data);
}
// Called right after `modify_manifold`, for the same manifold.
static void RAPIER_CALL modify_solver_contacts(void *user_data, const R2ReadContext *read,
R2ColliderHandle collider1, R2ColliderHandle collider2,
R2ContactModificationContext *context) {
(void)user_data;
(void)read;
(void)collider1;
(void)collider2;
r2ContactModificationContext_SetTangentVelocity(context, r2Vector(10.0, 0.0));
}
static void step_with_contact_modification(R2World *world) {
R2PhysicsHooks hooks = {0};
hooks.modify_solver_contacts = modify_manifold;
hooks.modify_solver_contacts_context = modify_solver_contacts;
r2Step(world, &hooks, NULL);
}
The solver contacts can also be modified one by one: r3ContactModificationContext_SolverContactCount gives their
number, r3ContactModificationContext_SolverContact and r3ContactModificationContext_SetSolverContact read and
write one of them, and r3ContactModificationContext_RemoveSolverContact removes one of them (the last solver contact
taking its place). Inside the hook, the contact points of an R3SolverContact (point1 and point2) are expressed in
world-space. Finally, r3ContactModificationContext_IsSoft indicates whether the context holds the contact candidates
of two soft surfaces rather than a contact manifold:
// Modifies the solver contacts one by one:
// - Delete the first contact.
// - Set the tangent velocities to X * 10.0
static void RAPIER_CALL modify_each_solver_contact(void *user_data, const R2ReadContext *read,
R2ColliderHandle collider1, R2ColliderHandle collider2,
R2ContactModificationContext *context) {
(void)user_data;
(void)read;
(void)collider1;
(void)collider2;
// The contacts of two soft surfaces are candidates rather than a manifold.
if (r2ContactModificationContext_IsSoft(context)) {
return;
}
// The last solver contact takes the place of the removed one.
if (r2ContactModificationContext_SolverContactCount(context) > 0) {
r2ContactModificationContext_RemoveSolverContact(context, 0);
}
size_t count = r2ContactModificationContext_SolverContactCount(context);
for (size_t i = 0; i < count; i++) {
R2SolverContact solver_contact = r2ContactModificationContext_SolverContact(context, i);
solver_contact.tangent_velocity.x = 10.0;
r2ContactModificationContext_SetSolverContact(context, i, &solver_contact);
}
}
The ContactModificationContext given to PhysicsHooks.modify_solver_contacts exposes the contact manifold being
modified through its properties: its world-space contact normal, its friction and restitution coefficients, and
its persistent user_data can be read and modified, whereas local_n1 and local_n2 give its contact normal in the
local-space of each collider. Its solver contacts are given by solver_contacts (a list of copies of the
SolverContact) and counted by num_solver_contacts. They can be modified one by one with set_solver_contact
(given the index of the solver contact, and the new values of its point, point2, dist, or tangent_velocity as
keyword arguments), removed one by one with remove_solver_contact, or all removed at once with
clear_solver_contacts. set_tangent_velocity sets the tangent velocity of every solver contact of the manifold at
once (e.g. for conveyor belts): it is the velocity of the surface of the second collider relative to the surface
of the first one, so a belt dragging objects along a direction v sets v if it is collider1, and -v if it is
collider2. Inside the hook, the contact points of a SolverContact (point and point2) are expressed
in world-space. Note that this hook isn't called for the contacts between two soft surfaces, which are contact
candidates rather than a contact manifold:
class MyPhysicsHooks:
def modify_solver_contacts(self, context):
# 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
context.normal = -context.normal
if context.num_solver_contacts() > 0:
context.remove_solver_contact(0)
# Friction and restitution are combined once per manifold, so they are set
# for the whole manifold rather than per solver contact.
context.friction = 0.3
context.restitution = 0.4
for i in range(context.num_solver_contacts()):
context.set_solver_contact(i, tangent_velocity=(10.0, 0.0, 0.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
print(f"Contact manifold has been modified {context.user_data} times since its creation.")
world.physics_hooks = MyPhysicsHooks()
Finally, ContactModificationContext.update_as_oneway_platform implements the removal of the contacts required for
one-way-platforms (it relies on the user_data of the manifold). It only keeps the contacts whose normal, in the
local-space of the first collider of the pair, is within the given angle of the given direction:
class OneWayPlatformHooks:
def __init__(self, platform):
self.platform = platform
def modify_solver_contacts(self, context):
# The allowed normal is expressed in the local-space of the first collider of the pair:
# it points upward if the platform is that first collider, and downward otherwise.
if context.collider1 == self.platform:
allowed_local_n1 = (0.0, 1.0, 0.0)
else:
allowed_local_n1 = (0.0, -1.0, 0.0)
# Remove the contacts unless the normal is within 45 degrees of the allowed one, so
# that the colliders can pass through the platform from below.
context.update_as_oneway_platform(allowed_local_n1, math.pi / 4.0)
platform_handle = world.add_collider(
rp.Collider.cuboid(2.0, 0.1, 2.0)
.translation((0.0, 3.0, 0.0))
.active_hooks(rp.ActiveHooks.MODIFY_SOLVER_CONTACTS)
)
world.physics_hooks = OneWayPlatformHooks(platform_handle)
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.