Skip to main content

Simulation structures

This page describes all the data structures needed for stepping the simulation, i.e., the structures owned by the physics worldthe components making up a physics context (spawned automatically by the RapierPhysicsPlugin), and the options controlling how it is simulatedthe world of the C API, which owns all the data structures needed for stepping the simulation, and these structuresall the data structures needed for stepping the simulation, i.e., the structures owned by the physics world, and how they are exposed to Python.

Physics context​

The structures described in this page are stored in the components of a physics context entity. The RapierPhysicsPlugin spawns one automatically during the PreStartup schedule, marked with the DefaultRapierContext component, so most applications don't need to create one themselves (see the multiple physics contexts page otherwise). A physics context is made of the following components:

  • RapierContextSimulation contains the island manager, the broad-phase, the narrow-phase, the CCD solver, the pipelines, and the integration parameters.
  • RapierContextColliders contains the collider set.
  • RapierRigidBodySet contains the rigid-body set and the soft-body set.
  • RapierContextJoints contains the impulse joint set and the multibody joint set.
  • RapierConfiguration contains the gravity and the other options of the simulation described in the configuration section.
  • SimulationToRenderTime contains the difference between the simulated time and the real time, which is used by the TimestepMode::Interpolated timestep mode.

The Rapier objects created from your entities store the bits of their entity in their user-data, and these components also map each entity to the handle of its Rapier object, e.g., with RapierRigidBodySet::rigid_body_entity or RapierContextColliders::collider_entity. Keep in mind that these objects are managed by the plugin: they should be modified through the components of their entities (rigid-bodies, colliders, joints, etc.) rather than through the sets directly.

The ReadRapierContext and WriteRapierContext system parameters give access to all the components of the default physics context at once, through the RapierContext and RapierContextMut structures (another context can be selected with the query filter given as their type parameter). This is what most of the scene queries rely on. When only one of these components is needed, it can be queried directly like any other component, e.g., with Query<&RapierContextSimulation, With<DefaultRapierContext>>.

Physics world​

Wiring every structure of this page by hand gives a complete control over the simulation, but this is rarely what an application needs. The PhysicsWorld is a façade owning all of them (the sets, the pipelines, the gravity, and the integration parameters), so a simulation can be created, stepped, and queried with a less verbose API. Every structure it owns remains accessible as a public field, therefore switching to the PhysicsWorld doesn't give up any feature (for example in case where borrowing the entire PhysicsWorld would cause borrowing issues):

// The world owns every structure needed by the simulation.
let mut world = PhysicsWorld::new();
world.gravity = Vector::new(0.0, -9.81);
world.integration_parameters.dt = 1.0 / 60.0;

/* Create the ground: a collider without any parent rigid-body. */
world.insert_collider(ColliderBuilder::cuboid(100.0, 0.1), None);

/* Create the bouncing ball: the rigid-body and its collider are inserted at once. */
let (ball_handle, _ball_collider) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 10.0)),
ColliderBuilder::ball(0.5).restitution(0.7),
);

/* Run the game loop, stepping the simulation once per frame. */
for _ in 0..200 {
world.step();
println!("Ball altitude: {}", world.bodies[ball_handle].translation().y);
}

The world also forwards the scene queries and the most common insertions and removals, which saves passing the sets they need to each other:

// The scene queries are available on the world directly.
let ray = Ray::new(Vector::new(0.0, 10.0), Vector::new(0.0, -1.0));
if let Some((handle, distance)) = world.cast_ray(&ray, 100.0, true, QueryFilter::default()) {
println!("Collider {:?} hit at distance {}", handle, distance);
}

// Every set remains reachable as a public field of the world.
let num_pairs = world.narrow_phase.contact_pairs().count();
println!("{} contact pairs", num_pairs);
info

The rest of this page describes the structures owned by the PhysicsWorld.

Physics world​

The world (R3World) owns all the structures described in this page: the sets of rigid-bodies, colliders, joints, and soft-bodies, the pipelines, the gravity, and the integration parameters. None of them is accessible directly: the world is created by r3NewWorld, then everything is inserted, stepped, queried, and removed through the functions of the world (or through the handles it returns, which remember the world they belong to). It must be freed by r3FreeWorld, which frees everything it contains:

/* The world owns every structure needed by the simulation. */
R2World *world = r2NewWorld();
r2SetGravity(world, r2Vector(0.0, -9.81));
r2SetTimeStep(world, 1.0 / 60.0);

/* Create the ground: a collider without any parent rigid-body. */
R2ColliderDesc ground = r2CuboidColliderDesc(r2Vector(100.0, 0.1));
r2InsertColliderWithoutParent(world, &ground);

/* Create the bouncing ball: a rigid-body, then its collider attached to it. */
R2RigidBodyDesc ball_body = r2DynamicRigidBodyDesc();
ball_body.position.translation = r2Vector(0.0, 10.0);
R2RigidBodyHandle ball_handle = r2InsertRigidBody(world, &ball_body);
R2ColliderDesc ball_collider = r2BallColliderDesc(0.5);
ball_collider.restitution = 0.7;
r2InsertCollider(ball_handle, &ball_collider);

/* Run the game loop, stepping the simulation once per frame. */
for (int i = 0; i < 200; i++) {
r2Step(world, NULL, NULL);
printf("Ball altitude: %f\n", (double)r2RigidBody_Translation(ball_handle).y);
}

The scene queries, and everything else the world contains, are read through its functions as well:

