Skip to main content

The Rapier testbed

The Rapier testbed is a small physics sandbox based on the kiss3d renderer. It is designed to easily open a window and render a physics worldphysics worldphysics world. It provides basic controls like play/pause, grabbing objects with the mouse, changing simulation settings, etc. This is the application all the demos of Rapier are written with, and it is generally the quickest way of prototyping a scene, or of reproducing a problem before opening an issue.

The testbed is published as the rapier_testbed2d and rapier_testbed3d crates, which share the same version as Rapier itself.

Example​

A scene is one function building a PhysicsWorld, giving it to the viewer, and then stepping it from the rendering loop. Note that the loop is what owns the simulation: the viewer only draws the state of the world it is given, and tells whether the user asked for the simulation to keep running or to stop:

async fn bouncing_ball(viewer: &mut TestbedViewer) {
// The scene itself, built like in any other application.
let mut world = PhysicsWorld::new();
world.insert_collider(ColliderBuilder::cuboid(100.0, 0.1, 100.0), None);
world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 10.0, 0.0)),
ColliderBuilder::ball(0.5).restitution(0.7),
);

// Hand the world to the viewer, and place the camera.
viewer.set_world(&mut world);
viewer.look_at(Vec3::new(10.0, 10.0, 10.0), Vec3::ZERO);

// The rendering loop: it ends when the user closes the window or selects another scene.
while viewer.render_frame(&mut world).await {
if viewer.simulating() {
world.step();
}
}
}

The testbed itself is started from the main function of the application, which lists the scenes it can switch between, and runs the one to display. Note that this function relies on the kiss3d::main attribute, therefore kiss3d must be a dependency of the application as well:

#[kiss3d::main]
async fn main() {
// The scenes listed in the side panel of the testbed, as (group, name) pairs.
let entries = vec![ExampleEntry::new("Demos", "Bouncing ball")];
let mut viewer = TestbedViewer::new(entries).await;
bouncing_ball(&mut viewer).await;
}

Running the demos​

The examples of Rapier are all combined in a single application listing them in a side panel. Running them is the fastest way to get an overview of Rapier’s features and performances.

git clone https://github.com/dimforge/rapier
cd rapier
cargo run --release --bin all_examples2 --features parallel # 2D demos
cargo run --release --bin all_examples3 --features parallel # 3D demos
warning

Always run the testbed (and Rapier in general) in release mode: a debug build is up to 100 times slower. Parallelism is enabled with --features parallel.

The C bindings come with their own testbed: a small physics sandbox written in C, based on the raylib renderer and on Dear ImGui for its user interface. It is designed to render a physics world, and provides basic controls like play/pause, grabbing objects with the mouse, changing simulation settings (timestep, gravity, solver iterations, number of threads, etc.), saving and restoring snapshots, debug-rendering, etc. It runs the C ports of all the demos of Rapier, which are also the most complete examples of usage of the C API.

Running the demos​

The testbed is part of the c folder of the Rapier repository, and is built by enabling the RAPIER_BUILD_TESTBED option of CMake. It downloads its graphics dependencies during its first configuration (on Linux, the X11 and OpenGL development packages must be installed). From the root of the repository:

cmake -S bindings/c -B build/c3 -DRAPIER_BUILD_TESTBED=ON -DRAPIER_DIMENSION=3 -DCMAKE_BUILD_TYPE=Release
cmake --build build/c3 --target rapier_testbed --config Release --parallel
./build/c3/testbed/rapier_testbed # 3D demos

The 2D demos are built the same way, in another build directory, with -DRAPIER_DIMENSION=2. The demos relying on optional features (the FEM solver of the soft-bodies, the URDF and MJCF loaders) are only listed if these features are enabled, e.g., with -DRAPIER_FEATURES=fem,robotics. A demo can be selected at startup with the --example argument (e.g. --example restitution3), and the number of threads used by the simulation with --threads (the parallelism is enabled by default for the testbed).

