Simulation structures
This page describes RapierPhysicsPlugin), and the options controlling how it is
simulated
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:
RapierContextSimulationcontains the island manager, the broad-phase, the narrow-phase, the CCD solver, the pipelines, and the integration parameters.RapierContextColliderscontains the collider set.RapierRigidBodySetcontains the rigid-body set and the soft-body set.RapierContextJointscontains the impulse joint set and the multibody joint set.RapierConfigurationcontains the gravity and the other options of the simulation described in the configuration section.SimulationToRenderTimecontains the difference between the simulated time and the real time, which is used by theTimestepMode::Interpolatedtimestep 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):
- Example 2D
- Example 3D
// 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 owns every structure needed by the simulation.
let mut world = PhysicsWorld::new();
world.gravity = Vector::new(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.insert_collider(ColliderBuilder::cuboid(100.0, 0.1, 100.0), 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, 0.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:
- Example 2D
- Example 3D
// 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);
// The scene queries are available on the world directly.
let ray = Ray::new(Vector::new(0.0, 10.0, 0.0), Vector::new(0.0, -1.0, 0.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);
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:
- Example 2D
- Example 3D
/* 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 world owns every structure needed by the simulation. */
R3World *world = r3NewWorld();
r3SetGravity(world, r3Vector(0.0, -9.81, 0.0));
r3SetTimeStep(world, 1.0 / 60.0);
/* Create the ground: a collider without any parent rigid-body. */
R3ColliderDesc ground = r3CuboidColliderDesc(r3Vector(100.0, 0.1, 100.0));
r3InsertColliderWithoutParent(world, &ground);
/* Create the bouncing ball: a rigid-body, then its collider attached to it. */
R3RigidBodyDesc ball_body = r3DynamicRigidBodyDesc();
ball_body.position.translation = r3Vector(0.0, 10.0, 0.0);
R3RigidBodyHandle ball_handle = r3InsertRigidBody(world, &ball_body);
R3ColliderDesc ball_collider = r3BallColliderDesc(0.5);
ball_collider.restitution = 0.7;
r3InsertCollider(ball_handle, &ball_collider);
/* Run the game loop, stepping the simulation once per frame. */
for (int i = 0; i < 200; i++) {
r3Step(world, NULL, NULL);
printf("Ball altitude: %f\n", (double)r3RigidBody_Translation(ball_handle).y);
}
The scene queries, and everything else the world contains, are read through its functions as well:
- Example 2D
- Example 3D
/* 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);
/* The scene queries are functions of the world. */
R3OptionalRayHit hit =
r3TryCastRay(world, NULL, r3Vector(0.0, 10.0, 0.0), r3Vector(0.0, -1.0, 0.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 = r3ContactPairs(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");
}
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)
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.")
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 PhysicsWorld::gravity, or by passing a different vector to
PhysicsPipeline::step)RapierConfiguration::gravity)r3SetGravity, its default value being along the axis)PhysicsWorld.gravity, or by passing a different vector to PhysicsPipeline.step)
Integration parameters
The IntegrationParametersR3IntegrationParametersRapierContextSimulation::integration_parameters field)PhysicsWorld.integration_parameters property)
Island manager
The IslandManagerRapierContextSimulation::islands field)PhysicsPipeline::stepr3Step
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 PhysicsPipelineRapierContextSimulation::pipeline field)
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::SyncBackendcreates, modifies, or removes the Rapier objects according to the components of your entities.PhysicsSet::StepSimulationsteps the simulation of each physics context. The number of timesteps executed at each run of this set, and their length, are selected by theTimestepModeresource described in the integration parameters page.PhysicsSet::Writebackwrites 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
R3PhysicsHooksstructure of callbacks called during the timestep, e.g., to filter or modify the contacts. - the event collector, an
R3EventCollectorrecording 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 CollisionPipelinePhysicsPipelinePhysicsPipeline
Running both CollisionPipeline and the PhysicsPipeliner3DetectCollisions and r3StepPhysicsPipeliner3Step
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 QueryPipeline
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 queriesQueryPipeline
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 RapierContextSimulation::ccd_solver field)PhysicsPipeline::step method.bevy_rapier plugin.PhysicsPipeline.step, which PhysicsWorld.step does with its own CCD solver (the PhysicsWorld.ccd_solver property).
Physics hooks
The physics hooks are PhysicsHooks traitR3PhysicsHooks structure given to r3Step
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 EventHandler traitR3EventCollector objects given to r3Step, created by r3NewEventCollector and freed by r3FreeEventCollectorCollisionEvent and
ContactForceEvent Bevy messages, and your own EventHandler can be installed in addition with
RapierContextSimulation::set_event_handler.
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:
- Example 2D
- Example 3D
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);
}
}
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, 100.0).build();
collider_set.insert(collider);
/* Create the bounding ball. */
let rigid_body = RigidBodyBuilder::dynamic()
.translation(Vector::new(0.0, 10.0, 0.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, 0.0);
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:
gravityis the gravity vector applied to the dynamic rigid-bodies. Its default value, along the axis, is multiplied by the length unit of the context.physics_pipeline_activecan be set tofalseto pause the simulation.simulation_modeselects between the full simulation (SimulationMode::Full) and collision-detection only (SimulationMode::CollisionOnly).num_threadsgives the physics context its own thread pool with the given number of threads. IfNone(the default), the simulation runs on the thread pool of the calling thread (usually the global pool ofrayon). This is ignored unless theparallelfeature is enabled (and theunsync-callbacksfeature isn't).scaled_shape_subdivisionis the number of subdivisions used to approximate a shape which cannot be represented exactly once the scale of itsTransformis applied (e.g. a ball with a non-uniform scale becomes a convex polyhedron).force_update_from_transform_changesmakes the plugin accept every change of theTransformof the rigid-bodies, including the changes that may originate from its own writeback.
- Example 2D
- Example 3D
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(())
}
fn modify_configuration(
mut configurations: Query<&mut RapierConfiguration, With<DefaultRapierContext>>,
) -> Result {
let mut configuration = configurations.single_mut()?;
configuration.gravity = Vec3::new(0.0, -9.81, 0.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(())
}