/* The scene queries are functions of the world. */
R2OptionalRayHit hit =
r2TryCastRay(world, NULL, r2Vector(0.0, 10.0), r2Vector(0.0, -1.0), 100.0, 1);
if (hit.found) {
printf("Collider {%u, %u} hit at distance %f\n", hit.hit.collider.index,
hit.hit.collider.generation, (double)hit.hit.time_of_impact);
}

/* So is everything else the world contains, e.g., its contact pairs. */
size_t num_pairs = r2ContactPairs(world, NULL, 0);
printf("%zu contact pairs\n", num_pairs);

Handles​

Each object inserted into the world is identified by a handle (R3RigidBodyHandle, R3ColliderHandle, R3ImpulseJointHandle, R3MultibodyJointHandle, or R3SoftBodyHandle). The world stores its objects in generational-arenas, i.e., vectors where each element is indexed by a handle combining an u32 index and an u32 generation number (in addition to the pointer of its world). This ensures that every object is given a unique handle, even if it reuses the slot of an object removed previously. The number of objects of each kind is given by r3RigidBodyCount, r3ColliderCount, r3ImpulseJointCount, r3MultibodyJointCount, and r3SoftBodyCount, and their handles are copied into a buffer given by the application by r3RigidBodyHandles, r3ColliderHandles, r3ImpulseJointHandles, r3MultibodyJointHandles, and r3SoftBodyHandles. Like every function copying an array, they return the number of elements needed if the buffer is NULL (or too small), so they are generally called twice:

printf("%zu rigid-bodies, %zu colliders, %zu soft-bodies, %zu impulse joints, %zu multibody joints\n",
r3RigidBodyCount(world), r3ColliderCount(world), r3SoftBodyCount(world),
r3ImpulseJointCount(world), r3MultibodyJointCount(world));

/* Ask for the number of handles, then copy them into a large enough buffer. */
size_t num_colliders = r3ColliderHandles(world, NULL, 0);
R3ColliderHandle *colliders = malloc(num_colliders * sizeof(R3ColliderHandle));
num_colliders = r3ColliderHandles(world, colliders, num_colliders);

for (size_t i = 0; i < num_colliders; i++) {
R3Vector translation = r3Collider_Translation(colliders[i]);
printf("Collider {%u, %u} is at altitude %f\n", colliders[i].index, colliders[i].generation,
(double)translation.y);
}
free(colliders);

The objects are removed with r3RemoveRigidBody (which also removes the joints attached to the rigid-body, and its colliders if its second argument is 1), r3RemoveCollider, r3RemoveImpulseJoint, r3RemoveMultibodyJoint, and r3RemoveSoftBody. The handle of a removed object is then stale: its generation doesn't match the object occupying its slot anymore, so the functions given this handle (including the removal functions) fail with the R3_INVALID_HANDLE error. r3RigidBody_Contains (resp. r3Collider_Contains, r3SoftBody_Contains) tells whether a handle still refers to an object of its world:

/* Remove the ball's rigid-body, together with its colliders (its joints are always removed). */
r3RemoveRigidBody(ball_handle, 1);

/* The handle of a removed object is stale: every function using it fails. */
if (!r3RigidBody_Contains(ball_handle)) {
printf("The ball is no longer part of the world.\n");
}
warning

The generation can only detect a stale handle while its world is alive. Once the world is freed by r3FreeWorld, all its handles (including their copies stored anywhere in your application) must never be used again.

User data​

Rigid-bodies, colliders, and impulse joints can store a 128-bit value of your choice, the user data (R3UserData, made of its low and high 64-bit halves). It is generally used to find the object of your application (e.g., the index of a game object) that owns a Rapier object, e.g., after a scene query or an event. It is given by the userData field of the descriptions, and can be modified at any time, e.g., with r3RigidBody_SetUserData (resp. r3Collider_SetUserData, r3ImpulseJoint_SetUserData), and read with r3RigidBody_UserData (resp. r3Collider_UserData, r3ImpulseJoint_UserData):

/* Any 128-bit value, e.g., the identifier of the game object owning the rigid-body. */
R3RigidBodyDesc body = r3DynamicRigidBodyDesc();
body.userData.low = 42;
R3RigidBodyHandle body_handle = r3InsertRigidBody(world, &body);

/* It can be modified and read at any time. */
R3UserData user_data = {43, 0};
r3RigidBody_SetUserData(body_handle, user_data);
printf("Game object: %llu\n", (unsigned long long)r3RigidBody_UserData(body_handle).low);

Parallelism​

If the library is built with -DRAPIER_ENABLE_PARALLEL=ON, the simulation runs on multiple threads. By default, it uses the global thread pool of rayon. r3SetNumThreads gives the world its own thread pool with the given number of threads (zero selecting a number of threads automatically), r3NumThreads gives its current size, and r3ClearThreadPool switches back to the global pool. These functions fail with the R3_UNSUPPORTED error if the library isn't built with parallelism, which is indicated by the parallel field of r3BuildFeatures():

/* Only supported if the library was built with -DRAPIER_ENABLE_PARALLEL=ON. */
if (r3BuildFeatures().parallel) {
/* The world gets its own pool of 4 threads, used from the next timestep. */
r3SetNumThreads(world, 4);
}

Keep in mind that the world isn't a shared structure you can modify from several threads at once: its ordinary reads (getters, scene queries) may run concurrently, but its modifications and its stepping require an exclusive access. A conflicting access (e.g. modifying a rigid-body while another thread steps its world, or from a physics hook running during the step) fails with the R3_WORLD_BUSY error instead of waiting. Note that in a parallel build, the physics hooks may be called from several threads at once.

Measuring the simulation​

If the library is built with the profiler feature (indicated by the profiling field of r3BuildFeatures()), the internal counters of the pipeline can be enabled with r3SetCountersEnabled. Then, r3StepTimeMs gives the time spent by the engine during the last timestep, in milliseconds:

/* Only supported if the library was built with the `profiler` feature. */
if (r3BuildFeatures().profiling) {
r3SetCountersEnabled(world, 1);
r3Step(world, NULL, NULL);
printf("The last timestep took %f ms\n", r3StepTimeMs(world));
}

Physics world​

The PhysicsWorld owns all the structures described in this page: the sets of rigid-bodies, colliders, joints, and soft-bodies, the pipelines, the gravity, and the integration parameters, as well as the physics hooks and the event handler given to each timestep. Each of them is exposed as a property of the world (e.g. PhysicsWorld.rigid_bodies, PhysicsWorld.colliders, or PhysicsWorld.narrow_phase) which returns the same Python object every time, so modifying it modifies the world directly. Therefore, a simulation can be created, stepped, and queried without ever creating these structures yourself:

# The world owns every structure needed by the simulation. Note that its gravity is zero
# unless it is given to its constructor.
world = rp.PhysicsWorld(gravity=(0.0, -9.81, 0.0))
world.integration_parameters.dt = 1.0 / 60.0

# Create the ground: a collider without any parent rigid-body.
world.add_collider(rp.Collider.cuboid(100.0, 0.1, 100.0))

# Create the bouncing ball: the rigid-body and its colliders are inserted at once.
ball_handle = world.add_body(
rp.RigidBody.dynamic(translation=(0.0, 10.0, 0.0)),
colliders=[rp.Collider.ball(0.5).restitution(0.7)],
)

# Run the game loop, stepping the simulation once per frame.
for _ in range(200):
world.step()
print("Ball altitude:", world.rigid_bodies[ball_handle].translation.y)
warning

Unlike the other versions of Rapier, a PhysicsWorld created without its gravity argument has no gravity at all.

The scene queries are run by the query pipeline of the world, and everything else it contains is read through its properties:

# The scene queries are run by the query pipeline of the world.
ray = rp.Ray((0.0, 10.0, 0.0), (0.0, -1.0, 0.0))
hit = world.query_pipeline.cast_ray(ray, max_toi=100.0, solid=True)
if hit is not None:
handle, distance = hit
print(f"Collider {handle} hit at distance {distance}")

# Every structure remains reachable as a property of the world.
num_pairs = len(world.narrow_phase.contact_pairs())
print(f"{num_pairs} contact pairs")

Handles​

Each object inserted into the world is identified by a handle (RigidBodyHandle, ColliderHandle, ImpulseJointHandle, MultibodyJointHandle, or SoftBodyHandle). The world stores its objects in generational-arenas, i.e., vectors where each element is indexed by a handle combining an integer index and an integer generation number (its index and generation properties). This ensures that every object is given a unique handle, even if it reuses the slot of an object removed previously. The handles are hashable, so they can be used as the keys of a dict.

Indexing a set with a handle, e.g., world.rigid_bodies[handle], gives a live view of the object it contains rather than a copy: modifying this view (e.g. setting its linvel) modifies the object stored in the world. The sets can also be iterated, which gives (handle, object) pairs, and their length is their number of objects:

# Each object inserted into the world is identified by a handle.
box_handle = world.add_body(
rp.RigidBody.dynamic(translation=(2.0, 1.0, 0.0)),
colliders=[rp.Collider.cuboid(0.5, 0.5, 0.5)],
)
print(f"Index: {box_handle.index}, generation: {box_handle.generation}")

# The object given by a set is a live view of the object it contains: modifying it
# modifies the object stored in the world.
box = world.rigid_bodies[box_handle]
box.linvel = (1.0, 0.0, 0.0)
assert world.rigid_bodies[box_handle].linvel.x == 1.0

# The sets can be iterated, and their length is their number of objects.
for handle, body in world.rigid_bodies:
print(f"Rigid body {handle} at {body.translation}")
print(f"{len(world.colliders)} colliders")

The objects are removed with PhysicsWorld.remove_body (which also removes the colliders and the joints attached to the rigid-body), PhysicsWorld.remove_collider, PhysicsWorld.remove_soft_body, ImpulseJointSet.remove, and MultibodyJointSet.remove. The handle of a removed object is then stale: its generation doesn't match the object occupying its slot anymore, so indexing a set with this handle raises an InvalidHandle exception, whereas the get method of the set returns None. The in operator tells whether a handle still refers to an object of a set:

# Removing a rigid-body also removes its colliders and the joints attached to it.
world.remove_body(box_handle)

# The handle is now stale: it doesn't refer to any object of the world anymore.
assert box_handle not in world.rigid_bodies
assert world.rigid_bodies.get(box_handle) is None
try:
world.rigid_bodies[box_handle]
except rp.InvalidHandle:
print("The box was removed.")
info

A view given by a set raises an InvalidHandle exception as well once its object is removed. Therefore, it is generally simpler to keep the handles of your objects rather than their views, and to index the set again whenever you need them.

User data​

Rigid-bodies, colliders, and joints can store an integer of your choice (up to 128 bits), the user data. It is generally used to find the object of your application (e.g., the index of a game object) that owns a Rapier object, e.g., after a scene query or an event. It is given by the user_data method of the builders, and can be read and modified at any time with the user_data property of the objects:

# The user data is an integer of your choice, e.g., the index of a game object.
game_objects = ["player", "enemy"]
enemy_handle = world.add_body(
rp.RigidBody.dynamic(translation=(-2.0, 1.0, 0.0)).user_data(1),
colliders=[rp.Collider.ball(0.5).user_data(1)],
)
enemy = world.rigid_bodies[enemy_handle]
print("This rigid-body belongs to the", game_objects[enemy.user_data])
# It can be modified at any time.
enemy.user_data = 0

Threads and the GIL​

The engine is always built with parallelism enabled: by default, the timesteps of every world run on the global pool of threads of rayon, with one worker per logical CPU, shared by every world. PhysicsWorld.set_num_threads gives the world its own pool with the given number of worker threads (1 running everything on the thread calling PhysicsWorld.step, and None switching back to the global pool), and PhysicsWorld.num_threads gives its current size. Note that the number of threads never changes the results of the simulation:

# Give this world its own pool of four worker threads.
world.set_num_threads(4)
assert world.num_threads == 4
# Run everything on the thread calling `world.step()`.
world.set_num_threads(1)
# Go back to the global pool shared by every world (one worker per logical CPU).
world.set_num_threads(None)

PhysicsWorld.step releases the GIL while the simulation runs, so your other Python threads keep running meanwhile. The GIL is acquired again whenever your physics hooks or your event handler are called during the timestep (possibly from one of the worker threads of the world). A world can be used from any thread, but keep in mind that it isn't a shared structure you can modify from several threads at once: while a thread steps the world, modifying it (or reading it anywhere else than in its callbacks) from another thread raises a RuntimeError. Several simulations can run in parallel by giving each thread its own world (and possibly its own pool of workers, so they don't compete for the same workers):

def simulate(results, i):
# Each thread simulates its own world.
world = rp.PhysicsWorld(gravity=(0.0, -9.81, 0.0))
world.add_collider(rp.Collider.cuboid(100.0, 0.1, 100.0))
ball = world.add_body(
rp.RigidBody.dynamic(translation=(0.0, 1.0 + i, 0.0)),
colliders=[rp.Collider.ball(0.5)],
)
for _ in range(100):
# The GIL is released during the step, so the other threads keep running.
world.step()
results[i] = world.rigid_bodies[ball].translation.y