warning

Always run the testbed (and Rapier in general) in release mode: a debug build is up to 100 times slower. The build mode of the Rapier library actually loaded is displayed by the testbed.

Controls​

  • T: play/pause. S: execute one timestep. R: restart the demo. F: frame the whole simulation.
  • Left drag: pull a dynamic object (or a particle of a soft-body) with a spring joint.
  • Right drag: rotate the camera around its target in 3D, or pan it in 2D. Shift + right drag (or middle drag): pan the camera in 3D. Mouse wheel: zoom.
  • The side panel gives access to the list of demos, the simulation settings, the performance measurements (the time spent by the engine for each timestep, etc.), and the debug-rendering options.

Headless runs​

The rapier_testbed_headless executable (built together with the testbed) runs the same demos without opening any window, which is useful to check that a demo runs without errors, or to measure its performance:

./build/c3/testbed/rapier_testbed_headless --list
./build/c3/testbed/rapier_testbed_headless --example restitution3 --steps 120 --threads 1

Writing a scene​

Unlike the Rust testbed, the C testbed isn't a library meant to be used by your own application: it is an application of the Rapier repository, which demos are compiled with it. Each demo is one C function (e.g. tbRestitution3 in bindings/c/testbed/examples3d/restitution3.c) building a world like in any other application, giving it to the viewer, and then stepping it from the rendering loop. Note that the loop is what owns the simulation: the viewer only draws the state of the world it is given, and tells whether the user asked for the simulation to keep running or to stop:

void tbBouncingBall3(Testbed *testbed) {
/* The scene itself, built like in any other application. */
R3World *world = r3NewWorld();
R3ColliderDesc ground = r3CuboidColliderDesc(r3Vector(100.0, 0.1, 100.0));
r3InsertColliderWithoutParent(world, &ground);

R3RigidBodyDesc ball_body = r3DynamicRigidBodyDesc();
ball_body.position.translation = r3Vector(0.0, 10.0, 0.0);
R3ColliderDesc ball = r3BallColliderDesc(0.5);
ball.restitution = 0.7;
r3InsertCollider(r3InsertRigidBody(world, &ball_body), &ball);

/* Hand the world to the viewer, and place the camera (eye, then target). */
tbSetWorld(testbed, world);
tbCamera(testbed, 10, 10, 10, 0, 0, 0);

/* The rendering loop: it ends when the user closes the window or selects another scene. The
* viewer may replace the world, e.g., when a snapshot is restored, hence its address. */
while (tbRenderFrame(testbed, &world)) {
if (tbSimulating(testbed)) {
r3Step(world, NULL, NULL);
}
}
r3FreeWorld(world);
}

Therefore, prototyping a scene with the testbed (e.g. for reproducing a problem before opening an issue) is done in your local copy of the repository: its source file is added to the bindings/c/testbed/examples3d folder (resp. examples2d), which files are all compiled with the testbed, and its function is added to the demos listed by the tbExamples array of bindings/c/testbed/registry3.c (resp. registry2.c):

/* An entry of the `tbExamples` array: identifier, group, name, source, scene function, and the
* reason why it is unavailable (NULL if it is available). */
TbExample entry = {"bouncing_ball3", "Demos", "Bouncing ball", "examples3d/bouncing_ball3.c",
tbBouncingBall3, NULL};
info

The registries are generated from the list of the Rust demos by the bindings/c/testbed/update_catalog.py script, which doesn't know about the entries added by hand: they are lost whenever the registries are generated again.

The Python bindings come with their own testbed: a small physics sandbox written in Python, based on the Panda3D renderer. It is designed to easily open a window and render a simulation, and provides basic controls like play/pause, single-stepping, restarting the scene, switching between scenes, or showing the wireframe of the colliders with the debug-renderer. It runs the Python ports of most of the 3D demos of Rapier, which are also good examples of usage of the bindings. It is generally the quickest way of prototyping a scene, or of reproducing a problem before opening an issue.

