Stretch
Package @ferret/physics-stretch, capability stretch. It runs last in each step, after the rigid bodies have moved and after breakage.
What it is for#
Anything that gives: a spring, a bungee, a rope that sags and may snap, a flag, a curtain, a tarp over a crate, a cube of jelly. Three tools, from cheapest to most expensive:
- Links: springs, ropes and bands between two bodies. Elastic at first, then permanently bent past a yield point, then broken past a limit.
- Cloth: a sheet of particles, pinned where you say, draped over colliders, torn if pulled too hard.
- Soft bodies: a closed skin that keeps its volume, so a ball of jelly squashes and bulges rather than collapsing.
How it works#
Links are springs inside the rigid-body kernel. The capability keeps their rest length and stiffness up to date as they strain.
Cloth and soft bodies are not kernel bodies. They are particles advanced by their own solver with a fixed step and a fixed number of passes, so a replay is identical. They meet the world through colliders you give them (spheres, planes, boxes and rigid bodies) and read gravity and wind from the host.
A spring#
import { PhysicsHost, LightKernel } from '@ferret/physics';
import { stretch } from '@ferret/physics-stretch';
const cap = stretch();
const host = world.addService(new PhysicsHost({ kernel: new LightKernel(), capabilities: [cap] }));
host.enable('stretch');
const anchor = host.kernel.addBody({ shape: { type: 'sphere', radius: 0.05 }, kind: 'static', position: [0, 0, 0] });
const bob = host.kernel.addBody({ shape: { type: 'sphere', radius: 0.05 }, mass: 2, position: [1.1, 0, 0] });
cap.link({ a: anchor, b: bob, length: 1, k: 200 }); // stiffness in N/m
The bob oscillates with the period 2π√(m/k), as a real spring does.
Ropes#
cap.rope builds a chain of beads joined by links. Give it Young's modulus E (in pascals) and the cross-section area (m²), and the whole rope has the stiffness E·A/L, so a load stretches it by F·L/(E·A). A soft rope stretches visibly under a weight; a stiff one hardly at all.
const rope = cap.rope({ from: [0, 0, 0], to: [0, -2, 0], segments: 8, mass: 0.5,
E: 1e7, area: 1e-4, damping: 30, anchorA: anchor, radius: 0.02 });
Hang the rope from a static or kinematic anchor. Leave anchorB out for a free end. By default a rope link does not push: when the ends come closer than the rest length it carries no force, so the rope sags instead of acting like a rod (rope: false makes it a rod).
Bending and breaking#
A link's strain is (L − L₀) / L₀.
| Strain | What happens |
|---|---|
below yieldStrain | elastic: it springs back |
| above it | plastic: the rest length grows, a permanent set; hardening makes it harder to bend further (0 means perfectly plastic) |
above breakStrain | it breaks and the link disappears |
Without yieldStrain a link is purely elastic; without breakStrain it never breaks. The events are stretch:yield (the first time a link goes plastic) and stretch:break, each carrying the link's name, so name the links you care about. For orientation: a bungee stays elastic up to 100 % strain, a copper wire yields at 0.2 %, a rubber band breaks at 400 %.
Cloth#
A cloth is a grid of nu × nv particles, spacing apart, with a total mass. pinned: 'top' holds the first row (a curtain, a flag on a pole), 'corners' the two top corners, or you can list particle indices.
import { Cloth } from '@ferret/physics-stretch';
const cloth = new Cloth({ nu: 20, nv: 20, spacing: 0.05, mass: 0.4, pinned: 'top',
origin: [0, 0, 0], u: [1, 0, 0], v: [0, -1, 0],
strainLimit: 1.05, iterations: 6, substeps: 2 });
cloth.step(1 / 60);
To drape it over things, push colliders onto cloth.colliders: a sphere, a plane, a box, or { type: 'body', body } for any rigid body (spheres, capsules, cylinders, compounds, convex hulls, closed meshes). Particles keep thickness away from the surface. friction says how much the cloth grips what it lies on.
The dials, in the order you will need them:
| Dial | What it does |
|---|---|
stiffness (0–1) | how firmly the weave is held: lower is stretchier |
bending | 0 a flexible sheet, 1 a stiff plate |
strainLimit | no link gets longer than rest × this; 1.05 reads as cloth, 1.5 as rubber |
damping, drag | energy loss, and resistance to wind |
iterations, substeps | quality against cost; 6 and 2 hold a 400-particle curtain within 5 % |
tearStrain | 0.15 tears a link stretched by 15 %; leave it out and the cloth never tears |
Move a pinned particle with cloth.move(i, p) (a hand carrying a flag), pin or release with pin(i) and release(i). cloth.onTear and cloth.torn report tears.
Cloth in a world: budget and wind#
cap.addCloth(name, def) registers a cloth with the capability. It is stepped every fixed step with the host's gravity and the wind value.
The particle budget (particleBudget, 4000 across all cloths by default) is spent whole cloths at a time: a cloth that does not fit is not stepped in that frame, never half of it. cap.particles() returns how many were asked for and how many stepped. The tier table's clothParticles is the number to hand the budget (see budgets).
Soft bodies#
A SoftBody is a closed skin of particles held by its edges and by one constraint on the volume it encloses. It squashes and bulges instead of losing volume. pressure is the volume it keeps as a share of its start volume: 1 is a closed ball of water, more is a blown-up balloon. It collides with planes in floors.
import { SoftBody } from '@ferret/physics-stretch';
const jelly = new SoftBody({ centre: [0, 0.6, 0], radius: 0.3, mass: 2, stiffness: 0.3, pressure: 1 });
jelly.floors.push({ normal: [0, 1, 0], offset: 0 });
jelly.step(1 / 60);
Parameters#
| Value (scoped) | Default | Effect |
|---|---|---|
particleBudget | 4000 | most cloth particles stepped per frame, all cloths together |
wind | [0, 0, 0] | wind velocity on cloth, m/s |
| Link setting | Meaning |
|---|---|
k, or E and area | stiffness in N/m, or Young's modulus and section |
length | rest length; the present distance if omitted |
damping | N·s/m |
rope | no force when slack |
yieldStrain, hardening, breakStrain | see above |
name | appears in events |
Commands#
stretch.info (links and cloths: strain, plastic set, force, the particle budget) and cloth.pin { cloth, index, pinned }.
Works with#
- Links need a kernel; they are kernel springs. On the light kernel they are the main way to join bodies; on Havok, joints (
hinge,ball,fixed) are in bodies and kernels. - Cloth reads the
gravitycapability's field when that is on. It does not read the wind ofmedia: stretch has its ownwind. - strength breaks rigid joints by force; links here break by strain. Use links for things that stretch and rigid constraints for things that hold.
Cost#
Links cost one pass per step. Cloth costs particles × iterations × substeps: a 20×20 curtain at 6 iterations and 2 substeps is about half a millisecond on desktop. When time runs short the governor's last resort is to stop cloth; lower iterations before that. Use real cloth for what the player sees up close and fake distant flags with a shader.
When it does not behave#
| Symptom | Cause |
|---|---|
| A cloth does not move | it is over the particle budget (cap.particles()), stretch is off, or you step it by hand and did not call step |
| Cloth stretches like rubber | raise stiffness, lower strainLimit (1.05) |
| Cloth passes through a body | the collider is not in cloth.colliders, or thickness is smaller than the distance travelled in a step: raise substeps |
| A rope does not sag | rope: false (it is a rod), or it is too stiff for the load: lower E |
| A link never breaks | no breakStrain |
| Jelly collapses | pressure too low or stiffness 0 |
| Results differ between runs | a variable dt; always use the fixed step |
Worked numbers (spring period, rope stretch under load, curtain length, cloth over a sphere, budget, jelly volume) are in tests/unit/examples/physics-stretch.test.ts.
Reference#
stretch, StretchCapability, Link, makeRope, Cloth, ClothCollider, SoftBody, STRETCH_DEFAULTS.