results = [None] * 4
threads = [threading.Thread(target=simulate, args=(results, i)) for i in range(4)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()

Measuring the simulation​

The internal counters of the pipeline of the world (the PhysicsPipeline.counters property, enabled by default) measure each timestep: Counters.step_time_ms gives the time spent by the engine during the last timestep, in milliseconds, and its stages, cd, solver, and ccd properties detail the time spent by each stage of the pipeline. The counters are disabled with Counters.disable. Note that these timings are only measured if the bindings are built with the profiler feature, which is indicated by the profiler property of rapier3d.build_features():

world.step()
# The counters of the pipeline measure the last timestep (they are enabled by default).
counters = world.physics_pipeline.counters
print(f"Step time: {counters.step_time_ms} ms")
print(f"Collision detection: {counters.stages.collision_detection_time_ms} ms")
print(f"Solver: {counters.stages.solver_time_ms} ms")
# Disable them to save the (small) cost of the measurements.
counters.disable()

Gravity​

Gravity is represented as a vector. It affects every dynamic rigid-body taking part of the simulation. The gravity can be altered at each timestep (by modifying PhysicsWorld::gravity, or by passing a different vector to PhysicsPipeline::step)(by modifying the component field RapierConfiguration::gravity)(with r3SetGravity, its default value being −9.81-9.81 along the yy axis)(by modifying PhysicsWorld.gravity, or by passing a different vector to PhysicsPipeline.step). Learn more about per-rigid-body gravity modification in the dedicated section.

Integration parameters​

The IntegrationParametersR3IntegrationParameters (the RapierContextSimulation::integration_parameters field)(the PhysicsWorld.integration_parameters property) controls various aspects of the physics simulation, including the timestep length, number of solver iterations, number of CCD substeps, etc. The default integration parameters are set to achieve a good balance between performance and accuracy for games. They can be changed to make the simulation more accurate at the expense of a bit of performance. Learn more about each integration parameter in the API docsthe API docsthe dedicated pagethe dedicated page.

Island manager​

The IslandManagerisland manager of the world (the RapierContextSimulation::islands field) is responsible for tracking the set of dynamic rigid-bodies that are still moving and these that are no longer moving (and can ignored by subsequent timesteps to avoid useless computations). The island manager is automatically updated by PhysicsPipeline::stepr3Step and can be queried to retrieve the list of all the rigid-bodies modified by the physics engine during the last timestep. This can be useful to update the rendering of only the rigid-bodies that moved:

The IslandManager (the PhysicsWorld.islands property) is responsible for tracking the set of dynamic rigid-bodies that are still moving and these that are no longer moving (and can ignored by subsequent timesteps to avoid useless computations). The island manager is automatically updated by PhysicsPipeline.step (hence by PhysicsWorld.step), and can be queried to retrieve the list of all the rigid-bodies modified by the physics engine during the last timestep, e.g., with PhysicsWorld.active_bodies (iterating the island manager itself gives the same handles). This can be useful to update the rendering of only the rigid-bodies that moved:

# Iter on each rigid-bodies that moved (dynamic and kinematic).
for rigid_body_handle in world.active_bodies():
rigid_body = world.rigid_bodies[rigid_body_handle]
print(f"Rigid body {rigid_body_handle} has a new position: {rigid_body.position}")
// Iter on each rigid-bodies that moved (dynamic and kinematic).
for rigid_body_handle in world.islands.active_bodies() {
let rigid_body = &world.bodies[rigid_body_handle];
println!(
"Rigid body {:?} has a new position: {:?}",
rigid_body_handle,
rigid_body.position()
);
}
fn print_active_bodies(context: ReadRapierContext, transforms: Query<&Transform>) -> Result {
let context = context.single()?;
// Iter on each rigid-body that moved (dynamic and kinematic).
for handle in context.simulation.islands.active_bodies() {
let Some(entity) = context.rigidbody_set.rigid_body_entity(handle) else {
continue;
};
if let Ok(transform) = transforms.get(entity) {
println!("Rigid body {entity} has a new position: {}", transform.translation);
}
}
Ok(())
}
/* Iterate on each rigid-body that moved (dynamic and kinematic). */
size_t num_active = r2ActiveRigidBodies(world, NULL, 0);
R2RigidBodyHandle *active = malloc(num_active * sizeof(R2RigidBodyHandle));
num_active = r2ActiveRigidBodies(world, active, num_active);

for (size_t k = 0; k < num_active; k++) {
R2Pose position = r2RigidBody_Position(active[k]);
printf("Rigid body {%u, %u} has a new position: (%f, %f), %f rad\n", active[k].index,
active[k].generation, (double)position.translation.x, (double)position.translation.y,
(double)position.rotation.angle);
}
free(active);

The states of many rigid-bodies (their pose, velocities, sleeping state, etc.) can also be copied at once with r3RigidBodyReadStates, in a single call instead of one call per getter and per rigid-body.

Learn more about sleeping rigid-bodies in the dedicated section.

Physics pipeline​

The PhysicsPipelinephysics pipeline of the world (the RapierContextSimulation::pipeline field) is responsible for tying everything together in order to run the physics simulation. It will take care of updating every data-structures mentioned in this page (except the other pipelines), running the collision-detection, running the force computation and integration, and running CCD resolution.

PhysicsPipeline::step executes one timestep. Its usage is illustrated in the basic simulation example.

The pipeline is run by the systems of the RapierPhysicsPlugin, which are organized in three system sets executed in this order (in the PostUpdate schedule by default, before the propagation of the transforms):

  • PhysicsSet::SyncBackend creates, modifies, or removes the Rapier objects according to the components of your entities.
  • PhysicsSet::StepSimulation steps the simulation of each physics context. The number of timesteps executed at each run of this set, and their length, are selected by the TimestepMode resource described in the integration parameters page.
  • PhysicsSet::Writeback writes the results of the simulation back into the components (Transform, Velocity, etc.), and sends the collision events.

Therefore, your systems reading the results of the simulation should run after PhysicsSet::Writeback (or in a later schedule), and those modifying the components before PhysicsSet::SyncBackend. The simulation of a physics context can be paused by setting its RapierConfiguration::physics_pipeline_active to false:

fn toggle_pause(mut configurations: Query<&mut RapierConfiguration>) {
for mut configuration in configurations.iter_mut() {
configuration.physics_pipeline_active = !configuration.physics_pipeline_active;
}
}

r3Step executes one timestep. Its usage is illustrated in the basic simulation example. Its two last arguments are optional (NULL if unused):

  • the physics hooks, an R3PhysicsHooks structure of callbacks called during the timestep, e.g., to filter or modify the contacts.
  • the event collector, an R3EventCollector recording the collision and contact-force events generated during the timestep.
/* The events of every timestep it is given to are accumulated by the collector. */
R3EventCollector *events = r3NewEventCollector();
/* The unset callbacks (NULL) keep the default behavior. */
R3PhysicsHooks hooks = {0};
hooks.filter_contact_pair = filter_contact_pair;

for (int i = 0; i < 10; i++) {
r3Step(world, &hooks, events);
}

size_t num_events = r3EventCollector_CollisionEvents(events, NULL, 0);
printf("%zu collision events during the last 10 steps\n", num_events);
/* Discard the events once they are processed. */
r3EventCollector_Clear(events);
r3FreeEventCollector(events);

PhysicsWorld.step executes one timestep with the pipeline of the world (the PhysicsWorld.physics_pipeline property). Its usage is illustrated in the basic simulation example. The physics hooks and the event handler called during the timestep are the ones stored in the PhysicsWorld.physics_hooks and PhysicsWorld.event_handler properties (None if unused). An exception raised by one of these callbacks doesn't interrupt the timestep: the first one is raised again by PhysicsWorld.step once the timestep is complete. Setting PhysicsWorld.event_error_policy to "strict" (instead of "defer", the default) also skips the calls to the other callbacks of the timestep after the first exception.

Collision pipeline​

The CollisionPipelinecollision pipeline of the world is similar to the PhysicsPipelinephysics pipeline except that it will only run collision-detection. It won't perform any dynamics (force computation, integration, CCD, etc.) It is generally used instead of the PhysicsPipelinephysics pipeline when one only needs collision-detection.

info

Running both the CollisionPipeline and the PhysicsPipeliner3DetectCollisions and r3Step is useless because the PhysicsPipeliner3Step already does collision-detection.

The collision pipeline is run by r3DetectCollisions, which takes the same arguments as r3Step. The contact and intersection pairs, and the scene queries, are updated (and the collision events and physics hooks are handled) as if a timestep was executed, but no forces, joints, or contact responses are applied, and nothing moves by itself: the colliders only follow the rigid-bodies you moved. Besides simulations that don't need any dynamics, this is useful to update the contacts and the scene queries right after teleporting objects, without waiting for the next timestep:

/* Teleport the ball, then update the contacts and the scene queries without moving anything. */
r3RigidBody_SetTranslation(ball_handle, r3Vector(0.0, 0.4, 0.0), 1);
r3DetectCollisions(world, NULL, NULL);

R3OptionalRayHit hit =
r3TryCastRay(world, NULL, r3Vector(0.0, 10.0, 0.0), r3Vector(0.0, -1.0, 0.0), 100.0, 1);
printf("The ray now hits the ball at distance %f\n", (double)hit.hit.time_of_impact);

A physics context runs its CollisionPipeline (the RapierContextSimulation::collision_pipeline field) instead of its PhysicsPipeline if its RapierConfiguration::simulation_mode is set to SimulationMode::CollisionOnly (the default is SimulationMode::Full). The collision events, the contact and intersection pairs, the physics hooks, and the scene queries keep working in this mode, but no forces, joints, or contact responses are applied: the rigid-bodies only move when their Transform is modified (the kinematic position-based rigid-bodies are teleported to their next kinematic position). Note that the contact force events are never emitted in this mode.

The collision pipeline of the world (the PhysicsWorld.collision_pipeline property) is run by PhysicsWorld.detect_collisions, with the prediction distance of the integration parameters, and the physics hooks and event handler of the world (CollisionPipeline.step runs it on structures you step yourself). The contact and intersection pairs, and the scene queries, are updated (and the collision events and physics hooks are handled) as if a timestep was executed, but no forces, joints, or contact responses are applied, and nothing moves by itself: the colliders only follow the rigid-bodies you moved. Besides simulations that don't need any dynamics, this is useful to update the contacts and the scene queries right after teleporting objects, without waiting for the next timestep:

# Teleport the ball, then update the contacts and the scene queries right away.
world.rigid_bodies[ball_handle].translation = (0.0, 5.0, 0.0)
world.detect_collisions()

# The ray-cast now hits the ball at its new position.
hit = world.query_pipeline.cast_ray(ray, max_toi=100.0, solid=True)

Query pipeline​

The QueryPipelinequery pipeline of the world is responsible for efficiently running scene queries, e.g., ray-casting, shape-casting (sweep tests), intersection tests, on all the colliders of the scene.

The QueryPipeline is a temporary object that is initialized from the broad-phase, collider set, and rigid-body set. It reuses the acceleration data-structure (BVH) from the broad-phase which is automatically updated by the physics stepping function. This is what PhysicsWorld::query_pipeline does:

// A temporary query pipeline borrowing the broad-phase, the narrow-phase, and the sets. This
// is what `PhysicsWorld::query_pipeline` does.
let query_pipeline = world.broad_phase.as_query_pipeline(
world.narrow_phase.query_dispatcher(),
&world.bodies,
&world.colliders,
QueryFilter::default(),
);

The QueryPipeline is created from the physics context each time a scene query is run through the RapierContext (see its RapierContext::with_query_pipeline method).

Every scene query is a function of the world, e.g., r3CastRay, which builds a query pipeline internally. It reuses the acceleration data-structure (BVH) of the broad-phase, which is only updated by r3Step and r3DetectCollisions. Therefore, the scene queries don't see the colliders inserted, removed, or moved since the last call to one of these functions (see the collision pipeline above).

The query pipeline of the world (the PhysicsWorld.query_pipeline property) refers to the broad-phase, the narrow-phase, and the sets of the world. It reuses the acceleration data-structure (BVH) of the broad-phase, which is updated by PhysicsWorld.step (and by the collision pipeline above). Therefore, the scene queries don't see the colliders inserted, removed, or moved since the last timestep, unless PhysicsWorld.update_query_pipeline is called: it refreshes the BVH for these changes (without updating the contacts, and without interfering with the next timestep). A QueryPipeline can also be created from structures you step yourself:

# A query pipeline refers to the broad-phase, the narrow-phase, and the sets it reads. This is
# what `PhysicsWorld.query_pipeline` is made of.
query_pipeline = rp.QueryPipeline(
world.broad_phase, world.narrow_phase, world.rigid_bodies, world.colliders
)

Learn more about scene queries with the QueryPipeline in the dedicated page.

Rigid-body set​

The RigidBodySet contains all the rigid-bodies that needs to be simulated. This set is represented as a generational-arena, i.e., a vector where each element is indexed using a handle that combines an u32 index and an u32 generation number. This ensures that every rigid-body is given a unique handle. Learn more about rigid-bodies in the dedicated page.

Collider set​

The ColliderSet contains all the colliders that needs to be simulated. This set is represented as a generational-arena, i.e., a vector where each element is indexed using a handle that combines an u32 index and an u32 generation number. This ensures that every collider is given a unique handle. Learn more about colliders in the dedicated page.

Joint set​

The ImpulseJointSet contains all the impulse-based joints that needs to be simulated. This set is represented as a generational-arena, i.e., a vector where each element is indexed using a handle that combines an u32 index and an u32 generation number. This ensures that every joint is given a unique handle. Learn more about joints in the dedicated page.

Soft-body set​

The SoftBodySet contains all the soft-bodies that needs to be simulated, as well as the particles and the elements they are made of. Like the other sets, it is a generational-arena giving a unique handle to each soft-body. Note that this set must be given to the pipeline even if the simulation doesn't have any soft-body. Learn more about soft-bodies in the dedicated page.

Rigid-body set​

The RigidBodySet (the PhysicsWorld.rigid_bodies property) contains all the rigid-bodies that needs to be simulated. Like every set, it gives a unique handle to each of them. Learn more about rigid-bodies in the dedicated page.

Collider set​

The ColliderSet (the PhysicsWorld.colliders property) contains all the colliders that needs to be simulated. Learn more about colliders in the dedicated page.

Joint sets​

The ImpulseJointSet (the PhysicsWorld.impulse_joints property) contains all the impulse-based joints that needs to be simulated, and the MultibodyJointSet (the PhysicsWorld.multibody_joints property) contains all the multibody joints. Learn more about joints in the dedicated page.

Soft-body set​

The SoftBodySet (the PhysicsWorld.soft_bodies property) contains all the soft-bodies that needs to be simulated, as well as the particles and the elements they are made of. Learn more about soft-bodies in the dedicated page.

CCD solver​

The CCD solver (the RapierContextSimulation::ccd_solver field) is responsible for the resolution of Continuous-Collision-Detection. By itself, this structure doesn't expose any useful feature. So it should simply be passed to the PhysicsPipeline::step method.It is given to the pipeline automatically by the bevy_rapier plugin.It is owned and run by the world.So it should simply be passed to PhysicsPipeline.step, which PhysicsWorld.step does with its own CCD solver (the PhysicsWorld.ccd_solver property). Learn more about CCD in the dedicated section.

Physics hooks​

The physics hooks are trait-objects implementing the PhysicsHooks traitcallbacks grouped in an R3PhysicsHooks structure given to r3Step. They can be used to apply arbitrary rules to ignore collision detection between some pairs of colliders. They can also be used to modify the contacts processed by the constraints solver for computing forces.

The physics hooks are a system parameter implementing the BevyPhysicsHooks trait, given as the type parameter of the RapierPhysicsPlugin (NoUserData if there are none).

The physics hooks are objects of your own classes implementing the methods of the PhysicsHooks protocol (filter_contact_pair, filter_intersection_pair, and modify_solver_contacts), stored in the PhysicsWorld.physics_hooks property. They can be used to apply arbitrary rules to ignore collision detection between some pairs of colliders. They can also be used to modify the contacts processed by the constraints solver for computing forces. Note that these classes don't need to inherit from PhysicsHooks, and only need to define the methods they use. The sets being stepped can be read during these calls (e.g. through the colliders and bodies properties of the context they are given), but not modified.

Learn more about physics hooks in the dedicated section.

Event handler​

The event handlers are trait-objects implementing the EventHandler traitR3EventCollector objects given to r3Step, created by r3NewEventCollector and freed by r3FreeEventCollector. They can be used to get notified when two non-sensor colliders start/stop having contacts, and when one sensor collider and one other collider start/stop intersecting. These events are sent as the CollisionEvent and ContactForceEvent Bevy messages, and your own EventHandler can be installed in addition with RapierContextSimulation::set_event_handler. Learn more about collision events in the dedicated section.

The event handlers are objects implementing the methods of the EventHandler protocol (handle_collision_event, handle_contact_force_event, and handle_soft_body_tear_event), stored in the PhysicsWorld.event_handler property, e.g., a ChannelEventCollector which records the events so they can be read after the timestep. They can be used to get notified when two non-sensor colliders start/stop having contacts, and when one sensor collider and one other collider start/stop intersecting. Learn more about collision events in the dedicated section.

Stepping the structures by hand​

Everything described in this page can be created and stepped without the physics world, which is exactly what the world does internally. This is useful when the structures must be owned by different parts of your application, e.g., by the resources of an ECS. Note that every one of them is then given to the pipeline at each timestep:

fn manual_stepping() {
let mut rigid_body_set = RigidBodySet::new();
let mut collider_set = ColliderSet::new();

/* Create the ground. */
let collider = ColliderBuilder::cuboid(100.0, 0.1).build();
collider_set.insert(collider);

/* Create the bouncing ball. */
let rigid_body = RigidBodyBuilder::dynamic()
.translation(Vector::new(0.0, 10.0))
.build();
let collider = ColliderBuilder::ball(0.5).restitution(0.7).build();
let ball_body_handle = rigid_body_set.insert(rigid_body);
collider_set.insert_with_parent(collider, ball_body_handle, &mut rigid_body_set);

/* Create other structures necessary for the simulation. */
let gravity = Vector::new(0.0, -9.81);
let integration_parameters = IntegrationParameters::default();
let mut physics_pipeline = PhysicsPipeline::new();
let mut island_manager = IslandManager::new();
let mut broad_phase = DefaultBroadPhase::new();
let mut narrow_phase = NarrowPhase::new();
let mut impulse_joint_set = ImpulseJointSet::new();
let mut multibody_joint_set = MultibodyJointSet::new();
let mut soft_body_set = SoftBodySet::new();
let mut ccd_solver = CCDSolver::new();
let physics_hooks = ();
let event_handler = ();

/* Run the game loop, stepping the simulation once per frame. */
for _ in 0..200 {
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 soft_body_set,
&mut ccd_solver,
&physics_hooks,
&event_handler,
);

let ball_body = &rigid_body_set[ball_body_handle];
println!("Ball altitude: {}", ball_body.translation().y);
}
}