Installation​

The testbed is published as the rapier-testbed package, which depends on rapier3d, NumPy, and Panda3D:

pip install rapier-testbed

It can also be installed from the bindings/python/rapier-testbed folder of the Rapier repository, e.g., to use it with bindings built from source. From the root of the repository, and with your virtual environment activated (--no-deps keeps the bindings you built instead of downloading them, and -e makes your modifications of the testbed effective without reinstalling it):

pip install panda3d numpy
pip install --no-deps -e ./bindings/python/rapier-testbed

Running the demos​

The demos are the modules of the rapier_testbed.examples3 package (e.g. rapier_testbed/examples3/domino3.py). Running the rapier_testbed module lists them by category in the terminal, and asks which one to open. A demo can also be opened directly by running its module:

python -m rapier_testbed # List the demos, and open the selected one.
python -m rapier_testbed.examples3.domino3 # Open the domino demo.

The bindings/python/examples_tour.py script of the repository opens every demo in turn, each of them being opened when the window of the previous one is closed.

warning

Always run the testbed with bindings built in release mode (see common mistakes): a debug build is up to 100 times slower.

Controls​

  • Space: play/pause. Right arrow: execute one timestep. R: restart the demo. Tab: open the next demo (the digits 1 to 9 open the demo with this number). W: toggle the wireframe of the colliders. Esc: close the demo.
  • Left drag: rotate the camera around its target. Right drag: pan the camera. Mouse wheel: zoom.
  • The time spent by the simulation and by the rendering of each frame, as well as the frame rate, are displayed at the top of the window.

Headless runs​

If the PANDA_NO_WINDOW environment variable is set to 1, the testbed runs the demo for a fixed number of timesteps without opening any window, which is useful to check that a demo runs without errors (e.g. in a CI):

PANDA_NO_WINDOW=1 python -m rapier_testbed.examples3.domino3

Writing a scene​

A scene is one function building a physics world, and giving it to the testbed with Testbed.set_world. Unlike the testbed of the Rust version of Rapier, the testbed owns the simulation loop: it steps the world it is given at each frame (with its gravity, integration parameters, physics hooks, and event handler), and calls the functions registered by the scene with Testbed.add_callback after each timestep, e.g., to control the scene or to read the results of the simulation:

def bouncing_ball(testbed):
# The scene itself, built like in any other application.
world = rp.PhysicsWorld(gravity=(0.0, -9.81, 0.0))
world.add_collider(rp.Collider.cuboid(100.0, 0.1, 100.0))
ball_handle = world.add_body(
rp.RigidBody.dynamic(translation=(0.0, 10.0, 0.0)),
colliders=[rp.Collider.ball(0.5).restitution(0.7)],
)

# Hand the world to the testbed, and place the camera.
testbed.set_world(world)
testbed.look_at((10.0, 10.0, 10.0), (0.0, 0.0, 0.0))

# A function called after each timestep, e.g., to control the scene.
def print_altitude(testbed):
print("Ball altitude:", world.rigid_bodies[ball_handle].translation.y)

testbed.add_callback(print_altitude)

The scene is then registered with a category and a name, which makes it available in the list of the testbed together with the demos, and the testbed can be opened on it directly:

from rapier_testbed import register, run

# The scenes listed by the picker of the testbed, as (category, name) pairs.
register("Demos", "Bouncing ball", bouncing_ball)

if __name__ == "__main__":
# Open the testbed directly on this scene.
run(initial="Demos / Bouncing ball")

Instead of a world, Testbed.set_world also accepts the sets of a simulation (the rigid-body, collider, impulse-joint, and multibody-joint sets, and optionally the soft-body set), which the testbed then steps with its own pipeline and integration parameters, and with a gravity of −9.81-9.81 along the yy axis. In this case, the gravity, the physics hooks, and the event handler of the simulation are given with Testbed.set_gravity, Testbed.set_hooks, and Testbed.set_event_handler.