Skip to main content

Simulation structures

This page describes the world of the C API, which owns all the data structures needed for stepping the simulation, and these structures.

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

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 (with r3SetGravity, its default value being −9.81-9.81 along the yy axis). Learn more about per-rigid-body gravity modification in the dedicated section.

Integration parameters​

The R3IntegrationParameters 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 island manager of the world 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 r3Step 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:

/* 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 physics pipeline of the world 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.

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);

Collision pipeline​

The collision pipeline of the world is similar to the physics 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 physics pipeline when one only needs collision-detection.

info

Running both r3DetectCollisions and r3Step is useless because r3Step 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);

Query pipeline​

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

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

Learn more about scene queries 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. It is owned and run by the world. Learn more about CCD in the dedicated section.

Physics hooks​

The physics hooks are callbacks 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.

Learn more about physics hooks in the dedicated section.

Event handler​

The event handlers are R3EventCollector 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. Learn more about collision events in the dedicated section.