Colliders
Colliders represent the geometric shapes that generate contacts and collision events when they touch. Attaching one or multiple colliders to a rigid body allow the rigid-body to be affected by contact forces.
Creation and insertion
A collider is described by a R3ColliderDesc structure, initialized by one of its constructors (e.g.
r3BallColliderDesc, r3CuboidColliderDesc, r3CapsuleYColliderDesc, or r3DefaultColliderDesc) which set
meaningful default values to all its fields. Its geometric shape is given by its shape field, a R3ShapeDesc
whose kind (e.g. R3_SHAPE_DESC_CUBOID) selects which of its fields are actually read. Then it needs to be
inserted into the physics world: with r3InsertCollider to attach it
to a rigid-body, or with r3InsertColliderWithoutParent otherwise. Both return the R3ColliderHandle identifying
the new collider.
The arrays referenced by a description (vertex buffers, index buffers, compound children, etc.) are only borrowed
until its insertion returns: they can be freed or reused right after. This is also the case of the shared
shapes (R3SharedShape) a description can point to, with the R3_SHAPE_DESC_SHARED kind. A shared shape is an
immutable geometry created by one of the r3...SharedShape functions (e.g. r3BallSharedShape), which can be given
to any number of colliders, and must be freed with r3FreeSharedShape. Some shapes, like convex decompositions or
voxels, can only be created as shared shapes.
The following example shows several fields that can be set to customize the collider being described. The input values are just random so using this example as-is will not lead to a useful result.
- Example 2D
- Example 3D
// The world that will contain our colliders.
R2World *world = r2NewWorld();
// Description of a ball-shaped collider.
R2ColliderDesc ball = r2BallColliderDesc(0.5);
// Description of a cuboid-shaped collider.
R2ColliderDesc cuboid = r2CuboidColliderDesc(r2Vector(0.5, 0.2));
// Description of a capsule-shaped collider. The capsule principal axis is the `x` coordinate axis.
R2ColliderDesc capsule_x = r2CapsuleXColliderDesc(0.5, 0.2);
// Description of a capsule-shaped collider. The capsule principal axis is the `y` coordinate axis.
R2ColliderDesc capsule_y = r2CapsuleYColliderDesc(0.5, 0.2);
// Description of a triangle-mesh-shaped collider.
R2ColliderDesc trimesh = r2DefaultColliderDesc();
r2ShapeDesc_SetTrimesh(&trimesh.shape, (R2VectorView){vertices, 3}, (R2TriangleView){indices, 1}, 0);
// Description of a heightfield-shaped collider.
R2ColliderDesc heightfield = r2DefaultColliderDesc();
heightfield.shape.kind = R2_SHAPE_DESC_HEIGHTFIELD;
heightfield.shape.heights = (R2RealView){heights, 4};
heightfield.shape.rows = 4;
heightfield.shape.columns = 1;
heightfield.shape.scale = scale;
// Description of a collider with the given shared shape.
R2SharedShape *shape = r2BallSharedShape(0.5);
R2ColliderDesc collider = r2DefaultColliderDesc();
collider.shape.kind = R2_SHAPE_DESC_SHARED;
collider.shape.sharedShape = shape;
// The collider translation wrt. the body it is attached to.
// Default: the zero vector.
collider.position.translation = r2Vector(1.0, 2.0);
// The collider rotation wrt. the body it is attached to.
// Default: the identity rotation.
collider.position.rotation = r2Rotation(R2_PI);
// The collider position wrt. the body it is attached to.
// Default: the identity pose.
collider.position = r2Pose(r2Vector(1.0, 2.0), r2Rotation(R2_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
collider.density = 1.3;
// The friction coefficient of this collider.
// Default: 0.5
collider.friction = 0.8;
// Whether this collider is a sensor.
// Default: 0
collider.isSensor = 1;
// Insert the collider into the world, without attaching it to a rigid-body.
R2ColliderHandle collider_handle = r2InsertColliderWithoutParent(world, &collider);
R2RigidBodyDesc rigid_body = r2DynamicRigidBodyDesc();
R2RigidBodyHandle rigid_body_handle = r2InsertRigidBody(world, &rigid_body);
// Or insert the collider into the world and attach it to a rigid-body.
R2ColliderHandle handle = r2InsertCollider(rigid_body_handle, &collider);
// The descriptions only borrow the shared shape: free it once it is no longer needed.
r2FreeSharedShape(shape);
// The world that will contain our colliders.
R3World *world = r3NewWorld();
// Description of a ball-shaped collider.
R3ColliderDesc ball = r3BallColliderDesc(0.5);
// Description of a cuboid-shaped collider.
R3ColliderDesc cuboid = r3CuboidColliderDesc(r3Vector(0.5, 0.2, 0.1));
// Description of a capsule-shaped collider. The capsule principal axis is the `x` coordinate axis.
R3ColliderDesc capsule_x = r3CapsuleXColliderDesc(0.5, 0.2);
// Description of a capsule-shaped collider. The capsule principal axis is the `y` coordinate axis.
R3ColliderDesc capsule_y = r3CapsuleYColliderDesc(0.5, 0.2);
// Description of a capsule-shaped collider. The capsule principal axis is the `z` coordinate axis.
R3ColliderDesc capsule_z = r3CapsuleZColliderDesc(0.5, 0.2);
// Description of a triangle-mesh-shaped collider.
R3ColliderDesc trimesh = r3DefaultColliderDesc();
r3ShapeDesc_SetTrimesh(&trimesh.shape, (R3VectorView){vertices, 3}, (R3TriangleView){indices, 1}, 0);
// Description of a heightfield-shaped collider (heights in column-major order).
R3ColliderDesc heightfield = r3DefaultColliderDesc();
heightfield.shape.kind = R3_SHAPE_DESC_HEIGHTFIELD;
heightfield.shape.heights = (R3RealView){heights, 4};
heightfield.shape.rows = 2;
heightfield.shape.columns = 2;
heightfield.shape.scale = scale;
// Description of a collider with the given shared shape.
R3SharedShape *shape = r3BallSharedShape(0.5);
R3ColliderDesc collider = r3DefaultColliderDesc();
collider.shape.kind = R3_SHAPE_DESC_SHARED;
collider.shape.sharedShape = shape;
// The collider translation wrt. the body it is attached to.
// Default: the zero vector.
collider.position.translation = r3Vector(1.0, 2.0, 3.0);
// The collider rotation wrt. the body it is attached to.
// Default: the identity rotation.
collider.position.rotation = r3RotationFromAxisAngle(r3Vector(0.0, 1.0, 0.0), R3_PI);
// The collider position wrt. the body it is attached to.
// Default: the identity pose.
collider.position = r3Pose(r3Vector(1.0, 2.0, 3.0), r3RotationFromAxisAngle(r3Vector(0.0, 1.0, 0.0), R3_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
collider.density = 1.3;
// The friction coefficient of this collider.
// Default: 0.5
collider.friction = 0.8;
// Whether this collider is a sensor.
// Default: 0
collider.isSensor = 1;
// Insert the collider into the world, without attaching it to a rigid-body.
R3ColliderHandle collider_handle = r3InsertColliderWithoutParent(world, &collider);
R3RigidBodyDesc rigid_body = r3DynamicRigidBodyDesc();
R3RigidBodyHandle rigid_body_handle = r3InsertRigidBody(world, &rigid_body);
// Or insert the collider into the world and attach it to a rigid-body.
R3ColliderHandle handle = r3InsertCollider(rigid_body_handle, &collider);
// The descriptions only borrow the shared shape: free it once it is no longer needed.
r3FreeSharedShape(shape);
A collider can also be disabled, by setting the enabled field of its description to 0 or, after its creation, with
r3Collider_SetEnabled. A disabled collider is excluded from all the collision-detection and physics until it is
enabled again, which is useful to "turn off" a collider temporarily without removing it (a collider is removed from
the world with r3RemoveCollider).
Collider type
There are two types of colliders:
- A solid collider represents a geometric shape that can have contact points with other colliders to generate contact forces to prevent objects from penetrating-each-others.
- Sensor colliders on the other end don't generate contacts: they only generate intersection events when one sensor collider and another collider start/stop touching. Sensor colliders are generally used to detect when something enters an area. Note that, for symmetry with non-sensor colliders, sensors do contribute to the mass of a rigid-body they are attached to.
By default a collider is a solid collider. This can be changed to a sensor when constructing the collider, or after its construction:
/* Set the collider type when the collider is created. */
R2ColliderDesc collider = r2BallColliderDesc(0.5);
collider.isSensor = 1;
/* Set the collider type after the collider creation. */
r2Collider_SetSensor(collider_handle, 1);
assert(r2Collider_IsSensor(collider_handle));
Shapes
Overview
The main characteristic of a collider is its geometric shape. The supported shapes are illustrated below:
Shapes only hold information about their geometry. Their world-space position is given by the collider's position. Balls, cuboids, capsules, cylinders, and cones are all described by their half-height and/or radius. Compound shapes, convex meshes, triangle meshes, heightfields, and polylines are more complicated shapes described in the next paragraphs.
Convex meshes
A convex mesh is a shape such that, if two points are part of the shape, then the segment between these two points is also part of the shape:
There are two ways of creating a collider with a convex shape:
-
Using
r3ShapeDesc_SetConvexHull(&desc.shape, points)(orr3ConvexHullSharedShape(points)). This is the simplest approach: it will automatically compute the convex hull of the given set of points. A convex hull is the smallest convex shape that contains all the given points. -
Using
r3ConvexMeshSharedShape(points, indices)in 3D orr2ConvexPolylineSharedShape(points)in 2D. This takes a mesh described by its vertex buffer and index buffer and assumes it is already convex (you need to ensure that it is convex yourself). This will be more efficient than ther3ShapeDesc_SetConvexHullconstructor because it won't perform any calculations to ensure convexity. However, if the input mesh isn't actually convex, the collision-detection for that shape will give an incorrect result.
Triangle meshes and polylines
Triangle meshes (in 3D) and polylines (in 2D) can be used to describe the boundary of any kind of shape. This is generally useful to describe the fixed environment in games (terrains, buildings, etc.) Triangle meshes and polylines are defined by their vertex buffer and their index buffer. The winding of the triangles of a triangle mesh does not matter. Its topology doesn't matter either (it can have holes, cavities, doesn't need to be closed or manifold). It is however strongly recommended to avoid triangles that are long and thin because they can result in a lower numerical stability of collision-detection.
A triangle mesh/polyline is composed of triangles/segments with no thickness. This means that geometric queries like point-containment tests won't work intuitively because the triangle mesh is assumed to have no interior.
Triangle mesh
A triangle-mesh collider can be built
with r3ShapeDesc_SetTrimesh(&desc.shape, vertices, indices, flags) (or r3TrimeshSharedShape(vertices, indices)) where
vertices is the buffer containing all the vertices of the mesh, and indices is a set of indices indicating what vertex is used by
what triangle. The vertex buffer and index buffer may have different lengths, and any vertex can be shared
by multiple triangles.
To have more control over the resulting Trimesh,
you can give a combination of the R3_TRIMESH_* flags to the last argument of r3ShapeDesc_SetTrimesh (or call r3TrimeshSharedShapeWithFlags(vertices, indices, flags)).
For example, R3_TRIMESH_FIX_INTERNAL_EDGES is a popular choice to help with correcting ghost collision.
See the documentation of the R3_TRIMESH_* constants in rapier.h for more information.
Polyline
A polyline collider can be built
with r3ShapeDesc_SetPolyline(&desc.shape, vertices, indices, flags) (or r3PolylineSharedShape(vertices, indices)) where
vertices is the buffer containing all the vertices of the polyline, and indices is an optional set of indices
indicating what vertex is used by what segment. The vertex buffer and index buffer may have different lengths, and any
vertex can be shared by multiple segments. If the given index buffer is empty then the input vertices are assumed to
form a line strip, i.e., the polyline is formed from the segments [vertices[0], vertices[1]], [vertices[1], vertices[2]], etc.
A triangle-mesh and a polyline are two-sided by default: they generate contacts on both of their sides, which lets a
body crushed against a thin wall squeeze through it when the contact normal flips. This is why they can also be built
as oriented (one-sided) shapes, with the R2_POLYLINE_ORIENTED flag of r2ShapeDesc_SetPolyline (or with
r2OrientedPolylineSharedShape) in 2D, and with the R3_TRIMESH_ORIENTED flag of r3ShapeDesc_SetTrimesh (or of
r3TrimeshSharedShapeWithFlags) in 3D. An oriented shape only collides on its outward side, which is given by the
winding of its vertices, and is therefore the right choice for the walls of a container.
It is discouraged to use a triangle meshes or a polylines for colliders attached to dynamic rigid-bodies. Because they have no interior, it is easy for another object to get stuck into them. In order to simulate properly non-convex objects, it is recommended to use a convex decomposition with a compound shape instead.
Heightfields
Heightfields are a more restrictive version of triangle-meshes and polylines. However, they can be easier to define and use much less memory. Therefore heightfields are useful to define large parts of terrains with simple topologies.
A 3D heightfield is basically large rectangle in the X-Z plane, subdivided in a grid pattern at regular intervals. Each
vertex of this subdivision is given a height, i.e., the coordinate of that point along the Y axis. A 3D heightfield
collider can be created
with r3HeightfieldSharedShape(heights, rows, columns, scale) (or the R3_SHAPE_DESC_HEIGHTFIELD shape kind) where
heights is a matrix indicating the altitude of each subdivision point of that heightfield (given as a flat array of rows * columns heights in column-major order). The number of rows of that
matrix is the number of subdivision along the Z axis, and the number of columns is the number of subdivision along the
X axis. The scale argument indicates the size of the rectangle of the X-Z plane.
A heightfield collider can be given any orientation by changing the orientation of the collider itself.
A 2D heightfield is a large segment along the X axis, subdivided at regular intervals. Each vertex of this
subdivision is given a height, i.e., the coordinate of that point along the Y axis. A 2D heightfield collider
can be created
with r2HeightfieldSharedShape(heights, rows, 1, scale) (or the R2_SHAPE_DESC_HEIGHTFIELD shape kind with a single column) where
heights is a vector indicating the altitude of each subdivision point of that heightfield. The number of elements on
that vector is the number of subdivision of the heightfield. The scale argument indicates the length of the subdivided
segment along the X axis.
Voxels
Voxel shapes are useful to represent 3D volumes made of small uniform cubes (voxels), such as Minecraft-like worlds or volumetric data. Unlike triangle meshes, voxel-based shapes can offer improved collision detection robustness and performance due to their regular structure.
A voxel shape is a shared shape constructed from a grid of occupied cells, e.g., with
r3VoxelsSharedShapeFromPoints which fills the cells containing the given points:
- Example 2D
- Example 3D
// A voxels shape from arbitrary points.
R2Vector points[] = {{0.0, 0.0}, {1.0, 1.0}, {-1.0, 1.0}};
R2SharedShape *shape = r2VoxelsSharedShapeFromPoints(r2Vector(1.0, 1.0), (R2VectorView){points, 3});
R2ColliderDesc collider = r2DefaultColliderDesc();
collider.shape.kind = R2_SHAPE_DESC_SHARED;
collider.shape.sharedShape = shape;
// A voxels shape from arbitrary points.
R3Vector points[] = {{0.0, 0.0, 0.0}, {1.0, 1.0, 1.0}};
R3SharedShape *shape = r3VoxelsSharedShapeFromPoints(r3Vector(1.0, 1.0, 1.0), (R3VectorView){points, 2});
R3ColliderDesc collider = r3DefaultColliderDesc();
collider.shape.kind = R3_SHAPE_DESC_SHARED;
collider.shape.sharedShape = shape;
You can also voxelize a mesh (a polyline in 2D, or a triangle mesh in 3D) with r3VoxelizedMeshSharedShape:
R2SharedShape *shape =
r2VoxelizedMeshSharedShape((R2VectorView){mesh, 2}, (R2SurfaceElementView){indices, 2}, 0.2);
The voxels of a collider with a voxel shape can then be inspected with r3Collider_IsVoxels and
r3Collider_VoxelAtFlatId, and filled or cleared individually with r3Collider_SetVoxel.
Compound shapes
It is not recommended to use a triangle mesh or polyline for the shape of a collider attached to a dynamic rigid-body. The alternative is to use a compound shape to model a non-convex object as the union of multiple convex parts (which can be cuboids, balls, convex meshes, etc.) This is commonly known as a convex decomposition.
An alternative to using a compound shape is to attach multiple colliders to the same rigid-body: all the colliders will move with the rigid-body automatically, and the simulation quality (contact resolution, stability) is identical with both approaches. They differ in other ways, so pick based on how you use the object:
- Performance: a compound shape is a single collider, so the broad-phase handles one entry (with its own internal acceleration structure for the parts) instead of one entry per collider. With many parts (hundreds or more), a compound shape makes the physics step significantly cheaper, especially while the rigid-body is awake.
- Collision events: each collider generates its own individual collision start/stop events and can have its own friction, restitution, collision groups, or sensor status. A compound shape is a single collider: one set of events and properties for the whole shape.
- Mutability: adding or removing one collider from a rigid-body is easy and cheap, whereas adding or removing a part of a compound shape requires rebuilding the whole compound shape.
To build a compound shape, it is possible to directly provide the set of shapes as well as their position in the
compound shape's local space, as an array of R3CompoundShapeDesc given to a shape description with the
R3_SHAPE_DESC_COMPOUND kind (or to r3CompoundSharedShape):
R2CompoundShapeDesc parts[] = {{pos1, shape}, {pos2, shape}};
R2ColliderDesc collider = r2DefaultColliderDesc();
collider.shape.kind = R2_SHAPE_DESC_COMPOUND;
collider.shape.children = (R2CompoundShapeView){parts, 2};
It is also possible to build a compound shape modelling the convex decomposition of a 3D triangle mesh or 2D polyline
using the r3ConvexDecompositionSharedShape(vertices, indices) function. This will automatically create a compound
shape composed of multiple convex meshes obtained from the approximate convex decomposition of the triangle mesh (or
polyline in 2D) using the VHACD algorithm. Here are examples of a 2D concave
polygon decomposed into two convex parts as well as a 3D mesh with its approximate convex decomposition composed of 7
convex parts:
