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 world, and how they are exposed to Python.

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). Learn more about per-rigid-body gravity modification in the dedicated section.

Integration parameters​

The IntegrationParameters (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 dedicated page.

Island manager​

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}")

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

Physics pipeline​

The PhysicsPipeline 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.

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 CollisionPipeline is similar to the PhysicsPipeline 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 PhysicsPipeline when one only needs collision-detection.

info

Running both the CollisionPipeline and the PhysicsPipeline is useless because the PhysicsPipeline already does collision-detection.

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 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 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 (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 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 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 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 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. 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)