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
SoftBodyBuilder::volumetric_skinnedSoftBody::volumetric_skinnedSoftBodyDesc.volumetricSoftBody.volumetrictrueskinned argument is True
The r3VolumetricSoftBodyDesc constructor computes the cage of a closed mesh automatically, and the same mesh becomes
the skin of the body once it is also given to r3SoftBodyDesc_SetSkin. 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_collisionsetSkinCollisionskinCollision field of R3SoftBodyDescSoftBodyBuilder.skin_collision
- 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<Vector> = (0..num)
.map(|i| {
let angle = i as f32 / num as f32 * std::f32::consts::TAU;
Vector::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 = SoftBodyBuilder::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.
.skin_collision(true)
.translated(Vector::new(0.0, 4.0));
let skinned_handle = world.insert_soft_body(skinned);
// The skin is the body's collision mesh: read its vertices back to render it.
let body = &world.soft_bodies[skinned_handle];
let skin = body.collision_mesh().expect("the skin collides");
let skin_vertices: Vec<Vector> = skin.vertex_positions(body).collect();
assert_eq!(skin_vertices.len(), vertices.len());
// 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 = SoftBodyBuilder::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.
.skin_collision(true)
.translated(Vector::new(0.0, 4.0, 3.0));
let skinned_handle = world.insert_soft_body(skinned);
// The skin is the body's collision mesh: read its vertices back to render it.
let body = &world.soft_bodies[skinned_handle];
let skin = body.collision_mesh().expect("the skin collides");
let skin_vertices: Vec<Vector> = skin.vertex_positions(body).collect();
assert_eq!(skin_vertices.len(), vertices.len());
- Example 2D
- Example 3D
// A detailed outline held by a coarse cage of cells (the last `true` argument): only the
// cells are simulated, and the outline (the skin) follows their deformation.
let numSegments = 48;
let circleVertices = new Float32Array(numSegments * 2);
let circleIndices = new Uint32Array(numSegments * 2);
for (let i = 0; i < numSegments; ++i) {
let angle = (i / numSegments) * 2.0 * Math.PI;
circleVertices.set([Math.cos(angle) * 0.5, Math.sin(angle) * 0.5], i * 2);
circleIndices.set([i, (i + 1) % numSegments], i * 2);
}
let skinnedDesc = RAPIER.SoftBodyDesc.volumetric(circleVertices, circleIndices, 0.25, true)
// Collide through the skin instead of the boundary of the cage.
.setSkinCollision(true)
.setTranslation({ x: 0.0, y: 4.0 });
let skinned = world.createSoftBody(skinnedDesc);
// The skin is the body's collision mesh: read its vertices back to render it.
let skinPositions: Float32Array = skinned.meshVertices(0);
console.log("The skin has", skinPositions.length / 2, "vertices");
// A mesh held by a cage of cells (the last `true` argument): only the cells are simulated,
// and the mesh (the skin) follows their deformation.
let skinnedDesc = RAPIER.SoftBodyDesc.volumetric(boxVertices, boxIndices, 0.25, true)
// Collide through the skin instead of the boundary of the cage.
.setSkinCollision(true)
.setTranslation({ x: 0.0, y: 4.0, z: 3.0 });
let skinned = world.createSoftBody(skinnedDesc);
// The skin is the body's collision mesh: read its vertices back to render it.
let skinPositions: Float32Array = skinned.meshVertices(0);
console.log("The skin has", skinPositions.length / 3, "vertices");
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))),
));
Note that the mesh arrays are only borrowed by the description until its insertion. The vertices of the skin, as well as
the ones of any other mesh of the body, are read back with r3SoftBody_MeshVertices, given the collider of the mesh
(r3SoftBody_MeshColliders gives the colliders of the meshes of a body). A skin that doesn't collide has no collider:
r3SoftBody_Meshes lists every mesh of the body with its identifier (an R3SoftMeshInfo which is_skinned and
collision_enabled fields tell which mesh is which), and r3SoftBody_MeshVerticesById reads the vertices of a mesh
from that identifier. Their triangles (segments in 2D) are read the same way, with r3SoftBody_MeshIndices and
r3SoftBody_MeshIndicesById:
- 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.
R2Vector vertices[48];
R2Edge indices[48];
for (uint32_t i = 0; i < 48; i++) {
R2Real angle = (R2Real)i / 48 * 2.0 * R2_PI;
vertices[i] = r2Vector(0.5 * cos(angle), 0.5 * sin(angle));
indices[i] = (R2Edge){i, (i + 1) % 48};
}
R2VectorView outline_vertices = {vertices, 48};
R2SurfaceElementView outline_segments = {indices, 48};
// The cage: the outline filled with cells of about 0.25 in size.
R2SoftBodyDesc skinned =
r2VolumetricSoftBodyDesc(outline_vertices, outline_segments, r2NewVolumeMeshParameters(0.25));
// The skin: the outline itself, following the cells holding its vertices.
r2SoftBodyDesc_SetSkin(&skinned, outline_vertices, outline_segments);
// Collide through the skin instead of the boundary of the cage.
skinned.skinCollision = 1;
skinned.translation = r2Vector(0.0, 4.0);
R2SoftBodyHandle skinned_handle = r2InsertSoftBody(world, &skinned);
// The skin is the body's collision mesh: read its vertices back to render it.
R2ColliderHandle skin_collider;
r2SoftBody_MeshColliders(skinned_handle, &skin_collider, 1);
R2Vector skin_vertices[48];
size_t num_skin_vertices = r2SoftBody_MeshVertices(skinned_handle, skin_collider, skin_vertices, 48);
// A detailed mesh held by a coarse cage of cells: only the cells are simulated, and the mesh
// (the skin) follows their deformation.
R3SharedShape *ball_shape = r3BallSharedShape(0.5);
R3TriMeshData *ball_mesh = r3SharedShape_ToTrimesh(ball_shape, 24, 24);
size_t num_vertices = r3TriMeshData_Vertices(ball_mesh, NULL, 0);
size_t num_indices = r3TriMeshData_Indices(ball_mesh, NULL, 0);
R3Vector *vertices = malloc(num_vertices * sizeof(R3Vector));
uint32_t *indices = malloc(num_indices * sizeof(uint32_t));
r3TriMeshData_Vertices(ball_mesh, vertices, num_vertices);
r3TriMeshData_Indices(ball_mesh, indices, num_indices);
r3FreeTriMeshData(ball_mesh);
r3FreeSharedShape(ball_shape);
R3VectorView mesh_vertices = {vertices, num_vertices};
R3SurfaceElementView mesh_triangles = {(const R3Triangle *)indices, num_indices / 3};
// The cage: the mesh filled with cells of about 0.25 in size.
R3SoftBodyDesc skinned =
r3VolumetricSoftBodyDesc(mesh_vertices, mesh_triangles, r3NewVolumeMeshParameters(0.25));
// The skin: the mesh itself, following the cells holding its vertices.
r3SoftBodyDesc_SetSkin(&skinned, mesh_vertices, mesh_triangles);
// Collide through the skin instead of the boundary of the cage.
skinned.skinCollision = 1;
skinned.translation = r3Vector(0.0, 4.0, 3.0);
R3SoftBodyHandle skinned_handle = r3InsertSoftBody(world, &skinned);
// The mesh arrays are only borrowed until the insertion.
free(vertices);
free(indices);
// The skin is the body's collision mesh: read its vertices back to render it.
R3ColliderHandle skin_collider;
r3SoftBody_MeshColliders(skinned_handle, &skin_collider, 1);
size_t num_skin_vertices = r3SoftBody_MeshVertices(skinned_handle, skin_collider, NULL, 0);
R3Vector *skin_vertices = malloc(num_skin_vertices * sizeof(R3Vector));
r3SoftBody_MeshVertices(skinned_handle, skin_collider, skin_vertices, num_skin_vertices);
SoftBody.volumetric raises a MeshConversionError if the mesh can't be filled with cells, e.g., because it isn't
closed. The vertices of the skin, as well as the ones of any other mesh of the body, are read back from a
SoftCollisionMesh: a snapshot of the mesh taken when it is requested, which vertices (in world-space) and indices
are NumPy arrays. SoftBody.collision_mesh gives the collision mesh of the body (its skin here), and
SoftBody.mesh_of gives the mesh of a collider. A skin that doesn't collide has no collider: SoftBody.meshes lists
every mesh of the body (its is_skinned and collision_enabled properties telling which mesh is which), and
SoftBody.mesh gives the mesh with the given identifier (a SoftMeshId):
# A detailed mesh held by a coarse cage of cells: only the cells are simulated, and the mesh
# (the skin) follows their deformation.
vertices, indices = rp.Ball(0.5).to_trimesh(24, 24)
# Raises `MeshConversionError` if the mesh isn't closed or doesn't enclose any volume.
skinned = (
rp.SoftBody.volumetric(vertices, indices, 0.25, skinned=True)
# Collide through the skin instead of the boundary of the cage.
.skin_collision(True)
.translated((0.0, 4.0, 3.0))
)
skinned_handle = world.add_soft_body(skinned)
# The skin is the body's collision mesh: read its vertices back to render it.
skin = world.soft_bodies[skinned_handle].collision_mesh()
assert skin is not None and skin.is_skinned
assert skin.vertices.shape == vertices.shape
A skin doesn't need a computed cage: any mesh can be given as the skin of a body built with cells, with
SoftBodyBuilder::skinSoftBodyDesc.setSkinr3SoftBodyDesc_SetSkinSoftBodyBuilder.skin
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
(ColliderSet::insert_deformableDeformableCollider component next to the Collider of an
entityWorld.createDeformableColliderr3InsertDeformableColliderPhysicsWorld.insert_deformableGlobalTransform of the collider entity when the
collider is created
How the vertices follow the particles is given by the binding
(SoftMeshBindingR3SoftMeshBindingDesc
skinned : each vertex is embedded in the cell of the cluster holding it, i.e., the collider is a skin of the cage.R3_SOFT_BINDING_SKINNEDdirect : the vertexR3_SOFT_BINDING_DIRECTifollows 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_positiondirectByPositionR3_SOFT_BINDING_DIRECT_BY_POSITION ) is useful when the mesh is the one the particles were built from.direct_by_position
- 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 polyline is given in the frame
// of the proxy it is attached to.
let root = world.soft_bodies[blob_handle].root_body();
let root_pose = *world.bodies[root].position();
let blob = &world.soft_bodies[blob_handle];
let num = blob.num_particles();
let vertices: Vec<Vector> = blob
.particle_positions()
.map(|p| root_pose.inverse() * p)
.collect();
let indices: Vec<[u32; 2]> = (0..num as u32).map(|i| [i, (i + 1) % num as u32]).collect();
let particles: Vec<u32> = (0..num as u32).collect();
let outline =
ColliderBuilder::polyline_with_flags(vertices, Some(indices), PolylineFlags::DEFORMABLE)
.sensor(true);
let outline_handle = world
.insert_deformable(outline, SoftMeshBinding::direct(particles), root)
.expect("a deformable polyline bound to a cluster proxy");
// The polyline follows the particles: read its current vertices back.
let blob = &world.soft_bodies[blob_handle];
let mesh = blob.mesh_of(outline_handle).unwrap();
let outline_vertices: Vec<Vector> = mesh.vertex_positions(blob).collect();
assert_eq!(outline_vertices.len(), num);
// A deformable triangle mesh bound to the jelly: each vertex is embedded in the cell
// holding it (`skinned`), or follows one particle (`direct`). The mesh is given in the
// frame of the proxy it is attached to.
let root = world.soft_bodies[jelly_handle].root_body();
let root_pose = *world.bodies[root].position();
let center = world.soft_bodies[jelly_handle].center_of_mass();
let r = 1.0;
let vertices: Vec<Vector> = [
Vector::new(r, 0.0, 0.0),
Vector::new(-r, 0.0, 0.0),
Vector::new(0.0, r, 0.0),
Vector::new(0.0, -r, 0.0),
Vector::new(0.0, 0.0, r),
Vector::new(0.0, 0.0, -r),
]
.iter()
.map(|v| root_pose.inverse() * (center + *v))
.collect();
let indices = vec![
[0, 2, 4],
[2, 1, 4],
[1, 3, 4],
[3, 0, 4],
[2, 0, 5],
[1, 2, 5],
[3, 1, 5],
[0, 3, 5],
];
let skin = ColliderBuilder::trimesh_with_flags(vertices, indices, TriMeshFlags::DEFORMABLE)
.unwrap()
.sensor(true);
let skin_handle = world
.insert_deformable(skin, SoftMeshBinding::skinned(), root)
.expect("a deformable mesh bound to a cluster proxy");
// The mesh follows the particles: read its current vertices back.
let jelly = &world.soft_bodies[jelly_handle];
let mesh = jelly.mesh_of(skin_handle).unwrap();
let skin_vertices: Vec<Vector> = mesh.vertex_positions(jelly).collect();
assert_eq!(skin_vertices.len(), 6);
- 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 polyline is given in the frame
// of the proxy it is attached to.
let root = blob.rootBody();
let origin = root.translation();
let num = blob.numParticles();
let vertices = blob.particlePositions();
for (let i = 0; i < num; ++i) {
vertices[i * 2] -= origin.x;
vertices[i * 2 + 1] -= origin.y;
}
let indices = new Uint32Array(num * 2);
let particles = [];
for (let i = 0; i < num; ++i) {
indices[i * 2] = i;
indices[i * 2 + 1] = (i + 1) % num;
particles.push(i);
}
let outlineDesc = RAPIER.ColliderDesc.polyline(vertices, indices, RAPIER.PolylineFlags.DEFORMABLE).setSensor(true);
let outline = world.createDeformableCollider(outlineDesc, RAPIER.SoftMeshBinding.direct(particles), root);
// The polyline follows the particles: read its current vertices back.
let meshIndex = blob.meshOfCollider(outline);
let outlineVertices: Float32Array = blob.meshVertices(meshIndex);
console.log("The outline has", outlineVertices.length / 2, "vertices");
// A deformable triangle mesh bound to the jelly: each vertex is embedded in the cell
// holding it (`skinned`), or follows one particle (`direct`). The mesh is given in the
// frame of the proxy it is attached to.
let root = jelly.rootBody();
let origin = root.translation();
let c = jelly.centerOfMass();
let r = 1.0;
let vertices = new Float32Array([
c.x + r, c.y, c.z, c.x - r, c.y, c.z, c.x, c.y + r, c.z,
c.x, c.y - r, c.z, c.x, c.y, c.z + r, c.x, c.y, c.z - r,
]);
for (let i = 0; i < 6; ++i) {
vertices[i * 3] -= origin.x;
vertices[i * 3 + 1] -= origin.y;
vertices[i * 3 + 2] -= origin.z;
}
let indices = new Uint32Array([0, 2, 4, 2, 1, 4, 1, 3, 4, 3, 0, 4, 2, 0, 5, 1, 2, 5, 3, 1, 5, 0, 3, 5]);
let skinDesc = RAPIER.ColliderDesc.trimesh(vertices, indices, RAPIER.TriMeshFlags.DEFORMABLE).setSensor(true);
let skin = world.createDeformableCollider(skinDesc, RAPIER.SoftMeshBinding.skinned(), root);
// The mesh follows the particles: read its current vertices back.
let meshIndex = jelly.meshOfCollider(skin);
let skinVertices: Float32Array = jelly.meshVertices(meshIndex);
console.log("The skin has", skinVertices.length / 3, "vertices");
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(())
}
The collider is described by an ordinary R3ColliderDesc which shape is a polyline (2D) or a triangle mesh (3D) flagged
with R2_POLYLINE_DEFORMABLE or R3_TRIMESH_DEFORMABLE (see r2ShapeDesc_SetPolyline and r3ShapeDesc_SetTrimesh),
and its binding by an R3SoftMeshBindingDesc initialized with r3DefaultSoftMeshBindingDesc:
kindselects the binding:R3_SOFT_BINDING_SKINNED(the default),R3_SOFT_BINDING_DIRECT, orR3_SOFT_BINDING_DIRECT_BY_POSITION.particlesis the particle followed by each vertex, for a direct binding.epsilonis the distance within which each vertex is bound to the closest particle, for a binding by position.selfContactsmakes the mesh collide with itself.
The collider is created by r3InsertDeformableCollider, given the rigid-body handle of the root body
(r3SoftBody_RootBody) or of a cluster proxy (r3SoftBody_ClusterProxy) it is attached to. Its other properties
(friction, collision groups, events, sensor, etc.) apply as usual. The arrays of the collider and of the binding are
only borrowed until the insertion. If the binding fails, the error handler is called and the returned handle is
invalid. Then the current vertices of the collider are read with r3SoftBody_MeshVertices:
- 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 polyline is given in the frame
// of the proxy it is attached to.
R2RigidBodyHandle root = r2SoftBody_RootBody(blob);
R2Pose root_pose_inverse = r2PoseInverse(r2RigidBody_Position(root));
size_t num = r2SoftBody_NumParticles(blob); // 24 particles.
R2Vector vertices[24];
R2Edge indices[24];
uint32_t particles[24];
r2SoftBody_ParticlePositions(blob, vertices, 24);
for (uint32_t i = 0; i < num; i++) {
vertices[i] = r2PoseTransformPoint(root_pose_inverse, vertices[i]);
indices[i] = (R2Edge){i, (i + 1) % num};
// The vertex `i` follows the particle `i`.
particles[i] = i;
}
R2ColliderDesc outline = r2DefaultColliderDesc();
r2ShapeDesc_SetPolyline(&outline.shape, (R2VectorView){vertices, num}, (R2EdgeView){indices, num},
R2_POLYLINE_DEFORMABLE);
outline.isSensor = 1;
R2SoftMeshBindingDesc binding = r2DefaultSoftMeshBindingDesc();
binding.kind = R2_SOFT_BINDING_DIRECT;
binding.particles = (R2IndexView){particles, num};
R2ColliderHandle outline_handle = r2InsertDeformableCollider(&outline, &binding, root);
// The polyline follows the particles: read its current vertices back.
R2Vector outline_vertices[24];
size_t num_outline_vertices = r2SoftBody_MeshVertices(blob, outline_handle, outline_vertices, 24);
// A deformable triangle mesh bound to the jelly: each vertex is embedded in the cell
// holding it (skinned), or follows one particle (direct). The mesh is given in the
// frame of the proxy it is attached to.
R3RigidBodyHandle root = r3SoftBody_RootBody(jelly);
R3Pose root_pose_inverse = r3PoseInverse(r3RigidBody_Position(root));
R3Vector center = r3SoftBody_CenterOfMass(jelly);
R3Real r = 1.0;
R3Vector offsets[6] = {{r, 0.0, 0.0}, {-r, 0.0, 0.0}, {0.0, r, 0.0},
{0.0, -r, 0.0}, {0.0, 0.0, r}, {0.0, 0.0, -r}};
R3Vector vertices[6];
for (size_t i = 0; i < 6; i++) {
vertices[i] = r3PoseTransformPoint(root_pose_inverse, r3VectorAdd(center, offsets[i]));
}
R3Triangle indices[8] = {{0, 2, 4}, {2, 1, 4}, {1, 3, 4}, {3, 0, 4},
{2, 0, 5}, {1, 2, 5}, {3, 1, 5}, {0, 3, 5}};
R3ColliderDesc skin = r3DefaultColliderDesc();
r3ShapeDesc_SetTrimesh(&skin.shape, (R3VectorView){vertices, 6}, (R3TriangleView){indices, 8},
R3_TRIMESH_DEFORMABLE);
skin.isSensor = 1;
// Default: R3_SOFT_BINDING_SKINNED.
R3SoftMeshBindingDesc binding = r3DefaultSoftMeshBindingDesc();
R3ColliderHandle skin_handle = r3InsertDeformableCollider(&skin, &binding, root);
// The mesh follows the particles: read its current vertices back.
R3Vector skin_vertices[6];
size_t num_skin_vertices = r3SoftBody_MeshVertices(jelly, skin_handle, skin_vertices, 6);
The collider is built by Collider.trimesh with the TriMeshFlags.DEFORMABLE flag, and its binding by one of the
static methods of SoftMeshBinding: skinned(), direct(particles), or direct_by_position(eps), the
self_contacts method of the binding making the mesh collide with itself. The collider is created by
PhysicsWorld.insert_deformable (or by ColliderSet.insert_deformable when the sets are used directly), given the
rigid-body handle of the root body (SoftBody.root_body) or of a cluster proxy (SoftBody.cluster_proxy) it is
attached to. Its other properties (friction, collision groups, events, sensor, etc.) apply as usual. If the binding
fails, a SoftBindingError is raised. Then the current vertices of the collider are read from its SoftCollisionMesh
(SoftBody.mesh_of), and the collider tells which soft-body mesh it is with its deformable_mesh_ref property:
# A deformable triangle mesh bound to the jelly: each vertex is embedded in the cell
# holding it (`skinned`), or follows one particle (`direct`). The mesh is given in the
# frame of the proxy it is attached to.
jelly = world.soft_bodies[jelly_handle]
root = jelly.root_body
to_root = world.rigid_bodies[root].position.inverse()
center = jelly.center_of_mass
r = 1.0
offsets = [(r, 0.0, 0.0), (-r, 0.0, 0.0), (0.0, r, 0.0), (0.0, -r, 0.0), (0.0, 0.0, r), (0.0, 0.0, -r)]
vertices = np.array([tuple(to_root.transform_point(center + v)) for v in offsets], dtype=np.float32)
indices = np.array(
[[0, 2, 4], [2, 1, 4], [1, 3, 4], [3, 0, 4], [2, 0, 5], [1, 2, 5], [3, 1, 5], [0, 3, 5]],
dtype=np.uint32,
)
skin = rp.Collider.trimesh(vertices, indices, rp.TriMeshFlags.DEFORMABLE).sensor(True)
# Raises `SoftBindingError` if the mesh can't be bound to the cluster of `root`.
skin_handle = world.insert_deformable(skin, rp.SoftMeshBinding.skinned(), root)
# The mesh follows the particles: read its current vertices back (a NumPy array).
mesh = world.soft_bodies[jelly_handle].mesh_of(skin_handle)
skin_vertices = mesh.vertices
assert skin_vertices.shape == (6, 3)
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.