Stepping the structures by hand​

Everything described in this page can be created and stepped without the physics world, which is exactly what the world does internally. This is useful when the structures must be owned by different parts of your application. Note that every one of them is then given to PhysicsPipeline.step at each timestep (except the soft-body set, the physics hooks, and the event handler which are optional keyword arguments):

rigid_body_set = rp.RigidBodySet()
collider_set = rp.ColliderSet()

# Create the ground.
collider_set.insert(rp.Collider.cuboid(100.0, 0.1, 100.0))

# Create the bouncing ball.
ball_body_handle = rigid_body_set.insert(rp.RigidBody.dynamic(translation=(0.0, 10.0, 0.0)))
collider = rp.Collider.ball(0.5).restitution(0.7)
collider_set.insert_with_parent(collider, ball_body_handle, rigid_body_set)

# Create other structures necessary for the simulation.
gravity = (0.0, -9.81, 0.0)
integration_parameters = rp.IntegrationParameters()
physics_pipeline = rp.PhysicsPipeline()
island_manager = rp.IslandManager()
broad_phase = rp.BroadPhaseBvh()
narrow_phase = rp.NarrowPhase()
impulse_joint_set = rp.ImpulseJointSet()
multibody_joint_set = rp.MultibodyJointSet()
soft_body_set = rp.SoftBodySet()
ccd_solver = rp.CCDSolver()
physics_hooks = None
event_handler = None

