Soft-bodies
For a high-level overview of the methods behind our soft-body implementation, see our blog-post.
Rigid-bodies can't deform in any way, which is precisely what makes them cheap and easy to control. Soft-bodies, aka. deformable bodies, are made for everything that must bend, stretch, or squash: ropes, cloth, jelly, balloons, the chassis of a car denting on impact, etc. They live in the same world as the rigid-bodies and the colliders, and interact with all the other features of the engine with little to no restriction:
- Any kind of impulse joint can be attached to a soft-body through its soft frames, which allows the definition of links between two soft-bodies, between a soft-body and a rigid-body, or between a soft-body and a multibody.
- Deformable colliders, as well as rigid ones, can be attached to a soft-body, sensors included.
- A detailed mesh can follow the deformations of a coarser simulated lattice (skinning).
- Parts of a soft-body can be controlled kinematically.
- A soft-body can deform permanently (plasticity), and tear apart (tearing).
The FEM solver requires the library to be built with the fem feature (see
building the C bindings).
Definition of a soft-body
A rigid-body is a single moving frame: the position and the velocity of any of its points follow from one translation and one rotation. This is precisely what a deformable body can't be, because its parts must be able to move (somewhat) independently of each other. Instead, a soft-body is made of particles, aka. mass points, i.e., points without any orientation, each having 2 degrees of freedom in 2D and 3 in 3D. The more particles a body is made of, the more detailed its deformations can be, and the more expensive it is to simulate.
The particles are kept together by a lattice made of up to three kinds of elements:
- The particles are the only mandatory element, i.e., the positions the
R3SoftBodyDescis built from. They carry the mass and the velocity of the body. - The edges (
edges) connect two particles. The structural edges resist the stretching (and the compression) of the body, whereas the bending edges (bendEdges) and the dihedral constraints (dihedrals, 3D) resist its bending. - The cells (
cells) connect three particles in 2D (triangles) and four in 3D (tetrahedra). They resist the deformation of the area (2D) or of the volume (3D) they cover.
Two more concepts describe how the body interacts with the rest of the world. The surface
(surface) is the boundary of the body (segments in 2D, triangles in 3D): this is
what the body collides with, and what the preservation of its volume integrates over. A body without any surface, e.g.,
a cloud of particles, collides with nothing at all, unless it is given wire segments
(wire, 3D), in which case it collides as a polyline, which is what a 3D rope does.
Finally, a skin (skinVertices and skinIndices) is a mesh which vertices follow the cells without being
particles themselves, as detailed in the skinning section.
Every element can be given manually to the soft-body description, but the constructors of the next section build the lattices of the most common shapes directly. The arrays of elements are borrowed by the description (as views pairing a pointer with their number of elements), and copied by its insertion: a non-empty array replaces the elements generated by the constructor. Note that the elements a body is made of decide which of the cohesion models can hold it together.
Creation and insertion
A soft-body is described by a R3SoftBodyDesc, which constructors build the lattice of the most common shapes:
| Constructor | Dimension | Lattice |
|---|---|---|
r3DefaultSoftBodyDesc, then r3SoftBodyDesc_SetParticles | 2D, 3D | No element at all: only the given particles. |
r3RopeSoftBodyDesc | 2D, 3D | Structural and bending edges between the particles of a line. |
r3ClothSoftBodyDesc, r3ClothAnisotropicSoftBodyDesc, r3ClothTubeSoftBodyDesc | 3D | Structural, shear and bending edges, with a triangle surface. |
r2GridSoftBodyDesc | 2D | Triangle cells filling a rectangle. |
r3CuboidSoftBodyDesc | 3D | Tetrahedral cells filling a box. |
r2PolygonSoftBodyDesc, r2DiskSoftBodyDesc | 2D | A closed boundary preserving its area. |
r3SphereSoftBodyDesc | 3D | A closed surface preserving its volume, with dihedral bending constraints. |
r3SoftBodyDesc_SetSurfaceMesh | 2D (segments), 3D (triangles) | The vertices and edges of a mesh, held by shape matching. |
r2SoftBodyDesc_SetTrimesh | 2D | The vertices and edges of a triangle mesh, held by shape matching. |
r3VolumetricSoftBodyDesc | 2D, 3D | Cells filling a closed mesh. |
The constructors return a description initialized with the default values of every other field, which can then be
modified before its insertion. The ones existing in a single dimension are only given with their prefix in that
dimension, e.g., there is no r3GridSoftBodyDesc.
The r3VolumetricSoftBodyDesc constructor is the one to use for an arbitrary solid: it fills a closed mesh (segments in 2D, triangles
in 3D, oriented outward) with cells of about the requested size. The interior of the mesh is triangulated by Delaunay
refinement in 2D, whereas in 3D every cell of a lattice the mesh reaches is kept whole: the result contains the mesh
instead of following it exactly, and its boundary is as blocky as its cells. The meshing parameters are given to the constructor as an R3VolumeMeshParameters, initialized by r3NewVolumeMeshParameters from the size of the cells: in 2D, min_angle is the minimum angle of the triangles, whereas in 3D, cover_smoothing and cover_subdivisions control how much the cover is smoothed and subdivided around the boundary, i.e., how closely it follows the mesh, and enclosure whether the surface alone is covered (1), leaving the interior empty. Note that the mesh is only filled by the insertion, which reports an error if the mesh isn't closed or encloses nothing at that cell size.
- Example 2D
- Example 3D
// Fill a closed, counter-clockwise polyline with triangle cells of about 0.2 in size.
const R2Vector vertices[] = {
r2Vector(-0.5, -0.25),
r2Vector(0.5, -0.25),
r2Vector(0.5, 0.25),
r2Vector(-0.5, 0.25),
};
const R2Edge indices[] = {{0, 1}, {1, 2}, {2, 3}, {3, 0}};
R2SoftBodyDesc block = r2VolumetricSoftBodyDesc((R2VectorView){vertices, 4}, (R2EdgeView){indices, 4},
r2NewVolumeMeshParameters(0.2));
block.translation = r2Vector(-3.0, 1.0);
// The polyline is only read during the insertion, which fails if it isn't closed.
R2SoftBodyHandle block_handle = r2InsertSoftBody(world, &block);
// Fill a closed, outward-oriented triangle mesh with tetrahedral cells of about 0.2 in size.
// The triangle mesh of a cuboid (the subdivision counts only matter for curved shapes).
R3SharedShape *cuboid = r3CuboidSharedShape(r3Vector(0.5, 0.25, 0.25));
R3TriMeshData *mesh = r3SharedShape_ToTrimesh(cuboid, 0, 0);
size_t num_vertices = r3TriMeshData_Vertices(mesh, NULL, 0);
size_t num_indices = r3TriMeshData_Indices(mesh, NULL, 0);
R3Vector *vertices = malloc(num_vertices * sizeof(R3Vector));
R3Triangle *triangles = malloc(num_indices * sizeof(uint32_t));
r3TriMeshData_Vertices(mesh, vertices, num_vertices);
r3TriMeshData_Indices(mesh, (uint32_t *)triangles, num_indices);
R3SoftBodyDesc block = r3VolumetricSoftBodyDesc((R3VectorView){vertices, num_vertices},
(R3TriangleView){triangles, num_indices / 3},
r3NewVolumeMeshParameters(0.2));
block.translation = r3Vector(-3.0, 1.0, 0.0);
// The mesh is only read during the insertion, which fails if it isn't closed.
R3SoftBodyHandle block_handle = r3InsertSoftBody(world, &block);
free(vertices);
free(triangles);
r3FreeTriMeshData(mesh);
r3FreeSharedShape(cuboid);
The description allows the definition of everything else that is specific to one soft-body: the particles held in place (the
pinned particles, pinned), the softness of its constraints
(material, e.g., the same softness for every constraint with r3UniformSoftBodyMaterial), the mass of its particles (one mass for every particle with
particleMass, a total mass for the whole body with
totalMass, or one mass per particle with
masses), their radius
(particleRadius), the collider its surface is made of, and whether that
surface is allowed to collide with itself (selfContacts). The particles
can also be given a linear damping (linearDamping), a gravity scale
(gravityScale), a dominance group
(dominanceGroup), and be allowed to sleep or
not (canSleep), exactly like a rigid-body. Inserting the
soft-body into the world (r3InsertSoftBody) will
automatically create the rigid-body standing for it (its root body), as well as the
colliders covering its surface:
- Example 2D
- Example 3D
// A world with a ground.
R2World *world = r2NewWorld();
R2ColliderDesc ground = r2CuboidColliderDesc(r2Vector(10.0, 0.1));
r2InsertColliderWithoutParent(world, &ground);
// Description of a rope of 20 particles between two points.
R2SoftBodyDesc rope = r2RopeSoftBodyDesc(r2Vector(0.0, 3.0), r2Vector(2.0, 3.0), 20);
// Description of a grid of `nx` by `ny` particles filled with triangle cells.
R2SoftBodyDesc grid = r2GridSoftBodyDesc(r2Vector(3.0, 1.0), r2Vector(1.0, 1.0), 6, 6);
// Description of a disk: a ring of particles holding its area (a pressurized blob).
R2SoftBodyDesc disk = r2DiskSoftBodyDesc(r2Vector(0.0, 3.0), 0.8, 24);
// Description of a closed polygon of particles holding its area.
const R2Vector polygon_points[] = {
r2Vector(5.0, 4.0),
r2Vector(7.0, 4.0),
r2Vector(7.0, 6.0),
r2Vector(5.0, 6.0),
};
R2SoftBodyDesc polygon = r2PolygonSoftBodyDesc((R2VectorView){polygon_points, 4});
const uint32_t n = 20;
R2SoftBodyDesc sheet = r2GridSoftBodyDesc(r2Vector(-3.0, 3.0), r2Vector(1.0, 1.0), n, n);
// Particles held in place.
const uint32_t pinned[] = {0, n - 1};
sheet.pinned = (R2IndexView){pinned, 2};
// A uniform softness (natural frequency in Hz, damping ratio) for every constraint.
sheet.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){30.0, 1.0});
// The mass of each particle.
// Default: 1.0
sheet.particleMass = 0.05;
// The thickness of the particles, for collisions.
// Default: disabled, i.e., the radius computed by the constructor.
sheet.particleRadius = (R2OptionalReal){1, 0.05};
// The template of the body's colliders: its shape is replaced by the deformable surface.
sheet.collider = r2BallColliderDesc(0.05);
sheet.collider.friction = 0.8;
// Whether the body may fall asleep.
// Default: 1
sheet.canSleep = 1;
// Insert the soft-body: this creates its hidden root rigid-body and its colliders.
R2SoftBodyHandle sheet_handle = r2InsertSoftBody(world, &sheet);
// A world with a ground.
R3World *world = r3NewWorld();
R3ColliderDesc ground = r3CuboidColliderDesc(r3Vector(10.0, 0.1, 10.0));
r3InsertColliderWithoutParent(world, &ground);
// Description of a rope of 20 particles between two points.
R3SoftBodyDesc rope = r3RopeSoftBodyDesc(r3Vector(0.0, 3.0, 0.0), r3Vector(2.0, 3.0, 0.0), 20);
// Description of a cloth: `nu` by `nv` particles, particle `(i, j)` at `origin + i * du + j * dv`.
const uint32_t n = 20;
R3SoftBodyDesc cloth =
r3ClothSoftBodyDesc(r3Vector(-1.0, 2.0, -1.0), r3Vector(0.1, 0.0, 0.0), r3Vector(0.0, 0.0, 0.1), n, n);
// Description of a box of `nx * ny * nz` particles filled with tetrahedral cells.
R3SoftBodyDesc box = r3CuboidSoftBodyDesc(r3Vector(3.0, 1.0, 0.0), r3Vector(0.5, 0.5, 0.5), 4, 4, 4);
// Description of a hollow sphere holding its volume (a balloon).
R3SoftBodyDesc balloon = r3SphereSoftBodyDesc(r3Vector(0.0, 3.0, 3.0), 0.8, 2);
// Any field of a description can be modified before its insertion.
// Particles held in place.
const uint32_t pinned[] = {0, n - 1, n * (n - 1), n * n - 1};
cloth.pinned = (R3IndexView){pinned, 4};
// A uniform softness (natural frequency in Hz, damping ratio) for every constraint.
cloth.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30.0, 1.0});
// The mass of each particle.
// Default: 1.0
cloth.particleMass = 0.05;
// The thickness of the particles, for collisions.
// Default: disabled, i.e., the radius computed by the constructor.
cloth.particleRadius = (R3OptionalReal){1, 0.02};
// The template of the body's colliders: its shape is replaced by the deformable surface.
cloth.collider = r3BallColliderDesc(0.05);
cloth.collider.friction = 0.8;
// Whether the surface collides with itself.
// Default: 0
cloth.selfContacts = 1;
// Whether the body may fall asleep.
// Default: 1
cloth.canSleep = 1;
// Insert the soft-body: this creates its hidden root rigid-body and its colliders.
R3SoftBodyHandle cloth_handle = r3InsertSoftBody(world, &cloth);
The collider given to the collider field of the description is
only a template: its shape is replaced by the deformable surface of the soft-body, and its density is ignored, whereas
all its other properties are kept. Therefore this is where the friction, the collision
groups, or the active events of
the soft-body must be set. A body that should only collide through colliders of your own can be built without any
default one (collisionEnabled set to 0).
Two descriptions can be merged into a single soft-body with r3SoftBodyDesc_SetAppended (each appended description keeps its own particles, masses, and elements, whereas every other setting comes from the description it is appended to), and their pieces
sewn together with additional edges (r3SoftBodyDesc_SetAddedEdges), which rest length is the distance
their particles have when they are added. This is, e.g., how the sleeves of a shirt are attached to its body.
Soft-bodies of the world
The soft-bodies are owned by the world, next to the rigid-bodies and the colliders, and are identified by their
R3SoftBodyHandle. There is no separate set to manage: r3InsertSoftBody creates the soft-body as well as the
rigid-body standing for it and the colliders of its surface, and r3FreeWorld frees all of them. The number of
soft-bodies of the world is given by r3SoftBodyCount, and their handles by r3SoftBodyHandles. A handle becomes
invalid once its soft-body is removed, which is checked by r3SoftBody_Contains. Conversely, the soft-body a
rigid-body stands for (its root body, or the proxy of one of its
clusters) is given by r3RigidBody_SoftBody, which returns an invalid handle for any
other rigid-body:
- Example 2D
- Example 3D
// The world owns every soft-body: their number and their handles can be read at any time.
R2SoftBodyDesc rope_desc = r2RopeSoftBodyDesc(r2Vector(0.0, 3.0), r2Vector(2.0, 3.0), 20);
R2SoftBodyHandle rope_handle = r2InsertSoftBody(world, &rope_desc);
size_t num_soft_bodies = r2SoftBodyCount(world);
R2SoftBodyHandle *soft_bodies = malloc(num_soft_bodies * sizeof(R2SoftBodyHandle));
r2SoftBodyHandles(world, soft_bodies, num_soft_bodies);
for (size_t i = 0; i < num_soft_bodies; i++) {
printf("Soft-body %u has %zu particles.\n", soft_bodies[i].index,
r2SoftBody_NumParticles(soft_bodies[i]));
}
free(soft_bodies);
// Whether a handle still refers to a soft-body of the world.
assert(r2SoftBody_Contains(rope_handle));
// The soft-body a rigid-body stands for (its root body, or the proxy of one of its clusters).
R2SoftBodyHandle owner = r2RigidBody_SoftBody(r2SoftBody_RootBody(rope_handle));
assert(owner.index == rope_handle.index && owner.generation == rope_handle.generation);
// The world owns every soft-body: their number and their handles can be read at any time.
R3SoftBodyDesc rope_desc = r3RopeSoftBodyDesc(r3Vector(0.0, 3.0, 0.0), r3Vector(2.0, 3.0, 0.0), 20);
R3SoftBodyHandle rope_handle = r3InsertSoftBody(world, &rope_desc);
size_t num_soft_bodies = r3SoftBodyCount(world);
R3SoftBodyHandle *soft_bodies = malloc(num_soft_bodies * sizeof(R3SoftBodyHandle));
r3SoftBodyHandles(world, soft_bodies, num_soft_bodies);
for (size_t i = 0; i < num_soft_bodies; i++) {
printf("Soft-body %u has %zu particles.\n", soft_bodies[i].index,
r3SoftBody_NumParticles(soft_bodies[i]));
}
free(soft_bodies);
// Whether a handle still refers to a soft-body of the world.
assert(r3SoftBody_Contains(rope_handle));
// The soft-body a rigid-body stands for (its root body, or the proxy of one of its clusters).
R3SoftBodyHandle owner = r3RigidBody_SoftBody(r3SoftBody_RootBody(rope_handle));
assert(owner.index == rope_handle.index && owner.generation == rope_handle.generation);
The state of the body as a whole can be read at any time as well: its number of particles (r3SoftBody_NumParticles),
its total mass (r3SoftBody_Mass), its mass-weighted center of mass (r3SoftBody_CenterOfMass), the area (2D) or the
volume (3D) it currently encloses and its rest value (r3SoftBody_Volume, r3SoftBody_RestVolume), and whether it is
sleeping (r3SoftBody_IsSleeping, see also r3SoftBody_WakeUp). Finally, a soft-body can be disabled until it is
enabled again with r3SoftBody_SetEnabled.
Keeping the body together
Rapier supports three ways of holding the particles of a soft-body together: shape matching, constraints, and the Finite Elements Method (FEM). They can be combined, e.g., shape matching on top of edge constraints. Note that shape matching works on the particles alone, whereas the constraints need edges or cells, and the FEM solver needs cells.
Shape matching
Shape matching doesn't need any element. At each timestep, the rest shape of the body is placed where it best fits its current shape, i.e., both shapes are given the same center of mass, and the rest shape is given the rotation bringing its particles the closest to the current ones. Each particle is then pulled toward its twin in that matched rest shape by a spring:
This is cheap, and a body always recovers its original shape whatever the deformation it went through. On the other
hand, the particles don't interact with their neighbors: pushing on one particle doesn't pull the ones around it, which
makes the deformations feel very local. Shape matching is enabled by
the shapeMatching field of the description, and the strength of its
springs is the shape matching softness (shapeMatchingSoftness)
of the material:
- Example 2D
- Example 3D
// A cloud of particles without any element: shape matching alone pulls them back toward
// their rest shape, placed where it best fits the current one.
R2Vector points[9];
for (int i = 0; i < 9; i++) {
points[i] = r2VectorScale(r2Vector(i % 3, i / 3 + 4.0), 0.3);
}
R2SoftBodyDesc cloud = r2DefaultSoftBodyDesc();
r2SoftBodyDesc_SetParticles(&cloud, (R2VectorView){points, 9});
cloud.shapeMatching = (R2OptionalBool){1, 1};
// How fast the particles are pulled back toward their rest shape.
cloud.material.shapeMatchingSoftness = (R2SpringCoefficients){5.0, 1.0};
cloud.particleRadius = (R2OptionalReal){1, 0.1};
R2SoftBodyHandle cloud_handle = r2InsertSoftBody(world, &cloud);
// A cloud of particles without any element: shape matching alone pulls them back toward
// their rest shape, placed where it best fits the current one.
R3Vector points[27];
for (int i = 0; i < 27; i++) {
points[i] = r3VectorScale(r3Vector(i % 3, i / 3 % 3 + 4.0, i / 9), 0.3);
}
R3SoftBodyDesc cloud = r3DefaultSoftBodyDesc();
r3SoftBodyDesc_SetParticles(&cloud, (R3VectorView){points, 27});
cloud.shapeMatching = (R3OptionalBool){1, 1};
// How fast the particles are pulled back toward their rest shape.
cloud.material.shapeMatchingSoftness = (R3SpringCoefficients){5.0, 1.0};
cloud.particleRadius = (R3OptionalReal){1, 0.1};
R3SoftBodyHandle cloud_handle = r3InsertSoftBody(world, &cloud);
Use shape matching for low-detail deformations, or whenever computing a topology (edges, cells) isn't desired. It is
enabled by default by the r3SoftBodyDesc_SetSurfaceMesh and r2SoftBodyDesc_SetTrimesh constructors (unless the shapeMatching override disables it). Note however that it performs very poorly for ropes,
cloth, or any open shape. Also note that combining it with edges makes the deformations spread to the neighbors to look
more realistic.
Constraints
The constraints-based soft-body solver is the default solver (R3_SOFT_SOLVER_CONSTRAINTS), and the most versatile one:
every edge and cell element of the deformation lattice becomes a constraint, solved together with the contacts and the joints of the scene.
An edge is a spring-damper pulling its two particles back toward its rest length. A cell is either a volume constraint
keeping its area or its volume, the shape itself being held by the edges, or an elastic element resisting any
deformation. This is selected by the cell model of the body
(cellModel):
R3_SOFT_CELL_VOLUME: one constraint per cell keeping its area (2D) or its volume (3D) at its rest value. This is the cheapest model, and combined with the edges it is often enough to obtain a convincing jelly.R3_SOFT_CELL_COROTATIONAL: linear elasticity expressed in the rotation-free frame of the cell. It is stable at any stiffness and recovers from inverted cells.R3_SOFT_CELL_NEO_HOOKEAN: stable Neo-Hookean hyperelasticity. It feels stiffer than linear elasticity on compression, but softer on tension.
The stiffness of every element is configured by the R3SoftBodyMaterial of the body, which can be given to the description or
set at any time with r3SoftBody_SetMaterial (the current one being given by r3SoftBody_Material). A material is initialized by r3DefaultSoftBodyMaterial, or by r3UniformSoftBodyMaterial which gives the same softness to every constraint. The edges and the volume constraints are given a softness, i.e., a
natural frequency (in Hz) and a damping ratio instead of a stiffness, so it doesn't depend on the masses of the
particles:
- The edge softness (
edgeSoftness) for the structural edges; - The bend softness (
bendSoftness) for the bending edges and the dihedral constraints; - The volume softness (
volumeSoftness) for the volume constraints.
The elastic cells are given a Young modulus (youngModulus, in force per unit
area in 3D, per unit length in 2D), a Poisson ratio (poissonRatio), and a
damping ratio (elasticDampingRatio) instead. Their natural frequency
is derived from these, therefore a body meshed more finely doesn't become stiffer, whereas it becomes more expensive to
simulate.
Finally, a body with a closed surface can preserve the area (2D) or the volume (3D) it encloses
(volumePreservation), which target can be scaled by a
volumeFactor (or r3SoftBody_SetVolumeFactor after the insertion)
greater than 1 in order to inflate the body, e.g., to simulate a pressurized blob:
- Example 2D
- Example 3D
// Elastic cells: a jelly square with corotational linear elasticity.
R2SoftBodyDesc jelly = r2GridSoftBodyDesc(r2Vector(3.0, 1.2), r2Vector(1.0, 1.0), 6, 6);
// The constitutive model of the cells: R2_SOFT_CELL_VOLUME (per-cell area constraints,
// the shape is held by the edges), R2_SOFT_CELL_COROTATIONAL or R2_SOFT_CELL_NEO_HOOKEAN.
jelly.cellModel = R2_SOFT_CELL_COROTATIONAL;
// Stiffness of the elastic cells.
jelly.material.youngModulus = 3.0e3;
jelly.material.poissonRatio = 0.35;
jelly.material.elasticDampingRatio = 0.5;
// Plasticity: the rest shape flows past 5% strain, at 20 per second.
jelly.material.plasticYield = 0.05;
jelly.material.plasticCreep = 20.0;
// Tearing: an element past 40% strain tears.
jelly.material.tearStrain = (R2OptionalReal){1, 0.4};
jelly.particleMass = 0.2;
R2SoftBodyHandle jelly_handle = r2InsertSoftBody(world, &jelly);
// A pressurized blob: a ring of particles inflated by area preservation.
R2SoftBodyDesc blob = r2DiskSoftBodyDesc(r2Vector(0.0, 3.0), 0.8, 24);
blob.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){20.0, 1.0});
// Target area multiplier (`> 1` inflates the body), for the area preservation enabled by
// the disk constructor (`volumePreservation`).
blob.volumeFactor = 1.1;
blob.selfContacts = 1;
R2SoftBodyHandle blob_handle = r2InsertSoftBody(world, &blob);
// Elastic cells: a jelly cube with corotational linear elasticity.
R3SoftBodyDesc jelly = r3CuboidSoftBodyDesc(r3Vector(3.0, 1.0, 0.0), r3Vector(0.5, 0.5, 0.5), 4, 4, 4);
// The constitutive model of the cells: R3_SOFT_CELL_VOLUME (per-cell volume constraints,
// the shape is held by the edges), R3_SOFT_CELL_COROTATIONAL or R3_SOFT_CELL_NEO_HOOKEAN.
jelly.cellModel = R3_SOFT_CELL_COROTATIONAL;
// Stiffness of the elastic cells.
jelly.material.youngModulus = 2.0e3;
jelly.material.poissonRatio = 0.35;
jelly.material.elasticDampingRatio = 0.5;
// Plasticity: the rest shape flows past 5% strain, at 20 per second.
jelly.material.plasticYield = 0.05;
jelly.material.plasticCreep = 20.0;
// Tearing: an element past 40% strain tears.
jelly.material.tearStrain = (R3OptionalReal){1, 0.4};
jelly.particleMass = 0.2;
R3SoftBodyHandle jelly_handle = r3InsertSoftBody(world, &jelly);
// A material shared by the edges, bending constraints and volume constraints.
R3SoftBodyMaterial material = r3UniformSoftBodyMaterial((R3SpringCoefficients){30.0, 1.0});
// Softness of the bending constraints, on top of a uniform 30 Hz softness.
material.bendSoftness = (R3SpringCoefficients){3.0, 1.0};
r3SoftBody_SetMaterial(cloth_handle, &material);
The stiffness effectively simulated by the constraints solver depends on its convergence: with too few iterations, a stiff
body looks softer than its material says. This is why soft-bodies are configured with 3 additional internal PGS solver iterations by default, which can
be modified with
additionalPgsIterations. The whole island a body belongs to
can also be given additional substeps with
additionalSolverIterations, just like
rigid-bodies.
The following table gathers the settings to look at for the most common problems:
| Problem | What to change |
|---|---|
| The body is too soft, or stretches too much. | Raise the natural frequency of the material's edgeSoftness, or its youngModulus for the elastic cells. Give it more additionalPgsIterations, or switch it to the FEM solver with solver. |
| A cloth stretches, but should still fold easily. | Keep a stiff edgeSoftness, and give it a soft bendSoftness. |
| A rope compresses like a spring. | Make its edges resist stretching only with tensionOnlyEdges. |
| The body keeps wobbling after an impact. | Raise the damping_ratio of the material's softnesses and its elasticDampingRatio, or its deformationDamping (which damps the deformations but not the motion of the body as a whole). |
| A closed body collapses, or must be inflated. | Enable volumePreservation, and give it a volumeFactor greater than 1. |
| The deformations are too local. | Combine shapeMatching with edges, or rely on edges and cells alone. |
FEM solver
The FEM solver (R3_SOFT_SOLVER_FEM, given to the solver field of the description or to r3SoftBody_SetSolver, and requiring the fem feature of the library) resolves the elasticity of the whole body at once and semi-implicitly:
the forces and the stiffness of every cell are assembled into a single linear system, solved at each substep. Therefore
the stiffness of the body no longer depends on the number of solver iterations, which makes it capable of simulating
very stiff materials, as well as more realistic plastic deformations and failures:
This comes at a price: the system is factorized at each timestep, and every constraint touching the body (contacts,
joints) needs a solve against it. Note that the FEM solver requires cells, so it only applies to the bodies built with
cells, e.g., with the r2GridSoftBodyDesc, r3CuboidSoftBodyDesc, or r3VolumetricSoftBodyDesc constructors. The configuration of its linear solves is shared
by every body using it, and lives in the integration parameters:
- Example 2D
- Example 3D
// A stiff beam simulated by the FEM solver (requires the `fem` feature): its stiffness
// doesn't depend on the number of solver iterations.
R2SoftBodyDesc beam = r2GridSoftBodyDesc(r2Vector(0.0, 2.0), r2Vector(1.0, 0.1), 21, 3);
beam.solver = R2_SOFT_SOLVER_FEM;
beam.cellModel = R2_SOFT_CELL_NEO_HOOKEAN;
beam.material.youngModulus = 1.0e5;
beam.material.poissonRatio = 0.3;
// The particles of the side at `x = -1` are the first 3 ones.
const uint32_t beam_pinned[] = {0, 1, 2};
beam.pinned = (R2IndexView){beam_pinned, 3};
R2SoftBodyHandle beam_handle = r2InsertSoftBody(world, &beam);
// The tuning of the linear solves of the FEM solver, shared by every body using it.
r2FemSetLinearTolerance(world, 1.0e-5);
r2FemSetMaxLinearIterations(world, 20);
// A stiff beam simulated by the FEM solver (requires the `fem` feature): its stiffness
// doesn't depend on the number of solver iterations.
R3SoftBodyDesc beam = r3CuboidSoftBodyDesc(r3Vector(0.0, 2.0, -3.0), r3Vector(1.0, 0.1, 0.1), 11, 3, 3);
beam.solver = R3_SOFT_SOLVER_FEM;
beam.cellModel = R3_SOFT_CELL_NEO_HOOKEAN;
beam.material.youngModulus = 1.0e5;
beam.material.poissonRatio = 0.3;
// The particles of the face at `x = -1` are the first 3 × 3 ones.
const uint32_t beam_pinned[] = {0, 1, 2, 3, 4, 5, 6, 7, 8};
beam.pinned = (R3IndexView){beam_pinned, 9};
R3SoftBodyHandle beam_handle = r3InsertSoftBody(world, &beam);
// The tuning of the linear solves of the FEM solver, shared by every body using it.
r3FemSetLinearTolerance(world, 1.0e-5);
r3FemSetMaxLinearIterations(world, 20);
The linear solves stop at the relative residual
linearTolerance (set by r3FemSetLinearTolerance), or after
maxLinearIterations (set by r3FemSetMaxLinearIterations) conjugate-gradient iterations,
whatever the residual. The bodies with at most
maxDenseDofs (set by r3FemSetMaxDenseDofs) degrees of freedom (600 by default) are
factorized directly, whereas the larger ones rely on the iterative conjugate gradient.
Use the FEM solver for stiff materials which simulated stiffness must not depend on the iteration count, e.g., the chassis of a car or a metal beam. This also results in more realistic plasticity and tearing.
Contacts and self-intersections
A soft-body collides through the collider covering its surface, which is a deformable triangle-mesh (a polyline in 2D) built from the template given to the builder.
Thickness
The radius of the particles
(the particleRadius field of R3SoftBodyDesc) is
the thickness of the soft-body wrt. collision-detection: it is the contact skin of its surface collider, i.e., the
distance kept between the surface and the objects touching it, as well as the distance the solver bounds the motion of
one particle by at each substep. A radius that is too small relative to the distance between two neighboring particles
may let thin objects pass through the surface, whereas a radius larger than that distance will make the body collide
with itself even when it is at rest. Therefore it is recommended to keep it well below the distance between two
neighboring particles. Note that every constructor picks a sensible default, i.e., about half the length of its edges
or of its cells.
Oriented surfaces and shells
A closed surface (a balloon, a jelly cube, a filled polygon) is oriented: its contacts are generated on its outward
side only, like for an oriented triangle-mesh, so nothing is held
inside it. An open surface (a rope, a cloth) is two-sided, because a body arriving from either side must be stopped.
This is what the builder does by default, and it is what a solid body wants. A shell, i.e., a closed surface which
inner side must hold the bodies contained in it, is obtained by asking for a surface that is not oriented
(the oriented field of R3SoftBodyDesc):
- Example 2D
- Example 3D
// A shell: a closed surface that is not oriented, so its inner side holds the bodies put
// inside it (a bowl, a box, a container). A closed surface is oriented by default.
R2SoftBodyDesc bowl = r2DiskSoftBodyDesc(r2Vector(-3.0, 2.0), 0.8, 24);
// Default: disabled, i.e., oriented if the surface is closed.
bowl.oriented = (R2OptionalBool){1, 0};
bowl.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){60.0, 1.0});
R2SoftBodyHandle bowl_handle = r2InsertSoftBody(world, &bowl);
// A shell: a closed surface that is not oriented, so its inner side holds the bodies put
// inside it (a bowl, a box, a container). A closed surface is oriented by default.
R3SoftBodyDesc bowl = r3SphereSoftBodyDesc(r3Vector(-3.0, 2.0, 0.0), 0.8, 2);
// Default: disabled, i.e., oriented if the surface is closed.
bowl.oriented = (R3OptionalBool){1, 0};
bowl.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){60.0, 1.0});
R3SoftBodyHandle bowl_handle = r3InsertSoftBody(world, &bowl);
After the insertion, the shape of the surface collider (given by r3SoftBody_MeshColliders) is
the authority: the flag is changed there, like for any other collider.
Self-contacts
A soft-body doesn't collide with itself by default. Self-contacts are enabled by
the selfContacts field of R3SoftBodyDesc,
which makes the vertices and the edges of the surface collide with the surface of their own body: this is what keeps
a cloth folding onto itself, or a jelly squashed against itself, from passing through itself. Note that they are more
expensive, since the whole surface must be tested against itself.
Penetrations and tangles
The contacts between two meshes are computed triangle by triangle (segment by segment in 2D), without any notion of their interiors. As long as the two surfaces don't penetrate, this works well. But as soon as they do, some of these local contacts start pointing the wrong way, and actively keep the two surfaces in their penetrating state instead of separating them. Deformations make it worse, since a single surface can also cross itself and end up tangled:
Rapier handles these configurations by measuring the volume of the overlap between the two surfaces. The gradient of that volume gives a good approximation of the direction separating the two bodies, aka. the volume normal, which is used both to push the overlapping regions apart, and to correct the direction of the local contacts inside them:
This is enabled by default for the closed surfaces, against other soft-bodies as well as against rigid colliders.
It is configured, along with the detection and the recovery of the tangled configurations, by the recovery settings of
the global settings
(softBodies.recovery of R3IntegrationParameters).
Every mechanism can be switched off individually, and the table below lists the ones to look at for the most common
problems:
| Problem | What to change |
|---|---|
| Thin or fast objects pass through a surface. | Raise the particle radius (particleRadius). Let the body request more substeps while it is hit fast (maxExtraSubsteps). |
| Two bodies crossing corner-first don't collide. | Enable edgeSpeculation (note that it can leave pressed 3D piles crossed). |
| A body stays tangled with itself. | Keep selfStandDown enabled (the default), which lets the elasticity untangle it, or push the crossed features apart with crossingRepulsion. |
| The recovery from a penetration is too slow, or too violent. | Change the recoveryPace, i.e., the corrective speed allowed to the recovery (in length units per second). |
| Soft contacts feel too spongy. | Raise the contactStiffening of the soft-body contacts relative to the rigid ones. |
| The overlap of two bodies isn't resolved at all. | Make sure both surfaces are closed: the intersection-volume constraints (overlapConstraints) only apply to them. |
Each of these settings has its own setter (and getter), named after its field: r3SoftBodiesSetMaxExtraSubsteps and
r3SoftBodiesSetContactStiffening for the settings of the soft-bodies, and r3RecoverySetEdgeSpeculation,
r3RecoverySetSelfStandDown, r3RecoverySetCrossingRepulsion, etc. for the recovery settings. See the
global settings for an example.
Soft frames: joints and rigid colliders
Joints and rigid colliders both need a frame to be attached to, i.e., a translation and a rotation, which a soft-body
doesn't have. This is what soft frames are for: a soft frame is a rigid-body of type
R3_SOFT_FRAME (see r3RigidBody_IsSoftFrame) which pose is computed at each timestep
from a set of particles, by shape-matching. Since it is an ordinary rigid-body, every API working with rigid-bodies
works with it too: impulse joints of any kind (fixed, revolute, prismatic, generic, etc.) can be
attached to it, as well as rigid colliders (sensors included), and its position can be read at any time. Therefore a
soft-body is linked to another soft-body, to a rigid-body, or to a multibody
exactly the same way two rigid-bodies are.
The root body
Every soft-body is created with one soft frame covering all of its particles: its root body. It is the rigid-body
given by r3SoftBody_RootBody. A
joint attached to it acts on the soft-body as a whole, and so does a force or an impulse applied to it. It also stands
for the soft-body in the islands, and it is the parent of the colliders the engine built for the body's surface (a
deformable collider bound to another cluster has the proxy of that cluster as its parent
instead). The soft-body a collider belongs to is given by r3Collider_SoftBody, which is how a collider reported by a
scene query or by a collision
event is traced back to the body it
covers.
- Example 2D
- Example 3D
// The rigid-body the engine created for the whole soft-body, read back after its insertion.
R2RigidBodyHandle root = r2SoftBody_RootBody(jelly_handle);
assert(r2RigidBody_IsSoftFrame(root));
// A rigid collider attached to it follows the frame of the whole body: here a sensor
// detecting what comes close to the jelly.
R2ColliderDesc sensor = r2BallColliderDesc(1.6);
sensor.isSensor = 1;
r2InsertCollider(root, &sensor);
// A joint attached to it acts on the soft-body as a whole: this one hangs the jelly under a
// fixed anchor by a spring.
R2RigidBodyDesc anchor_desc = r2FixedRigidBodyDesc();
anchor_desc.position.translation = r2Vector(3.0, 5.0);
R2RigidBodyHandle anchor = r2InsertRigidBody(world, &anchor_desc);
R2JointDesc spring = r2SpringJointDesc(2.0, 60.0, 2.0);
r2InsertImpulseJoint(anchor, root, &spring);
// The rigid-body the engine created for the whole soft-body, read back after its insertion.
R3RigidBodyHandle root = r3SoftBody_RootBody(jelly_handle);
assert(r3RigidBody_IsSoftFrame(root));
// A rigid collider attached to it follows the frame of the whole body: here a sensor
// detecting what comes close to the jelly.
R3ColliderDesc sensor = r3BallColliderDesc(1.0);
sensor.isSensor = 1;
r3InsertCollider(root, &sensor);
// A joint attached to it acts on the soft-body as a whole: this one hangs the jelly under a
// fixed anchor by a spring.
R3RigidBodyDesc anchor_desc = r3FixedRigidBodyDesc();
anchor_desc.position.translation = r3Vector(3.0, 4.0, 0.0);
R3RigidBodyHandle anchor = r3InsertRigidBody(world, &anchor_desc);
R3JointDesc spring = r3SpringJointDesc(2.5, 60.0, 2.0);
r3InsertImpulseJoint(anchor, root, &spring);
The pose of the root body is recomputed from the particles at each timestep, therefore moving it has no effect.
Removing it with r3RemoveRigidBody is rejected (with R3_INVALID_ARGUMENT): the soft-body is removed as a whole with r3RemoveSoftBody instead (see removal).
Clusters
A single frame for the whole body is often not expressive enough: several joints attached to the root body all act on
the body as a whole, and their effect isn't concentrated where they are attached. This is why a soft-body can also be
given clusters (r3SoftBody_AddCluster), i.e.,
soft frames over any subset of its particles, each with its own pose computed by shape-matching over that subset only.
Joints attached to different clusters then act on different parts of the body, each with its own orientation:
Similarly, rigid colliders attached to the proxies of different clusters move and rotate independently, which is what allows the definition of rigid parts on a deformable body: the handle of a deformable hammer, the bones of a soft character, or the plate a jelly is carried on.
A cluster is identified by its index in the soft-body, returned by r3SoftBody_AddCluster, and its proxy is given by
r3SoftBody_ClusterProxy. The indices of the live clusters of a body are given by r3SoftBody_Clusters, and the
particles of one of them by r3SoftBody_ClusterParticles:
- Example 2D
- Example 3D
// A cluster over the top particles of the jelly: a rigid proxy that joints and
// colliders can attach to.
size_t num_jelly_particles = r2SoftBody_NumParticles(jelly_handle);
uint32_t *top = malloc(num_jelly_particles * sizeof(uint32_t));
size_t num_top = 0;
for (uint32_t i = 0; i < num_jelly_particles; i++) {
if (r2SoftBody_ParticlePosition(jelly_handle, i).y > 2.0) {
top[num_top++] = i;
}
}
uint32_t cluster = r2SoftBody_AddCluster(jelly_handle, top, num_top);
free(top);
R2RigidBodyHandle proxy = r2SoftBody_ClusterProxy(jelly_handle, cluster);
// A rigid plate welded onto the cluster.
R2RigidBodyDesc plate_desc = r2DynamicRigidBodyDesc();
plate_desc.position.translation = r2Vector(3.0, 2.4);
R2RigidBodyHandle plate = r2InsertRigidBody(world, &plate_desc);
R2ColliderDesc plate_collider = r2CuboidColliderDesc(r2Vector(1.2, 0.05));
plate_collider.density = 0.4;
r2InsertCollider(plate, &plate_collider);
R2JointDesc weld = r2FixedJointDesc();
weld.localFrame1.translation = r2Vector(0.0, -0.1);
r2InsertImpulseJoint(plate, proxy, &weld);
// A cluster can be pinned, driven or tuned as a whole.
r2SoftBody_SetClusterStiffnessScale(jelly_handle, cluster, 2.0);
r2SoftBody_SetClusterShapeMatchingEnabled(jelly_handle, cluster, 1);
// A cluster over the top particles of the jelly: a rigid proxy that joints and
// colliders can attach to.
size_t num_jelly_particles = r3SoftBody_NumParticles(jelly_handle);
uint32_t *top = malloc(num_jelly_particles * sizeof(uint32_t));
size_t num_top = 0;
for (uint32_t i = 0; i < num_jelly_particles; i++) {
if (r3SoftBody_ParticlePosition(jelly_handle, i).y > 1.3) {
top[num_top++] = i;
}
}
uint32_t cluster = r3SoftBody_AddCluster(jelly_handle, top, num_top);
free(top);
R3RigidBodyHandle proxy = r3SoftBody_ClusterProxy(jelly_handle, cluster);
// A rigid plate welded onto the cluster.
R3RigidBodyDesc plate_desc = r3DynamicRigidBodyDesc();
plate_desc.position.translation = r3Vector(3.0, 1.9, 0.0);
R3RigidBodyHandle plate = r3InsertRigidBody(world, &plate_desc);
R3ColliderDesc plate_collider = r3CuboidColliderDesc(r3Vector(0.7, 0.05, 0.7));
plate_collider.density = 0.4;
r3InsertCollider(plate, &plate_collider);
R3JointDesc weld = r3FixedJointDesc();
weld.localFrame1.translation = r3Vector(0.0, -0.1, 0.0);
r3InsertImpulseJoint(plate, proxy, &weld);
// A cluster can be pinned, driven or tuned as a whole.
r3SoftBody_SetClusterStiffnessScale(jelly_handle, cluster, 2.0);
r3SoftBody_SetClusterShapeMatchingEnabled(jelly_handle, cluster, 1);
A cluster also defines a few settings for the elements it covers, which gives regional materials without needing separate bodies:
- The stiffness scale
(
r3SoftBody_SetClusterStiffnessScale) multiplies the Young modulus of every cell entirely contained in the cluster (the cells straddling its boundary are left unchanged). - The edge softness
(
r3SoftBody_SetClusterEdgeSoftness) overrides the softness of every edge entirely contained in the cluster, e.g., a stiffer collar on a shirt. - The tear resistance
(
r3SoftBody_SetClusterTearResistance) multiplies the tear thresholds of every element entirely contained in the cluster, e.g., a tough region, or a perforation line. - Shape-matching (
r3SoftBody_SetClusterShapeMatchingEnabled) pulls the particles of the cluster toward the frame of its proxy (or toward the target pose given byr3SoftBody_SetClusterShapeMatchingTarget), so that part of the body tends to keep the shape it was created with.
The rotation of a cluster is deduced from its particles, which isn't possible for a cluster made of a single particle (or, in 3D, of collinear particles). Such a cluster has no angular response, therefore the angular parts of the joints attached to its proxy are disabled.
Attaching a rigid-body to a particle
A particle can also be attached directly to a rigid-body (r3SoftBody_AttachParticle), which is
the simplest way of combining the two kinds of bodies: a rope tied to a swinging ball, a flag attached to its pole,
etc. Unlike pinning, an attachment is a two-way point-to-point constraint: the rigid-body holds the particle, and the
particle pulls the rigid-body back. The anchor is the position the particle has when the attachment is created,
expressed in the local frame of the rigid-body. Note that a particle attached twice keeps both of its attachments, and
that an attachment is undone with
r3SoftBody_DetachParticle:
- Example 2D
- Example 3D
// Attach the last particle of a rope to a rigid box, at the particle's position.
R2SoftBodyDesc rope = r2RopeSoftBodyDesc(r2Vector(8.0, 9.0), r2Vector(12.0, 9.0), 25);
uint32_t pinned[] = {0};
r2SoftBodyDesc_SetPinnedParticles(&rope, (R2IndexView){pinned, 1});
rope.material = r2UniformSoftBodyMaterial((R2SpringCoefficients){40.0, 1.0});
R2SoftBodyHandle rope_handle = r2InsertSoftBody(world, &rope);
R2Vector last_position = r2SoftBody_ParticlePosition(rope_handle, 24);
R2RigidBodyDesc weight_body = r2DynamicRigidBodyDesc();
weight_body.position.translation = r2VectorSub(last_position, r2Vector(0.0, 0.4));
R2RigidBodyHandle weight = r2InsertRigidBody(world, &weight_body);
R2ColliderDesc weight_collider = r2CuboidColliderDesc(r2Vector(0.3, 0.3));
weight_collider.density = 2.0;
r2InsertCollider(weight, &weight_collider);
r2SoftBody_AttachParticle(rope_handle, 24, weight);
// Attach the last particle of a rope to a rigid ball, at the particle's position.
R3SoftBodyDesc rope = r3RopeSoftBodyDesc(r3Vector(-0.5, 5.0, 3.0), r3Vector(2.5, 5.0, 3.0), 30);
uint32_t pinned[] = {0};
r3SoftBodyDesc_SetPinnedParticles(&rope, (R3IndexView){pinned, 1});
rope.material = r3UniformSoftBodyMaterial((R3SpringCoefficients){40.0, 1.0});
R3SoftBodyHandle rope_handle = r3InsertSoftBody(world, &rope);
R3Vector last_position = r3SoftBody_ParticlePosition(rope_handle, 29);
R3RigidBodyDesc ball_body = r3DynamicRigidBodyDesc();
ball_body.position.translation = r3VectorSub(last_position, r3Vector(0.0, 0.3, 0.0));
R3RigidBodyHandle ball = r3InsertRigidBody(world, &ball_body);
R3ColliderDesc ball_collider = r3BallColliderDesc(0.25);
ball_collider.density = 2.0;
r3InsertCollider(ball, &ball_collider);
r3SoftBody_AttachParticle(rope_handle, 29, ball);
Note that r3SoftBody_DetachParticle returns whether the particle was attached at all.
The target of an attachment must be an ordinary rigid-body: r3SoftBody_AttachParticle rejects the root body and
the cluster proxies of a soft-body (they are rigid-bodies too, see r3RigidBody_IsSoftFrame). Two soft-bodies are
linked with a joint between their soft frames instead.
Particles and kinematic control
The state of a soft-body is the state of its particles, which are identified by their index in the body. Their positions
and their velocities can be read (r3SoftBody_ParticlePosition, r3SoftBody_ParticlePositions, r3SoftBody_ParticleVelocity, r3SoftBody_ParticleVelocities) and modified (r3SoftBody_SetParticlePosition, r3SoftBody_SetParticleVelocity) at any time, one by one or all at
once. The elements built from them (r3SoftBody_Edges, r3SoftBody_Cells, and r3SoftBody_Boundary) can be read as well, e.g., in order to render the body with your own mesh. These arrays are copied into a buffer of your own, which capacity is given as the last argument, and a NULL buffer with a zero capacity only returns the length of the array. The elements are given as flat arrays of particle indices: 2 per edge, 3 (2D) or 4 (3D) per cell, and 2 (2D) or 3 (3D) per boundary element.
A particle can also be pinned (r3SoftBody_SetParticlePinned). A pinned particle is kinematic: it is no longer affected by the forces
nor by the contacts, and it will simply hold its position, or follow the kinematic target
(r3SoftBody_SetParticleKinematicTarget) or the velocity it is given. This is, e.g., how a piece of cloth
is hung on a wall, or how a rope is dragged by the player. Releasing the particle gives it back its nominal mass and
lets it keep its current velocity:
- Example 2D
- Example 3D
// Read the particles.
R2Vector position = r2SoftBody_ParticlePosition(sheet_handle, 0);
R2Vector velocity = r2SoftBody_ParticleVelocity(sheet_handle, 0);
size_t num_particles = r2SoftBody_NumParticles(sheet_handle);
R2Vector *positions = malloc(num_particles * sizeof(R2Vector));
r2SoftBody_ParticlePositions(sheet_handle, positions, num_particles);
// Move a particle.
r2SoftBody_SetParticlePosition(sheet_handle, 1, r2VectorAdd(position, r2Vector(0.0, 0.1)));
r2SoftBody_SetParticleVelocity(sheet_handle, 1, velocity);
// Pin (or release) a particle; a pinned particle can be driven like a kinematic body.
r2SoftBody_SetParticlePinned(sheet_handle, 2, 1);
r2SoftBody_SetParticleKinematicTarget(sheet_handle, 2, r2Vector(-3.5, 3.5));
// The elements: edges, cells and the boundary segments, as flat arrays of particle indices
// (2, 3, and 2 indices per element). A NULL buffer with a zero capacity gives their length.
size_t num_edges = r2SoftBody_Edges(sheet_handle, NULL, 0) / 2;
size_t num_cells = r2SoftBody_Cells(sheet_handle, NULL, 0) / 3;
size_t boundary_len = r2SoftBody_Boundary(sheet_handle, NULL, 0);
uint32_t *boundary = malloc(boundary_len * sizeof(uint32_t));
r2SoftBody_Boundary(sheet_handle, boundary, boundary_len);
assert(num_edges > 0 && num_cells > 0 && boundary_len > 0);
free(positions);
free(boundary);
// Read the particles.
R3Vector position = r3SoftBody_ParticlePosition(cloth_handle, 0);
R3Vector velocity = r3SoftBody_ParticleVelocity(cloth_handle, 0);
size_t num_particles = r3SoftBody_NumParticles(cloth_handle);
R3Vector *positions = malloc(num_particles * sizeof(R3Vector));
r3SoftBody_ParticlePositions(cloth_handle, positions, num_particles);
// Move a particle.
r3SoftBody_SetParticlePosition(cloth_handle, 1, r3VectorAdd(position, r3Vector(0.0, 0.1, 0.0)));
r3SoftBody_SetParticleVelocity(cloth_handle, 1, velocity);
// Pin (or release) a particle; a pinned particle can be driven like a kinematic body.
r3SoftBody_SetParticlePinned(cloth_handle, 2, 1);
r3SoftBody_SetParticleKinematicTarget(cloth_handle, 2, r3Vector(-1.0, 2.5, -0.8));
// The elements: edges, cells and the boundary triangles, as flat arrays of particle indices
// (2, 4, and 3 indices per element). A NULL buffer with a zero capacity gives their length.
size_t num_edges = r3SoftBody_Edges(cloth_handle, NULL, 0) / 2;
size_t num_cells = r3SoftBody_Cells(cloth_handle, NULL, 0) / 4;
size_t boundary_len = r3SoftBody_Boundary(cloth_handle, NULL, 0);
uint32_t *boundary = malloc(boundary_len * sizeof(uint32_t));
r3SoftBody_Boundary(cloth_handle, boundary, boundary_len);
assert(num_edges > 0 && num_cells == 0 && boundary_len > 0);
free(positions);
free(boundary);
Setting the position of a particle explicitly teleports it: no contact is taken into account along the way, so a particle can be moved inside of another object this way. Whenever the motion must be seen by the contacts and by the friction (to drag a piece of cloth, for example), it is recommended to pin the particle and to give it a kinematic target instead.
Controlling a region kinematically
A whole region of the body is controlled at once through a cluster covering it. Pinning
the cluster (r3SoftBody_SetClusterPinned) pins all of its particles, and its kinematic target
(r3SoftBody_SetClusterKinematicTarget) moves them
rigidly: each pinned particle is sent where the rest shape of the cluster places it at the target pose, with the
matching velocity. The rest of the body is then simulated as usual, and drags behind the controlled region, e.g., the
hand of a soft character carrying something:
- Example 2D
- Example 3D
// Pin every particle of the cluster, then move it along a path: the cluster behaves like a
// kinematic rigid part dragging the rest of the body.
r2SoftBody_SetClusterPinned(jelly_handle, cluster, 1);
r2SoftBody_SetClusterKinematicTarget(jelly_handle, cluster, r2TranslationPose(r2Vector(3.0, 2.5)));
// Release it: the cluster is simulated again.
r2SoftBody_SetClusterPinned(jelly_handle, cluster, 0);
// Pin every particle of the cluster, then move it along a path: the cluster behaves like a
// kinematic rigid part dragging the rest of the body.
r3SoftBody_SetClusterPinned(jelly_handle, cluster, 1);
r3SoftBody_SetClusterKinematicTarget(jelly_handle, cluster, r3TranslationPose(r3Vector(3.0, 2.0, 0.0)));
// Release it: the cluster is simulated again.
r3SoftBody_SetClusterPinned(jelly_handle, cluster, 0);
Deformable colliders and skinning
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 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
the skinCollision field of R3SoftBodyDesc.
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);
A skin doesn't need a computed cage: any mesh can be given as the skin of a body built with cells, with
r3SoftBodyDesc_SetSkin. 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
(r3InsertDeformableCollider). 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 the frame of that proxy, 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
(R3SoftMeshBindingDesc):
R3_SOFT_BINDING_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_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 (R3_SOFT_BINDING_DIRECT_BY_POSITION) is useful when the mesh is the one the particles were built from.
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);
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.
Plasticity and tearing
A soft-body can deform permanently in two ways:
- Plasticity changes the rest shape of the body, without any change of its topology: a metal sheet folding on impact, a piece of clay being modeled, the chassis of a car denting.
- Tearing changes its topology: pieces of the body physically disconnect from each other, e.g., a piece of fabric torn in two, or a jelly sliced by a blade.
Both are supported by the constraints solver and by the FEM solver. Note that with the constraints solver, the quality of the plastic deformations follows the convergence of the solver: more iterations result in more convincing permanent deformations.
Plasticity
Plasticity is configured by the material of the body, separately for its cells and for its edges:
- A cell strained past its plastic yield
(
plasticYield) absorbs the strain in excess into its rest shape, at the rate of its plastic creep (plasticCreep, per second), up to a total permanent deformation of its plastic max (plasticMax). This flow preserves the volume of the cell, and an inverted cell never flows. Note that this only applies to the elastic cells (theR3_SOFT_CELL_COROTATIONALandR3_SOFT_CELL_NEO_HOOKEANmodels): theR3_SOFT_CELL_VOLUMEcells never flow. - An edge strained past its edge plastic yield
(
edgePlasticYieldcompared to|length / rest_length - 1|) sees its rest length flow toward its current length at the rate of its edge plastic creep (edgePlasticCreep), up to a total permanent set of its edge plastic max (edgePlasticMax, as a fraction of its initial length). Its edge plastic flow (edgePlasticFlow, aSoftEdgePlasticFlow) selects whether that happens when it is squeezed, when it is stretched, or both.
A plastic deformation can be undone at any time
(r3SoftBody_ResetPlasticity),
the particles springing back elastically from there. Note that the tear thresholds of the edges are always measured on
their initial length, not on their plastic one:
The material is read with r3SoftBody_Material, which gives back a copy of the material of the body. That copy is
modified, then applied with r3SoftBody_SetMaterial:
- Example 2D
- Example 3D
// The jelly has elastic (corotational) cells: the plasticity of volume cells has no effect.
R2SoftBodyMaterial plastic_material = r2SoftBody_Material(jelly);
// Cells: the rest shape flows toward the current one past 5% strain, at a rate of 20 per
// second, up to a total permanent deformation of 50%.
plastic_material.plasticYield = 0.05;
plastic_material.plasticCreep = 20.0;
plastic_material.plasticMax = 0.5;
// Edges: the rest length flows past 10% strain, up to half the initial length, but only
// when squeezed (a dent stays, a stretch springs back).
plastic_material.edgePlasticYield = 0.1;
plastic_material.edgePlasticCreep = 10.0;
plastic_material.edgePlasticMax = 0.5;
plastic_material.edgePlasticFlow = R2_SOFT_EDGE_PLASTIC_FLOW_COMPRESSION;
r2SoftBody_SetMaterial(jelly, &plastic_material);
// Every permanent deformation can be undone at once.
r2SoftBody_ResetPlasticity(jelly);
// The jelly has elastic (corotational) cells: the plasticity of volume cells has no effect.
R3SoftBodyMaterial plastic_material = r3SoftBody_Material(jelly);
// Cells: the rest shape flows toward the current one past 5% strain, at a rate of 20 per
// second, up to a total permanent deformation of 50%.
plastic_material.plasticYield = 0.05;
plastic_material.plasticCreep = 20.0;
plastic_material.plasticMax = 0.5;
// Edges: the rest length flows past 10% strain, up to half the initial length, but only
// when squeezed (a dent stays, a stretch springs back).
plastic_material.edgePlasticYield = 0.1;
plastic_material.edgePlasticCreep = 10.0;
plastic_material.edgePlasticMax = 0.5;
plastic_material.edgePlasticFlow = R3_SOFT_EDGE_PLASTIC_FLOW_COMPRESSION;
r3SoftBody_SetMaterial(jelly, &plastic_material);
// Every permanent deformation can be undone at once.
r3SoftBody_ResetPlasticity(jelly);
Tearing
Tearing is configured by the material of the body as well. An element tears at the end of the timestep during which its load goes beyond one of the two thresholds of the material:
- The tear strain (
tearStrain) applies to the edges (a fraction of their initial rest length) and to the elastic cells (their largest tensile strain). Note that volume cells never tear. - The tear force (
tearForce) applies to the edges only: an edge tears if its force along its direction exceeds it.
The other settings of the material shape how a tear propagates:
- The tear smoothing (
tearSmoothing) is the time constant (in seconds) over which the load of an element is smoothed before being tested, so that a single impact spike doesn't tear. - The interior strength (
interiorStrength) makes the undamaged interior elements (without any particle on the surface or on an earlier tear) that many times tougher, so that tears start from the surface or from an existing damage, and run inward. - The max tears per step (
maxTearsPerStep) bounds how many edges may tear during one step, the most loaded going first, which paces the cracks of a taut sheet (an edge loaded past twice its threshold always tears). - The min piece (
minPiece) is the smallest piece (in elements) a tear may split off, any tear leaving a smaller piece waiting until it doesn't.
Individual edges can be made tougher (or weaker, e.g., a perforation line) with their tear resistance, given to the
builder
(the edgeTearResistance field of R3SoftBodyDesc)
or by cluster:
The optional thresholds of the material (tearStrain, tearForce, and minPiece) are only used when the enabled
field of their R3OptionalReal or R3OptionalU32 is set. The tear resistance of the edges and of the clusters can
also be changed after the insertion, with r3SoftBody_SetEdgeTearResistance and r3SoftBody_SetClusterTearResistance:
- Example 2D
- Example 3D
R2SoftBodyMaterial tear_material = r2SoftBody_Material(sheet);
// An edge tears past 40% of stretch, or past a force of 50 along its direction.
tear_material.tearStrain = (R2OptionalReal){1, 0.4};
tear_material.tearForce = (R2OptionalReal){1, 50.0};
// The load is smoothed over 0.1 second, so a single impact spike doesn't tear.
tear_material.tearSmoothing = 0.1;
// Undamaged interior elements are twice as tough: tears start from the surface.
tear_material.interiorStrength = 2.0;
// A tear never splits off a piece smaller than 10 elements.
tear_material.minPiece = (R2OptionalU32){1, 10};
r2SoftBody_SetMaterial(sheet, &tear_material);
R3SoftBodyMaterial tear_material = r3SoftBody_Material(cloth);
// An edge tears past 40% of stretch, or past a force of 50 along its direction.
tear_material.tearStrain = (R3OptionalReal){1, 0.4};
tear_material.tearForce = (R3OptionalReal){1, 50.0};
// The load is smoothed over 0.1 second, so a single impact spike doesn't tear.
tear_material.tearSmoothing = 0.1;
// Undamaged interior elements are twice as tough: tears start from the surface.
tear_material.interiorStrength = 2.0;
// A tear never splits off a piece smaller than 10 elements.
tear_material.minPiece = (R3OptionalU32){1, 10};
r3SoftBody_SetMaterial(cloth, &tear_material);
A tear can also be requested explicitly, either edge by edge
(r3SoftBody_TearEdge, r3SoftBody_TearCell),
or all at once along a set of edges and through a set of cells
(r3SoftBody_Tear).
Finally, a body can be cut
(r3CutSoftBody)
along a blade, i.e., a segment in 2D or a triangle in 3D, which is the most convenient way of slicing a body with the
weapon of a player. Note that the cuts ignore the min piece threshold.
Tearing and cutting lose no material: the particles are duplicated along the tear instead of being removed, so the area (2D) or the volume (3D) of the body is preserved. The pieces a tear disconnects become soft-bodies of their own, which keep the material and the settings of the body they come from, the deformable meshes and the joints following the pieces they were attached to. Therefore the particles of the torn body are renumbered, and the returned event tells where each of them went:
The event returned by r3SoftBody_Tear and r3CutSoftBody is an owned R3SoftBodyTearEvent, to be freed with
r3FreeSoftBodyTearEvent, or NULL if the tear or the cut changed nothing. It is read with the following functions:
r3SoftBodyTearEvent_SoftBodygives the torn soft-body, andr3SoftBodyTearEvent_Bodiesthe soft-bodies it is now made of: the torn body alone if nothing was split off, or its pieces otherwise, the piece keeping the handle of the torn body first (r3SoftBodyTearEvent_PieceCountgives their number). The particles of thei-th of them (i.e., their indices in the torn body) are given byr3SoftBodyTearEvent_PieceParticles.r3SoftBodyTearEvent_TryParticleDestinationtells in which soft-body a particle of the torn body is now, and what its index is there (r3SoftBodyTearEvent_ParticleDestinationdoes the same, but reports the particles without any destination as anR3_NOT_FOUNDerror).r3SoftBodyTearEvent_TornEdges,r3SoftBodyTearEvent_TornCells,r3SoftBodyTearEvent_RemovedEdges,r3SoftBodyTearEvent_SplitParticles, andr3SoftBodyTearEvent_InsertedParticlesgive the details of the change of topology, as flat arrays of particle indices.r3SoftBodyTearEvent_Clustersandr3SoftBodyTearEvent_MovedJointsgive the clusters the tear split, and the joints it moved from a cluster proxy to another.
The arrays are copied with the usual output-buffer protocol: each function returns the number of elements, and copies
them into the given buffer only if its capacity is large enough (a NULL buffer with a capacity of zero gives that
number first). Finally, r3SoftBody_TopologyVersion changes whenever the
connectivity of the particles of a body changes, which is a convenient way of knowing when the render meshes must be
rebuilt:
- Example 2D
- Example 3D
// Elements tear on their own past the material's thresholds; a tear can also be requested.
r2SoftBody_TearEdge(sheet, 10); // Applied at the end of the next step.
// Tear at once along edges and through cells; pieces the tear disconnects become soft
// bodies of their own. The event is NULL if nothing changed.
uint32_t torn_edges[] = {11, 12};
R2SoftBodyTearEvent *tear = r2SoftBody_Tear(sheet, torn_edges, 2, NULL, 0);
if (tear != NULL) {
printf("%zu edges torn\n", r2SoftBodyTearEvent_TornEdges(tear, NULL, 0) / 2);
r2FreeSoftBodyTearEvent(tear);
}
// Cut along a blade (a segment in 2D), without removing material.
R2Vector blade[2] = {{-3.0, -10.0}, {-3.0, 10.0}};
R2SoftBodyTearEvent *cut = r2CutSoftBody(sheet, blade);
if (cut != NULL) {
// The soft-bodies the sheet is now made of, the one keeping its handle first.
size_t num_pieces = r2SoftBodyTearEvent_PieceCount(cut);
R2SoftBodyHandle *pieces = malloc(num_pieces * sizeof(R2SoftBodyHandle));
r2SoftBodyTearEvent_Bodies(cut, pieces, num_pieces);
for (size_t i = 0; i < num_pieces; i++) {
size_t num_piece_particles = r2SoftBodyTearEvent_PieceParticles(cut, i, NULL, 0);
printf("piece %u has %zu particles\n", pieces[i].index, num_piece_particles);
}
free(pieces);
// Where a particle of the torn body went.
R2OptionalParticleDestination destination = r2SoftBodyTearEvent_TryParticleDestination(cut, n * n - 1);
if (destination.found) {
printf("particle %u is now particle %u of %u\n", n * n - 1, destination.index,
destination.body.index);
}
r2FreeSoftBodyTearEvent(cut);
}
// Elements tear on their own past the material's thresholds; a tear can also be requested.
r3SoftBody_TearEdge(cloth, 10); // Applied at the end of the next step.
// Tear at once along edges and through cells; pieces the tear disconnects become soft
// bodies of their own. The event is NULL if nothing changed.
uint32_t torn_edges[] = {11, 12};
R3SoftBodyTearEvent *tear = r3SoftBody_Tear(cloth, torn_edges, 2, NULL, 0);
if (tear != NULL) {
printf("%zu edges torn\n", r3SoftBodyTearEvent_TornEdges(tear, NULL, 0) / 2);
r3FreeSoftBodyTearEvent(tear);
}
// Cut along a blade (a triangle in 3D), without removing material.
R3Vector blade[3] = {{-0.1, -10.0, -10.0}, {-0.1, 10.0, 0.0}, {-0.1, -10.0, 10.0}};
R3SoftBodyTearEvent *cut = r3CutSoftBody(cloth, blade);
if (cut != NULL) {
// The soft-bodies the cloth is now made of, the one keeping its handle first.
size_t num_pieces = r3SoftBodyTearEvent_PieceCount(cut);
R3SoftBodyHandle *pieces = malloc(num_pieces * sizeof(R3SoftBodyHandle));
r3SoftBodyTearEvent_Bodies(cut, pieces, num_pieces);
for (size_t i = 0; i < num_pieces; i++) {
size_t num_piece_particles = r3SoftBodyTearEvent_PieceParticles(cut, i, NULL, 0);
printf("piece %u has %zu particles\n", pieces[i].index, num_piece_particles);
}
free(pieces);
// Where a particle of the torn body went.
R3OptionalParticleDestination destination = r3SoftBodyTearEvent_TryParticleDestination(cut, n * n - 1);
if (destination.found) {
printf("particle %u is now particle %u of %u\n", n * n - 1, destination.index,
destination.body.index);
}
r3FreeSoftBodyTearEvent(cut);
}
Tearing one edge with
r3SoftBody_TearEdge
only marks it: the tear is applied at the end of the next step, together with the tears the simulation generates itself.
The methods of the
world (r3SoftBody_Tear and r3CutSoftBody)
tear and cut immediately, which is why they are the ones giving back an event.
Volume cells never tear. Therefore a body which cells use the R3_SOFT_CELL_VOLUME model
will only tear along its edges, and a material with a tear strain should be combined with the
R3_SOFT_CELL_COROTATIONAL or the R3_SOFT_CELL_NEO_HOOKEAN
cell model if you expect it to be torn apart.
Tear events
The tears applied during a step, whether they were generated by the simulation itself or requested with
r3SoftBody_TearEdge,
are reported the same way as the
collision events: by giving an event collector (R3EventCollector) to r3Step.
Each event (an R3SoftBodyTearEvent) identifies the soft-body that tore and gives the
pieces it was split into, so the rendering of the scene can be updated accordingly:
The collector keeps the events of every step it is given to, until it is emptied with r3EventCollector_Clear. The
tear events are counted by r3EventCollector_TearEventCount, and r3EventCollector_TearEvent returns an owned copy of
one of them, to be freed with r3FreeSoftBodyTearEvent. This is the same R3SoftBodyTearEvent as the one returned by
r3SoftBody_Tear and r3CutSoftBody, so all the accessors described in the
previous section apply to it as well:
- Example 2D
- Example 3D
// Tears applied during a step are reported through the event collector.
R2EventCollector *events = r2NewEventCollector();
r2Step(world, NULL, events);
size_t num_tear_events = r2EventCollector_TearEventCount(events);
for (size_t i = 0; i < num_tear_events; i++) {
// An owned copy of the event.
R2SoftBodyTearEvent *tear_event = r2EventCollector_TearEvent(events, i);
R2SoftBodyHandle torn = r2SoftBodyTearEvent_SoftBody(tear_event);
printf("Soft body %u tore\n", torn.index);
r2FreeSoftBodyTearEvent(tear_event);
}
// The collector keeps its events until it is cleared.
r2EventCollector_Clear(events);
// Tears applied during a step are reported through the event collector.
R3EventCollector *events = r3NewEventCollector();
r3Step(world, NULL, events);
size_t num_tear_events = r3EventCollector_TearEventCount(events);
for (size_t i = 0; i < num_tear_events; i++) {
// An owned copy of the event.
R3SoftBodyTearEvent *tear_event = r3EventCollector_TearEvent(events, i);
R3SoftBodyHandle torn = r3SoftBodyTearEvent_SoftBody(tear_event);
printf("Soft body %u tore\n", torn.index);
r3FreeSoftBodyTearEvent(tear_event);
}
// The collector keeps its events until it is cleared.
r3EventCollector_Clear(events);
Forces and impulses
Forces and impulses can be applied to a soft-body as a whole
(r3SoftBody_AddForce, r3SoftBody_ApplyImpulse),
or to one particular particle
(r3SoftBody_AddParticleForce, r3SoftBody_ApplyParticleImpulse).
A force added to the whole body is added to each of its free particles and is persistent, i.e., it keeps being applied
at each step until the forces are reset (r3SoftBody_ResetForces), exactly like the forces of
a rigid-body. An impulse applied to the whole body is a velocity change
applied to each of its free particles, so the body is kicked as a whole without being deformed. Note that the pinned
particles ignore both.
Two additional methods are provided for the effects which magnitude depends on the distance to a point: an impulse
applied within a radius
(r3SoftBody_ApplyImpulseAtPoint), and a
radial blast pushing the particles away from its center
(r3SoftBody_ApplyRadialImpulse). In both
cases the impulse is scaled linearly down to zero at the given radius. Like for the rigid-bodies,
the last boolean argument of all these methods ensures the soft-body is
awake before the force or the impulse is applied:
- Example 2D
- Example 3D
// The last argument set to 1 makes sure the soft-body is awake.
r2SoftBody_ResetForces(sheet, 1); // Reset the forces to zero.
r2SoftBody_AddForce(sheet, r2Vector(0.0, 1.0), 1); // Added to the force of each particle.
r2SoftBody_AddParticleForce(sheet, 3, r2Vector(0.0, 1.0), 1);
r2SoftBody_ApplyImpulse(sheet, r2Vector(0.0, 0.1), 1);
r2SoftBody_ApplyParticleImpulse(sheet, 3, r2Vector(0.0, 0.1), 1);
// An impulse on the particles within 0.5 of a point, scaled down with the distance.
r2SoftBody_ApplyImpulseAtPoint(sheet, r2Vector(0.0, 0.1), r2Vector(-3.0, 3.0), 0.5, 1);
// A blast pushing the particles away from a center.
r2SoftBody_ApplyRadialImpulse(sheet, r2Vector(-3.0, 3.0), 0.1, 1.0, 1);
// The last argument set to 1 makes sure the soft-body is awake.
r3SoftBody_ResetForces(cloth, 1); // Reset the forces to zero.
r3SoftBody_AddForce(cloth, r3Vector(0.0, 1.0, 0.0), 1); // Added to the force of each particle.
r3SoftBody_AddParticleForce(cloth, 3, r3Vector(0.0, 1.0, 0.0), 1);
r3SoftBody_ApplyImpulse(cloth, r3Vector(0.0, 0.1, 0.0), 1);
r3SoftBody_ApplyParticleImpulse(cloth, 3, r3Vector(0.0, 0.1, 0.0), 1);
// An impulse on the particles within 0.5 of a point, scaled down with the distance.
r3SoftBody_ApplyImpulseAtPoint(cloth, r3Vector(0.0, 0.1, 0.0), r3Vector(0.0, 2.0, 0.0), 0.5, 1);
// A blast pushing the particles away from a center.
r3SoftBody_ApplyRadialImpulse(cloth, r3Vector(0.0, 2.0, 0.0), 0.1, 1.0, 1);
Global settings
A few settings are shared by every soft-body of the world. They are part of the integration
parameters (the softBodies field of R3IntegrationParameters), so they can
be changed between two steps:
- The re-sweep strain
(
r3SoftBodiesSetResweepStrain) is the strain beyond which a constraint is solved once more after the contacts of every substep. This is what keeps a light body buried under heavier ones from being torn apart by them. - The maximum number of extra substeps
(
r3SoftBodiesSetMaxExtraSubsteps) is the number of substeps a soft-body is allowed to ask for while it is hit fast. Those substeps bound the motion of its particles, and the motion of the rigid-bodies approaching them, by about one particle radius per substep. Giving zero disables them. - The contact stiffening
(
r3SoftBodiesSetContactStiffening) multiplies the natural frequencies of the contacts involving a soft-body. Those contacts run stiffer than the rigid ones because the mass behind one contact is the mass of a few particles, and not the mass of the whole body.
In addition, the detection of the penetrations and of the tangles, as well as the way the bodies recover from them, are
configured by the recovery settings (recovery, an R3SoftRecoverySettings), where every mechanism can be switched
off individually (see the contacts section for the ones to look at
first). The tuning of the linear solves of the FEM solver lives there as well (fem, an R3SoftFemParameters):
Each of these settings has its own getter and setter, named after its field: e.g., r3SoftBodiesResweepStrain and
r3SoftBodiesSetResweepStrain, r3RecoveryCrossingRepulsion and r3RecoverySetCrossingRepulsion, or
r3FemLinearTolerance and r3FemSetLinearTolerance. All of them can also be read at once with
r3IntegrationParameters, which returns a copy of the integration parameters of the world: that copy is modified, then
written back with r3SetIntegrationParameters. Note that the fem field, as well as the r3Fem* functions, only
exist when the library is built with the fem feature:
- Example 2D
- Example 3D
// Settings shared by every soft-body of the world.
// Strain beyond which a constraint is re-solved after the contacts of every substep.
// Default: 0.75
r2SoftBodiesSetResweepStrain(world, 0.75);
// Extra substeps a soft-body requests while it is hit fast; 0 disables them.
// Default: 4
r2SoftBodiesSetMaxExtraSubsteps(world, 4);
// Stiffening of the soft-body contacts relative to the rigid ones.
// Default: 4.0
r2SoftBodiesSetContactStiffening(world, 4.0);
// The tangle detection and recovery stack can be switched off mechanism by mechanism.
r2RecoverySetCrossingRepulsion(world, 1);
// The settings can also be read all at once (as a copy), modified, and written back.
R2IntegrationParameters params = r2IntegrationParameters(world);
params.softBodies.recovery.selfStandDown = 1;
r2SetIntegrationParameters(world, ¶ms);
// Settings shared by every soft-body of the world.
// Strain beyond which a constraint is re-solved after the contacts of every substep.
// Default: 0.75
r3SoftBodiesSetResweepStrain(world, 0.75);
// Extra substeps a soft-body requests while it is hit fast; 0 disables them.
// Default: 4
r3SoftBodiesSetMaxExtraSubsteps(world, 4);
// Stiffening of the soft-body contacts relative to the rigid ones.
// Default: 4.0
r3SoftBodiesSetContactStiffening(world, 4.0);
// The tangle detection and recovery stack can be switched off mechanism by mechanism.
r3RecoverySetCrossingRepulsion(world, 1);
// The settings can also be read all at once (as a copy), modified, and written back.
R3IntegrationParameters params = r3IntegrationParameters(world);
params.softBodies.recovery.selfStandDown = 1;
r3SetIntegrationParameters(world, ¶ms);
Removal
Removing a soft-body (r3RemoveSoftBody) removes everything the engine created for it: its root
body, the proxies of its clusters, the colliders of its surface and of its deformable meshes, as well as the joints
attached to any of them. One cluster can also be removed on its own
(r3SoftBody_RemoveCluster):
- Example 2D
- Example 3D
// Removing a soft-body removes its root body, its proxies, its colliders and the joints
// attached to them.
r2RemoveSoftBody(rope_handle);
// A cluster can be removed on its own.
r2SoftBody_RemoveCluster(jelly_handle, cluster);
// Removing a soft-body removes its root body, its proxies, its colliders and the joints
// attached to them.
r3RemoveSoftBody(rope_handle);
// A cluster can be removed on its own.
r3SoftBody_RemoveCluster(jelly_handle, cluster);
Removing a cluster also removes the particles that only this cluster covered, with their elements and their
attachments. Therefore removing the last cluster of a soft-body removes the soft-body itself. Note that removing the
proxy of a cluster with r3RemoveRigidBody is equivalent to removing the cluster.