soft_body_deformable_colliders
Games don't need the simulated shape of a body to be as detailed as its visual shape: a coarse and well-shaped lattice is faster and more stable to simulate than one cell per visual triangle. This is why Rapier supports cage simulation and skinning. The detailed mesh is embedded in a coarse volumetric lattice, aka. its cage, which is the only part being simulated. The vertices of the mesh are then interpolated from the deformed cells holding them, aka. skinning:
Skinned soft-bodies
The
SoftBody::volumetric_skinned
constructor computes the cage of a closed mesh automatically, and keeps the
mesh as the skin of the body. That automatic cage is built for performance rather than geometric fidelity: in 3D, it
encloses the whole mesh with the tetrahedra of a lattice, without snapping them to the mesh:

By default, the body still collides through the boundary of its cage, which is as coarse as its cells. Its skin can
become its actual collision mesh instead with
skin_collision.
The SoftBodyMeshSync component renders the skin of a 3D body, as in this example (the meshes of the deformable
colliders bound to a body are never rendered by this component). In 2D, it renders the cells of the cage instead, and
the vertices of the skin are read from the collision mesh of the Rapier soft-body (RapierSoftBody::collision_mesh):
- Example 2D
- Example 3D
// A detailed outline held by a coarse cage of cells: only the cells are simulated, and the
// outline (the skin) follows their deformation.
let num = 48;
let vertices: Vec<Vec2> = (0..num)
.map(|i| {
let angle = i as f32 / num as f32 * std::f32::consts::TAU;
Vec2::new(angle.cos(), angle.sin()) * 0.5
})
.collect();
let indices: Vec<[u32; 2]> = (0..num as u32).map(|i| [i, (i + 1) % num as u32]).collect();
let skinned = SoftBody::volumetric_skinned(&vertices, &indices, 0.25)
.expect("the polyline must be closed and enclose some area")
// Collide through the skin instead of the boundary of the cage.
.map(|builder| builder.skin_collision(true));
commands.spawn((
Transform::from_xyz(0.0, 4.0, 0.0),
skinned,
// The synchronized mesh renders the cells of the cage.
SoftBodyMeshSync::default(),
MeshMaterial2d(materials.add(Color::srgb(0.2, 0.6, 0.3))),
));
// A detailed mesh held by a coarse cage of cells: only the cells are simulated, and the mesh
// (the skin) follows their deformation.
let (vertices, indices) = Ball::new(0.5).to_trimesh(24, 24);
let skinned = SoftBody::volumetric_skinned(&vertices, &indices, 0.25)
.expect("the mesh must be closed and enclose some volume")
// Collide through the skin instead of the boundary of the cage.
.map(|builder| builder.skin_collision(true));
commands.spawn((
Transform::from_xyz(0.0, 4.0, 3.0),
skinned,
// The synchronized mesh renders the skin, since it is the collision mesh of the body.
SoftBodyMeshSync::default(),
MeshMaterial3d(materials.add(Color::srgb(0.2, 0.6, 0.3))),
));
A skin doesn't need a computed cage: any mesh can be given as the skin of a body built with cells, with
SoftBodyBuilder::skin. Each of its
vertices is bound to the cell closest to it, in the pose the cells are built in.
Deformable colliders
The colliders built from the surface or the skin of a soft-body are generated by the engine itself, but it is also
possible to give a body a collider of your own which vertices follow its particles: a deformable collider
(a DeformableCollider component next to the Collider of an
entity). This is a polyline in
2D, or a triangle mesh in 3D, flagged as deformable, and attached to the proxy of one of the
clusters of the body (the root body, a cluster itself, can be used too). Its vertices are
given in world-space, i.e., they are placed by the GlobalTransform of the collider entity when the
collider is created, and they can be read back at any time in order to render the mesh where the simulation moved
it. A deformable collider can be a sensor as well, e.g., to detect what enters a deformable volume.
How the vertices follow the particles is given by the binding
(SoftMeshBinding):
skinned: each vertex is embedded in the cell of the cluster holding it, i.e., the collider is a skin of the cage.direct: the vertexifollows the particle given for it, which must belong to the cluster. Its alternative that binds every vertex to the closest particle within a given distance (direct_by_position) is useful when the mesh is the one the particles were built from.
The DeformableCollider component targets the soft-body entity (for its root body) or a
cluster entity. The Collider of its entity must be a polyline (2D) or a triangle mesh
(3D) flagged with PolylineFlags::DEFORMABLE or TriMeshFlags::DEFORMABLE. Once created, the collider follows the
particles: the Transform and the shape of its entity are ignored, whereas its other collider components (friction,
collision groups, events, etc.) apply as usual. If the binding fails, an error is logged and a
DeformableColliderError component is inserted on the entity:
- Example 2D
- Example 3D
// A deformable polyline bound to the blob: each vertex follows one particle (`direct`),
// or is embedded in the cell holding it (`skinned`). The vertices are placed by the
// transform of the collider entity when the collider is created.
let vertices = blob_body.builder.particle_positions().to_vec();
let num = vertices.len() as u32;
let indices: Vec<[u32; 2]> = (0..num).map(|i| [i, (i + 1) % num]).collect();
commands.spawn((
blob_transform,
Collider::polyline_with_flags(vertices, Some(indices), PolylineFlags::DEFORMABLE),
Sensor,
DeformableCollider::new(blob, SoftMeshBinding::direct((0..num).collect())),
));
// A deformable triangle mesh bound to the jelly: each vertex is embedded in the cell
// holding it (`skinned`), or follows one particle (`direct`). The vertices are placed by the
// transform of the collider entity when the collider is created.
let (vertices, indices) = Ball::new(1.0).to_trimesh(10, 10);
commands.spawn((
Transform::from_xyz(3.0, 1.0, 0.0),
Collider::trimesh_with_flags(vertices, indices, TriMeshFlags::DEFORMABLE)
.expect("a valid triangle mesh"),
Sensor,
DeformableCollider::new(jelly, SoftMeshBinding::skinned()),
));
The current vertices of a deformable collider are read from the Rapier soft-body it follows, which is also given by
the deformable_mesh_ref of its Rapier collider:
- Example 2D
- Example 3D
fn read_deformable_colliders(
context: ReadRapierContext,
colliders: Query<&RapierColliderHandle, With<DeformableCollider>>,
) -> Result {
let context = context.single()?;
for handle in &colliders {
// The soft-body a collider follows.
let collider = &context.colliders.colliders[handle.0];
let Some(mesh_ref) = collider.deformable_mesh_ref() else {
continue;
};
let soft_body = &context.rigidbody_set.soft_bodies[mesh_ref.body];
// The mesh follows the particles: read its current vertices back (in world-space).
let mesh = soft_body.mesh_of(handle.0).unwrap();
let vertices: Vec<Vec2> = mesh.vertex_positions(soft_body).collect();
assert!(!vertices.is_empty());
}
Ok(())
}
fn read_deformable_colliders(
context: ReadRapierContext,
colliders: Query<&RapierColliderHandle, With<DeformableCollider>>,
) -> Result {
let context = context.single()?;
for handle in &colliders {
// The soft-body a collider follows.
let collider = &context.colliders.colliders[handle.0];
let Some(mesh_ref) = collider.deformable_mesh_ref() else {
continue;
};
let soft_body = &context.rigidbody_set.soft_bodies[mesh_ref.body];
// The mesh follows the particles: read its current vertices back (in world-space).
let mesh = soft_body.mesh_of(handle.0).unwrap();
let vertices: Vec<Vec3> = mesh.vertex_positions(soft_body).collect();
assert!(!vertices.is_empty());
}
Ok(())
}
A deformable collider has no mass: its density is ignored, and it is the particles which hold the mass of the soft-body. Note that a collider given no contact skin explicitly gets the particle radius of the soft-body as its skin, so its thickness matches the thickness of the surface of the body.