# Run the game loop, stepping the simulation once per frame.
for _ in range(200):
physics_pipeline.step(
gravity,
integration_parameters,
island_manager,
broad_phase,
narrow_phase,
rigid_body_set,
collider_set,
impulse_joint_set,
multibody_joint_set,
ccd_solver,
hooks=physics_hooks,
events=event_handler,
soft_bodies=soft_body_set,
)

ball_body = rigid_body_set[ball_body_handle]
print("Ball altitude:", ball_body.translation.y)

Configuration​

The RapierConfiguration component of a physics context contains the options of its simulation that are not part of the integration parameters:

  • gravity is the gravity vector applied to the dynamic rigid-bodies. Its default value, −9.81-9.81 along the yy axis, is multiplied by the length unit of the context.
  • physics_pipeline_active can be set to false to pause the simulation.
  • simulation_mode selects between the full simulation (SimulationMode::Full) and collision-detection only (SimulationMode::CollisionOnly).
  • num_threads gives the physics context its own thread pool with the given number of threads. If None (the default), the simulation runs on the thread pool of the calling thread (usually the global pool of rayon). This is ignored unless the parallel feature is enabled (and the unsync-callbacks feature isn't).
  • scaled_shape_subdivision is the number of subdivisions used to approximate a shape which cannot be represented exactly once the scale of its Transform is applied (e.g. a ball with a non-uniform scale becomes a convex polyhedron).
  • force_update_from_transform_changes makes the plugin accept every change of the Transform of the rigid-bodies, including the changes that may originate from its own writeback.
fn modify_configuration(
mut configurations: Query<&mut RapierConfiguration, With<DefaultRapierContext>>,
) -> Result {
let mut configuration = configurations.single_mut()?;
configuration.gravity = Vec2::new(0.0, -981.0);
// Only detect collisions: no forces, joints, or contact responses.
configuration.simulation_mode = SimulationMode::CollisionOnly;
// Run the simulation of this context on its own pool of 4 threads (needs the
// `parallel` feature).
configuration.num_threads = Some(4);
Ok(())
}

The initial configuration of the default physics context can also be given to the RapierPhysicsPlugin, as shown in the integration parameters page.

Diagnostics​

The RapierDiagnosticsPlugin registers Bevy diagnostics measuring the simulation of every physics context, so they can be displayed with any tool reading Bevy's diagnostics, e.g., with the LogDiagnosticsPlugin of Bevy:

App::new()
.add_plugins((
DefaultPlugins,
RapierPhysicsPlugin::<NoUserData>::default(),
// Measures the simulation of every context after each physics update (the
// measurements are summed over the contexts), and of each context separately.
RapierDiagnosticsPlugin::default().with_per_context_diagnostics(true),
// Prints every registered diagnostic once per second.
LogDiagnosticsPlugin::default(),
))

The diagnostics are identified by the constants RapierDiagnosticsPlugin::STEP_TIME (the total time spent in the simulation steps of a physics update), RapierDiagnosticsPlugin::STEPS, RapierDiagnosticsPlugin::RIGID_BODIES, RapierDiagnosticsPlugin::ACTIVE_BODIES, RapierDiagnosticsPlugin::COLLIDERS, RapierDiagnosticsPlugin::CONTACT_PAIRS, RapierDiagnosticsPlugin::SOLVER_CONSTRAINTS, etc. The time spent by each stage of the pipeline (broad-phase, narrow-phase, solver, CCD, etc.) is only measured if the profiler feature is enabled. Note that the plugin must run in the same schedule as the physics: if the RapierPhysicsPlugin runs in FixedUpdate, use RapierDiagnosticsPlugin::default().in_schedule(FixedUpdate).

The diagnostics at these paths are the sums of the measurements of all the physics contexts (counts and timings alike), so they measure the whole physics workload of the application. If RapierDiagnosticsPlugin::with_per_context_diagnostics(true) is given, as in the example above, each context is also measured separately, at the paths given by RapierDiagnosticsPlugin::context_path (e.g. rapier/12v0/step_time for the step time of the context with the entity 12v0). These are registered when the context is measured for the first time:

fn print_context_step_times(
diagnostics: Res<DiagnosticsStore>,
contexts: Query<Entity, With<RapierContextSimulation>>,
) {
for context in &contexts {
// The path of the step time of this context only.
let path =
RapierDiagnosticsPlugin::context_path(&RapierDiagnosticsPlugin::STEP_TIME, context);
if let Some(step_time) = diagnostics.get(&path).and_then(|d| d.smoothed()) {
println!("The context {context} spends {step_time:.3}ms per physics update.");
}
}
}

The same measurements can be read without Bevy's diagnostics from RapierContextSimulation::step_stats, which describes the timesteps executed by the last physics update. Only the number of steps and their total time are measured unless the counters of the PhysicsPipeline are enabled (which the RapierDiagnosticsPlugin does for every physics context):

fn print_step_stats(
contexts: Query<&RapierContextSimulation, With<DefaultRapierContext>>,
) -> Result {
let stats = contexts.single()?.step_stats();
if stats.num_steps > 0 {
println!(
"{} steps in {:.3}ms, {} contact pairs",
stats.num_steps, stats.step_time_ms, stats.num_contact_pairs
);
}
Ok(())
}