Rigid-body simulation

The real-time simulation of rigid-bodies subjected to forces and contacts is the main feature of a physics engine for video-games, robotics, or animation. Rigid-bodies are typically used to simulate the dynamics of non-deformable solids as well as to integrate the trajectory of solids which velocities are controlled by the user (e.g. moving platforms). On the other hand, rigid-bodies are not enough to simulate, e.g., cars, ragdolls, or robotic systems, as those use-cases require adding restrictions on the relative motion between their parts with joints.

In this chapter, we first show how to initialize a physics world which will construct all that is to be physically simulated and will drive the physics simulation.

Then we introduce colliders which are geometric shapes responsible for generating contacts or simulating sensors. Colliders are attached to rigid-bodies which are responsible for the simulation of the object trajectory under various forces including gravity.

Basic setup

First, let's see what is the minimum set of elements that must be setup to have an empty physical simulation up and running:

use rapier2d::na::Vector2;
use rapier2d::dynamics::{JointSet, RigidBodySet, IntegrationParameters};
use rapier2d::geometry::{BroadPhase, NarrowPhase, ColliderSet};
use rapier2d::pipeline::PhysicsPipeline;
fn main() {
// Here the gravity is -9.81 along the y axis.
let mut pipeline = PhysicsPipeline::new();
let gravity = Vector2::new(0.0, -9.81);
let integration_parameters = IntegrationParameters::default();
let mut broad_phase = BroadPhase::new();
let mut narrow_phase = NarrowPhase::new();
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let mut joints = JointSet::new();
// We ignore contact events for now.
let event_handler = ();
// Run the simulation in the game loop.
loop {
pipeline.step(
&gravity,
&integration_parameters,
&mut broad_phase,
&mut narrow_phase,
&mut bodies,
&mut colliders,
&mut joints,
&event_handler
);
}
}

The PhysicsPipeline is responsible for executing the whole simulation: collision detection as well as the simulation of physical phenomena: gravity, contact forces, joints, etc. All the state contained by the PhysicsPipeline are workspace buffers so you are free to discard it at any time without breaking the simulation. For example, it is perfectly fine to re-instantiate a new PhysicsPipeline at each game loop iteration. However this will be less efficient due to repeated allocations of the workspace buffers. All the actually useful simulation-related data are stored in the arguments of PhysicsPipeline::step:

  • The gravity is a global force affecting all dynamic rigid-bodies with non-zero mass.
  • The IntegrationParameters contains elements to configure the constraint solver.
  • The BroadPhase performs coarse AABB-based collision detections.
  • The NarrowPhase performs precise contact point determination based on the collision pairs generated by the broad-phase.
  • The RigidBodySet contains all the rigid-bodies part of the simulation.
  • The ColliderSet contains all the colliders part of the simulation.
  • The JointSet contains all the joint constraints part of the simulation.
  • The event_handler will collect all the events generated by the physics pipeline. Setting this to () simply ignores these events.

All the mentioned sets associate a unique handle to each element they contain. These handles are generational indices so they don't suffer from the ABA problem.

Each call to pipeline.step(...) will advance the simulation by a time equal to 1/601/60 seconds, which is a good default if your application has a refresh rate of 60Hz. The length of this timestep can be retrieved by integration_parameters.dt() and modified using integration_parameters.set_dt(value). Note that if you use the recommanded International System of Units (SI units), this timestep is to be given in seconds. Keep in mind that the timestep length strongly affects the accuracy of the simulation: the smaller the timestep, the more accurate the simulation will be.

Changes of timesteps during the simulation should be avoided as much as possible since they may introduce numerical instabilities or reduce the constraints solvers convergence.

warning

Note that we are free to replace the pipeline by a fresh instance of PhysicsPipeline whenever we want. This is not the case for the other mutable components. Once at least one pipeline.step has been called with these components, they will be inter-dependent. Replacing any of them (in subsequent calls to .step) with new/other instances may cause unexpected behaviors. We are working to ensure this is no longer an issue in the future.

Rigid-bodies

A rigid-body is the simplest type of body supported by rapier. It can be seen as the aggregation of a position, orientation, and mass properties (rotational inertia tensor, mass, and center of mass). It does not hold any information regarding its shape which can optionally be specified by attaching one or multiple colliders to it. A rigid-body with no collider, but with a non-zero mass, will be affected by all the forces the world is aware of, including joints attached to it, but not by contacts (because it does not have any shape that can be collided to).

Creating a rigid-body

A rigid-body can only be created by a RigidBodyBuilder structure that is based on the builder pattern:

info

The following example shows all the possible setters that can be called to customize the rigid-body being built. The input values are just random so using this example as-is will not lead to a useful result.

use rapier2d::na::{Vector2, Isometry2};
use rapier2d::dynamics::{BodyStatus, RigidBodyBuilder};
// Builder for a static rigid-body.
let _ = RigidBodyBuilder::new_static();
// Builder for a dynamic rigid-body.
let _ = RigidBodyBuilder::new_dynamic();
// Builder for a kinematic rigid-body.
let _ = RigidBodyBuilder::new_kinematic();
// Builder for a body with a status specified by an enum.
let rigid_body = RigidBodyBuilder::new(BodyStatus::Dynamic)
// The rigid body translation.
// Default: zero vector.
.translation(0.0, 5.0)
// The rigid body rotation.
// Default: no rotation.
.rotation(5.0)
// The rigid body position. Will override `.translation(...)` and `.rotation(...)`.
// Default: the identity isometry.
.position(Isometry2::new(Vector2::new(1.0, 2.0), f32::PI))
// The linear velocity of this body.
// Default: zero velocity.
.linvel(1.0, 2.0)
// The angular velocity of this body.
// Default: zero velocity.
.angvel(2.0)
// Whether or not this body can sleep.
// Default: true
.can_sleep(true)
// All done, actually build the rigid-body.
.build();

All the properties are optional. The only calls that are required are RigidBodyBuilder::new(status), RigidBodyBuilder::new_static(), RigidBodyBuilder::new_dynamic(), or RigidBodyBuilder::new_kinematic(), to initialize the builder, and .build() to actually build the rigid body.

Typically, the inertia and center of mass are automatically set to the inertia and center of mass resulting from the shapes of the colliders attached to the rigid-body.

The rigid-body handle

Once the rigid body is created it must be added to a RigidBodySet. A body set is a container that maps a unique handle to a body:

let mut body_set = RigidBodySet::new();
let handle = body_set.insert(rigid_body);

Upon insertion, the body set will return a handle to the rigid body. This will allow us to get a reference to this body later if needed:

let rigid_body = body_set.get(handle); // Retrieve an immutable reference.
let rigid_body = body_set.get_mut(handle); // Retrieve a mutable reference.
note

No two bodies in the same body set will share the same handle. This handle is what you should store for future addressing of this rigid body as it is required by various operations including: attaching colliders or joints to the rigid-body.

Body statuses

Any body can have one of three different statuses identified by the dynamics::BodyStatus enumeration:

  • BodyStatus::Dynamic: It indicates the body is affected by external forces, inertial forces (gyroscopic, coriolis, etc.) and contacts.
  • BodyStatus::Static: Indicates the body cannot move. It acts as if it has an infinite mass and will not be affected by any force. It will continue to collide with dynamic bodies but not with static nor with kinematic bodies. This is typically used for the ground or for temporarily freezing a body.
  • BodyStatus::Kinematic: Indicates the body position must not be altered by the physics engine. The user is free to set any position and the body velocity will be deduced at each update accordingly to ensure a realistic behavior of dynamic bodies in contact with it. This is typically used for moving platforms, elevators, etc.

Colliders

Colliders represent the geometric shapes that generate contacts and proximity events when they touch.

Creating a collider

A collider is built by a ColliderBuilder structure following the builder pattern:

info

The following example shows all the possible setters that can be called to customize the collider being built. The input values are just random so using this example as-is will not lead to a useful result.

use na::{Vector2, Isometry2};
use rapier2d::geometry::{ColliderBuilder, Shape, Ball};
// Builder for a ball-shaped collider.
let _ = ColliderBuilder::ball(0.5);
// Builder for a cuboid-shaped collider.
let _ = ColliderBuilder::cuboid(0.5, 0.2);
// Builder for a capsule-shaped collider. The capsule principal axis is the `x` coordinate axis.
let _ = ColliderBuilder::capsule_x(0.5, 0.2);
// Builder for a capsule-shaped collider. The capsule principal axis is the `y` coordinate axis.
let _ = ColliderBuilder::capsule_y(0.5, 0.2);
// Builder for a triangle-mesh-shaped collider.
let _ = ColliderBuilder::trimesh(vertices, indices);
// Builder for a heightfield-shaped collider.
let _ = ColliderBuilder::heightfield(heights, scale);
// Builder for a collider with the given shape.
let collider = ColliderBuilder::new(Shape::Ball(Ball::new(0.5))
// The collider translation wrt. the body it is attached to.
// Default: the zero vector.
.translation(1.0, 2.0)
// The collider rotation wrt. the body it is attached to.
// Default: the identity rotation.
.rotation(f32::PI)
// The collider position wrt. the body it is attached to.
// Default: the identity isometry.
.position(Isometry2::new(Vector2::new(1.0, 2.0), PI))
// The collider density. If non-zero the collider's mass and angular inertia will be added
// to the inertial properties of the body it is attached to.
// Default: 1.0
.density(1.3)
// The friction coefficient of this collider.
// Default: ColliderBuilder::default_friction() == 0.5
.friction(0.8)
// Whether this collider is a sensor, i.e., generate only proximity events.
// Default: false
.sensor(true)
// All done, actually build the collider.
.build();

The collider handle

Once the collider is created it must be added to a collider set. A collider set is a container that maps a unique handle to a collider:

let mut collider_set = ColliderSet::new();
let handle = collider_set.insert(collider, parent_handle, &mut rigid_body_set);

When inserting the collider, it will be attached to the rigid-body specified by is handle parent_handle. Upon insertion, the collider set will return a handle to the collider. This will allow you to get a reference to this collider if needed later:

let collider = body_set.get(handle); // Retrieve an immutable reference.
let collider = body_set.get_mut(handle); // Retrieve a mutable reference.
note

No two colliders in the same collider set will share the same handle. This handle is what you should store for future addressing of this collider.