soft_body_clusters
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
RigidBodyType::SoftFrameRigidBodyType.SoftFrameR3_SOFT_FRAME (see r3RigidBody_IsSoftFrame)RigidBodyType.SOFT_FRAME (see RigidBody.is_soft_frame)
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 SoftBody::root_bodyRapierSoftBody::root_bodySoftBody.rootBodyr3SoftBody_RootBodySoftBody.root_bodyCollider::deformable_mesh_refdeformable_mesh_ref of the Rapier colliderCollider.softBodyr3Collider_SoftBodyCollider.deformable_mesh_ref
- Example 2D
- Example 3D
// The rigid body the engine created for the whole soft body, read back after its insertion.
let root: RigidBodyHandle = world.soft_bodies[jelly_handle].root_body();
assert!(world.bodies[root].is_soft_frame());
// A rigid collider attached to it follows the frame of the whole body: here a sensor
// detecting what comes close to the jelly.
let _sensor = world.insert_collider(ColliderBuilder::ball(1.6).sensor(true), Some(root));
// 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.
let anchor = world.insert_body(RigidBodyBuilder::fixed().translation(Vector::new(3.0, 5.0)));
world.insert_impulse_joint(anchor, root, SpringJointBuilder::new(2.0, 60.0, 2.0));
// The rigid body the engine created for the whole soft body, read back after its insertion.
let root: RigidBodyHandle = world.soft_bodies[jelly_handle].root_body();
assert!(world.bodies[root].is_soft_frame());
// A rigid collider attached to it follows the frame of the whole body: here a sensor
// detecting what comes close to the jelly.
let _sensor = world.insert_collider(ColliderBuilder::ball(1.0).sensor(true), Some(root));
// 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.
let anchor =
world.insert_body(RigidBodyBuilder::fixed().translation(Vector::new(3.0, 4.0, 0.0)));
world.insert_impulse_joint(anchor, root, SpringJointBuilder::new(2.5, 60.0, 2.0));
- Example 2D
- Example 3D
// The rigid body the engine created for the whole soft body, read back after its insertion.
let rootBody = jelly.rootBody();
console.log("root body is a soft frame:", rootBody.isSoftFrame());
// A rigid collider attached to it follows the frame of the whole body: here a sensor
// detecting what comes close to the jelly.
world.createCollider(RAPIER.ColliderDesc.ball(1.6).setSensor(true), rootBody);
// 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.
let jellyAnchor = world.createRigidBody(RAPIER.RigidBodyDesc.fixed().setTranslation(3.0, 5.0));
let jellySpring = RAPIER.JointData.spring(2.0, 60.0, 2.0, { x: 0.0, y: 0.0 }, { x: 0.0, y: 0.0 });
world.createImpulseJoint(jellySpring, jellyAnchor, rootBody, true);
// The rigid body the engine created for the whole soft body, read back after its insertion.
let rootBody = jelly.rootBody();
console.log("root body is a soft frame:", rootBody.isSoftFrame());
// A rigid collider attached to it follows the frame of the whole body: here a sensor
// detecting what comes close to the jelly.
world.createCollider(RAPIER.ColliderDesc.ball(1.0).setSensor(true), rootBody);
// 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.
let jellyAnchor = world.createRigidBody(RAPIER.RigidBodyDesc.fixed().setTranslation(3.0, 4.0, 0.0));
let jellySpring = RAPIER.JointData.spring(
2.5, 60.0, 2.0,
{ x: 0.0, y: 0.0, z: 0.0 }, { x: 0.0, y: 0.0, z: 0.0 },
);
world.createImpulseJoint(jellySpring, jellyAnchor, rootBody, true);
The soft-body entity stands for its root body: an ImpulseJoint inserted on the soft-body entity,
or which parent is the soft-body entity, is attached to the root body, and so is a Collider inserted on a child
entity of the soft-body entity (such a child follows the pose of the root body, as explained in the
soft-bodies and entities section). The handle of the root body is
given by soft_body_whole_proxy:
- Example 2D
- Example 3D
// The soft-body entity stands for its root body. 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.
let anchor = commands
.spawn((Transform::from_xyz(3.0, 5.0, 0.0), RigidBody::Fixed))
.id();
commands.entity(jelly).insert(ImpulseJoint::new(
anchor,
SpringJointBuilder::new(2.0, 60.0, 2.0),
));
// A rigid collider on a child of the soft-body entity is attached to its root body: here a
// sensor detecting what comes close to the jelly.
commands
.entity(jelly)
.with_child((Transform::default(), Collider::ball(1.6), Sensor));
// The soft-body entity stands for its root body. 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.
let anchor = commands
.spawn((Transform::from_xyz(3.0, 4.0, 0.0), RigidBody::Fixed))
.id();
commands.entity(jelly).insert(ImpulseJoint::new(
anchor,
SpringJointBuilder::new(2.5, 60.0, 2.0),
));
// A rigid collider on a child of the soft-body entity is attached to its root body: here a
// sensor detecting what comes close to the jelly.
commands
.entity(jelly)
.with_child((Transform::default(), Collider::ball(1.0), Sensor));
- 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 rigid body the engine created for the whole soft body, read back after its insertion.
root = world.soft_bodies[jelly_handle].root_body
assert world.rigid_bodies[root].is_soft_frame
# A rigid collider attached to it follows the frame of the whole body: here a sensor
# detecting what comes close to the jelly.
sensor = world.add_collider(rp.Collider.ball(1.0).sensor(True), parent=root)
# 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.
anchor = world.add_body(rp.RigidBody.fixed(translation=(3.0, 4.0, 0.0)))
world.impulse_joints.insert(anchor, root, rp.SpringJointBuilder(2.5, 60.0, 2.0))
The pose of the root body is recomputed from the particles at each timestep, therefore moving it has no effect.
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 (PhysicsWorld::add_soft_body_clusterSoftBodyCluster
componentWorld.addSoftBodyClusterr3SoftBody_AddClusterPhysicsWorld.add_soft_body_cluster
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.
- Example 2D
- Example 3D
// A cluster over the top particles of the jelly: a rigid proxy that joints and
// colliders can attach to.
let top: Vec<u32> = {
let jelly = &world.soft_bodies[jelly_handle];
(0..jelly.num_particles() as u32)
.filter(|&i| jelly.particle_position(i as usize).y > 2.0)
.collect()
};
let cluster = world
.add_soft_body_cluster(jelly_handle, &top)
.expect("at least one valid particle");
let proxy: RigidBodyHandle = world.soft_bodies[jelly_handle]
.cluster_proxy(cluster)
.unwrap();
// A rigid plate welded onto the cluster.
let (plate, _) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(3.0, 2.4)),
ColliderBuilder::cuboid(1.2, 0.05).density(0.4),
);
world.insert_impulse_joint(
plate,
proxy,
FixedJointBuilder::new().local_anchor1(Vector::new(0.0, -0.1)),
);
// A cluster can be pinned, driven or tuned as a whole.
let jelly = &mut world.soft_bodies[jelly_handle];
jelly.set_cluster_stiffness_scale(cluster, 2.0);
jelly.enable_cluster_shape_matching(cluster, true);
// A cluster over the top particles of the jelly: a rigid proxy that joints and
// colliders can attach to.
let top: Vec<u32> = {
let jelly = &world.soft_bodies[jelly_handle];
(0..jelly.num_particles() as u32)
.filter(|&i| jelly.particle_position(i as usize).y > 1.3)
.collect()
};
let cluster = world
.add_soft_body_cluster(jelly_handle, &top)
.expect("at least one valid particle");
let proxy: RigidBodyHandle = world.soft_bodies[jelly_handle]
.cluster_proxy(cluster)
.unwrap();
// A rigid plate welded onto the cluster.
let (plate, _) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(3.0, 1.9, 0.0)),
ColliderBuilder::cuboid(0.7, 0.05, 0.7).density(0.4),
);
world.insert_impulse_joint(
plate,
proxy,
FixedJointBuilder::new().local_anchor1(Vector::new(0.0, -0.1, 0.0)),
);
// A cluster can be pinned, driven or tuned as a whole.
let jelly = &mut world.soft_bodies[jelly_handle];
jelly.set_cluster_stiffness_scale(cluster, 2.0);
jelly.enable_cluster_shape_matching(cluster, true);
- Example 2D
- Example 3D
// A cluster over the top particles of the jelly: a rigid proxy that joints and
// colliders can attach to.
let top = [];
for (let i = 0; i < jelly.numParticles(); ++i) {
if (jelly.particlePosition(i).y > 2.0) {
top.push(i);
}
}
let cluster = world.addSoftBodyCluster(jelly, top);
let proxy = jelly.clusterProxy(cluster);
// A rigid plate welded onto the cluster.
let plate = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(3.0, 2.4));
world.createCollider(RAPIER.ColliderDesc.cuboid(1.2, 0.05).setDensity(0.4), plate);
let weld = RAPIER.JointData.fixed({ x: 0.0, y: -0.1 }, 0.0, { x: 0.0, y: 0.0 }, 0.0);
world.createImpulseJoint(weld, plate, proxy, true);
// A cluster can be pinned, driven or tuned as a whole.
jelly.setClusterStiffnessScale(cluster, 2.0);
jelly.enableClusterShapeMatching(cluster, true);
// A cluster over the top particles of the jelly: a rigid proxy that joints and
// colliders can attach to.
let top = [];
for (let i = 0; i < jelly.numParticles(); ++i) {
if (jelly.particlePosition(i).y > 1.3) {
top.push(i);
}
}
let cluster = world.addSoftBodyCluster(jelly, top);
let proxy = jelly.clusterProxy(cluster);
// A rigid plate welded onto the cluster.
let plate = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(3.0, 1.9, 0.0));
world.createCollider(RAPIER.ColliderDesc.cuboid(0.7, 0.05, 0.7).setDensity(0.4), plate);
let weld = RAPIER.JointData.fixed(
{ x: 0.0, y: -0.1, z: 0.0 }, { w: 1.0, x: 0.0, y: 0.0, z: 0.0 },
{ x: 0.0, y: 0.0, z: 0.0 }, { w: 1.0, x: 0.0, y: 0.0, z: 0.0 },
);
world.createImpulseJoint(weld, plate, proxy, true);
// A cluster can be pinned, driven or tuned as a whole.
jelly.setClusterStiffnessScale(cluster, 2.0);
jelly.enableClusterShapeMatching(cluster, true);
A cluster is created by inserting a SoftBodyCluster component (containing the soft-body entity it
related to and the indices of the particles of the cluster) on another entity than the soft-body entity. The plugin then
inserts the RapierRigidBodyHandle of the proxy of the cluster on that entity, which can therefore be used like any rigid-body
entity by the ImpulseJoints, as well as by the Colliders inserted on its children. The pose of the proxy is written
back to the Transform of the cluster entity after each step, so its children follow every motion of the cluster.
Note that the cluster entity must not be given a RigidBody component, that modifying its Transform has no effect,
and that its SoftBodyCluster component is only read when the cluster is created:
- Example 2D
- Example 3D
// A cluster over the top particles of the jelly (their indices are read from its builder,
// in the local frame of the jelly entity).
let top: Vec<u32> = (0..)
.zip(jelly_body.builder.particle_positions())
.filter(|(_, p)| p.y > 0.8)
.map(|(i, _)| i)
.collect();
// A rigid plate welded onto the cluster.
let plate = commands
.spawn((
Transform::from_xyz(3.0, 2.4, 0.0),
RigidBody::Dynamic,
Collider::cuboid(1.2, 0.05),
ColliderMassProperties::Density(0.4),
))
.id();
// The cluster entity gets the proxy rigid-body of the cluster, which joints and colliders
// can be attached to like to any rigid-body.
commands.spawn((
PlateCluster,
SoftBodyCluster::new(jelly, top),
ImpulseJoint::new(
plate,
FixedJointBuilder::new().local_anchor1(Vec2::new(0.0, -0.1)),
),
// A cluster can be tuned as a whole.
SoftBodyClusterMaterial {
stiffness_scale: 2.0,
..default()
},
SoftBodyClusterShapeMatching::default(),
));
// A cluster over the top particles of the jelly (their indices are read from its builder,
// in the local frame of the jelly entity).
let top: Vec<u32> = (0..)
.zip(jelly_body.builder.particle_positions())
.filter(|(_, p)| p.y > 0.3)
.map(|(i, _)| i)
.collect();
// A rigid plate welded onto the cluster.
let plate = commands
.spawn((
Transform::from_xyz(3.0, 1.9, 0.0),
RigidBody::Dynamic,
Collider::cuboid(0.7, 0.05, 0.7),
ColliderMassProperties::Density(0.4),
))
.id();
// The cluster entity gets the proxy rigid-body of the cluster, which joints and colliders
// can be attached to like to any rigid-body.
commands.spawn((
PlateCluster,
SoftBodyCluster::new(jelly, top),
ImpulseJoint::new(
plate,
FixedJointBuilder::new().local_anchor1(Vec3::new(0.0, -0.1, 0.0)),
),
// A cluster can be tuned as a whole.
SoftBodyClusterMaterial {
stiffness_scale: 2.0,
..default()
},
SoftBodyClusterShapeMatching::default(),
));
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 is identified by its index in the soft-body, returned by add_soft_body_cluster (which returns None if
none of the given particles is valid), and its proxy is given by SoftBody.cluster_proxy. Snapshots of the live
clusters of a body (their index, particles, and proxy) are given by the clusters property of SoftBody, and the
cluster a proxy stands for by the soft_body and soft_cluster properties of its RigidBody:
# A cluster over the top particles of the jelly: a rigid proxy that joints and
# colliders can attach to.
positions = world.soft_bodies[jelly_handle].particle_positions
top = np.flatnonzero(positions[:, 1] > 1.3)
cluster = world.add_soft_body_cluster(jelly_handle, top)
assert cluster is not None, "at least one valid particle"
proxy = world.soft_bodies[jelly_handle].cluster_proxy(cluster)
# A rigid plate welded onto the cluster.
plate = world.add_body(
rp.RigidBody.dynamic(translation=(3.0, 1.9, 0.0)),
colliders=[rp.Collider.cuboid(0.7, 0.05, 0.7).density(0.4)],
)
world.impulse_joints.insert(
plate, proxy, rp.FixedJointBuilder().local_anchor1((0.0, -0.1, 0.0))
)
# A cluster can be pinned, driven or tuned as a whole.
jelly = world.soft_bodies[jelly_handle]
jelly.set_cluster_stiffness_scale(cluster, 2.0)
jelly.enable_cluster_shape_matching(cluster, True)
A cluster also defines a few settings for the elements it covers, which gives regional materials without needing separate bodies:
- The stiffness scale
(
set_cluster_stiffness_scaleSoftBodyClusterMaterial::stiffness_scalesetClusterStiffnessScaler3SoftBody_SetClusterStiffnessScale ) multiplies the Young modulus of every cell entirely contained in the cluster (the cells straddling its boundary are left unchanged).set_cluster_stiffness_scale - The edge softness
(
set_cluster_edge_softnessSoftBodyClusterMaterial::edge_softnesssetClusterEdgeSoftnessr3SoftBody_SetClusterEdgeSoftness ) overrides the softness of every edge entirely contained in the cluster, e.g., a stiffer collar on a shirt.set_cluster_edge_softness - The tear resistance
(
set_cluster_tear_resistanceSoftBodyClusterMaterial::tear_resistancesetClusterTearResistancer3SoftBody_SetClusterTearResistance ) multiplies the tear thresholds of every element entirely contained in the cluster, e.g., a tough region, or a perforation line.set_cluster_tear_resistance - Shape-matching (
enable_cluster_shape_matchingthe SoftBodyClusterShapeMatchingcomponentenableClusterShapeMatchingr3SoftBody_SetClusterShapeMatchingEnabled ) pulls the particles of the cluster toward the frame of its proxyenable_cluster_shape_matching(or toward the target pose given by SoftBodyCluster::set_shape_matching_target)(or toward the world-space targetof that component)(or toward the target pose given by r3SoftBody_SetClusterShapeMatchingTarget)(or toward the target pose given by , so that part of the body tends to keep the shape it was created with.set_cluster_shape_matching_target)
These cluster components, as well as the ones controlling a cluster kinematically, are applied again whenever they change, and can also be inserted on the soft-body entity itself in order to act on its whole-body cluster.
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.