Few visual elements in games feel as satisfying as a rope swinging realistically or a cape rippling in the wind. Rope and cloth physics appear throughout modern games — Spider-Man's webbing, Breath of the Wild's Paraglider, BioShock's hanging banners, the torn flags in Assassin's Creed. Behind all of them sits a surprisingly accessible algorithm that trades physical exactness for real-time stability: Position-Based Dynamics (PBD).
The Core Idea: Particles and Constraints
The key insight is to stop thinking about continuous elastic bodies and instead reason about discrete chunks. A rope becomes a chain of point masses connected by rigid-length constraints. A cloth becomes a grid of point masses connected by constraints in multiple directions. This approach was popularised by Thomas Jakobsen's 2001 paper Advanced Character Physics, written for IO Interactive's Hitman series, and has been a game-physics staple ever since.
The per-frame algorithm has three steps:
- Integrate: move each particle forward in time using its implicit velocity and gravity
- Constrain: nudge particles back to satisfy all length rules
- Repeat: run the constraint pass multiple times per frame to achieve stiffness
Those three steps, repeated over thousands of frames, produce behaviour that reads as physically convincing without explicitly solving any spring forces or differential equations.
Verlet Integration
Standard Euler integration tracks position and velocity as separate variables and advances each forward:
$$\vec{v}_{new} = \vec{v} + \vec{a} \cdot \Delta t, \quad \vec{p}_{new} = \vec{p} + \vec{v}_{new} \cdot \Delta t$$
Verlet integration instead encodes velocity implicitly in the difference between the current and previous positions. You never store velocity at all. The update rule is:
$$\vec{p}_{new} = 2\vec{p} - \vec{p}_{prev} + \vec{a} \cdot \Delta t^2$$
The term $2\vec{p} - \vec{p}_{prev}$ is equivalent to $\vec{p} + (\vec{p} - \vec{p}_{prev})$, where $(\vec{p} - \vec{p}_{prev})$ is the implicit velocity — exactly where the particle would go if nothing perturbed it. The acceleration term $\vec{a} \cdot \Delta t^2$ adds gravity on top. For a locked 60 fps loop, $\Delta t^2$ is folded into a small constant (e.g. -0.002 for the y component).
Why prefer Verlet over Euler? Two reasons. First, it is second-order accurate — error is $O(\Delta t^4)$ per step versus Euler's $O(\Delta t^2)$. Second, when the constraint solver teleports a particle to satisfy a length rule, it automatically zeroes the velocity component driving the violation. With explicit velocity you would need to correct velocity manually every time a constraint fires; with Verlet it just works.
A damping multiplier on the implicit velocity simulates air resistance and prevents the simulation from slowly gaining energy over time:
function integrateVerlet(particles, gravity, damping) {
for (const p of particles) {
if (p.locked) continue;
const vx = (p.pos.x - p.prev.x) * damping;
const vy = (p.pos.y - p.prev.y) * damping;
const vz = (p.pos.z - p.prev.z) * damping;
p.prev.copy(p.pos);
p.pos.x += vx;
p.pos.y += vy + gravity.y; // gravity.y is negative
p.pos.z += vz;
}
}Typical damping values range from 0.98 (very airy) to 0.999 (near-vacuum). Tuning this single parameter gives you everything from a heavy chain to a light silk ribbon.
Distance Constraints
After integration, particles have moved but may have violated their desired distance from neighbours. The constraint solver pushes them back. For two particles $p_1$ and $p_2$ with rest length $L$:
$$\vec{d} = p_2 - p_1, \quad \text{correction} = \frac{|\vec{d}| - L}{2 \cdot |\vec{d}|} \cdot \vec{d}$$
Apply the correction by moving $p_1$ forward and $p_2$ backward by the same vector. The factor of 2 in the denominator splits the error equally. If one particle is locked (pinned to the world), it absorbs none of the correction and the other takes it all:
function constrainDistance(p1, p2, restLength) {
const dx = p2.pos.x - p1.pos.x;
const dy = p2.pos.y - p1.pos.y;
const dz = p2.pos.z - p1.pos.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (dist < 0.0001) return;
const factor = (dist - restLength) / (dist * 2);
const cx = dx * factor, cy = dy * factor, cz = dz * factor;
if (!p1.locked) { p1.pos.x += cx; p1.pos.y += cy; p1.pos.z += cz; }
if (!p2.locked) { p2.pos.x -= cx; p2.pos.y -= cy; p2.pos.z -= cz; }
}A single pass through the constraint list is not enough. A correction at one end of a rope must ripple to the other end. Iterating the solver many times per frame (typically 10–30 iterations) achieves this propagation, producing a taut rope instead of a drooping spring:
function solveConstraints(constraints, iterations) {
for (let iter = 0; iter < iterations; iter++) {
for (const [p1, p2, restLength] of constraints) {
constrainDistance(p1, p2, restLength);
}
}
}From Rope to Cloth
Extending from a 1D rope to a 2D cloth replaces the chain with a grid and adds constraints in more directions. A particle at $(x, y)$ connects to neighbours via three constraint types:
- Structural: to $(x+1,\ y)$ and $(x,\ y+1)$ — resist stretching along the fabric axes
- Shear: to $(x+1,\ y+1)$ and $(x-1,\ y+1)$ diagonally — resist the cloth twisting sideways
- Bending: to $(x+2,\ y)$ and $(x,\ y+2)$ — resist sharp folding, tuning material stiffness
A cloth with only structural constraints collapses into unrealistic accordion folds. Adding shear constraints gives fabric-like behaviour. Bending constraints tune stiffness, weak for silk and strong for denim or leather.
function buildClothConstraints(particles, width, height, restLen) {
const constraints = [];
const diagLen = restLen * Math.sqrt(2);
const bendLen = restLen * 2;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = y * width + x;
// Structural
if (x < width - 1) constraints.push([particles[i], particles[i + 1], restLen]);
if (y < height - 1) constraints.push([particles[i], particles[i + width], restLen]);
// Shear
if (x < width-1 && y < height-1) {
constraints.push([particles[i], particles[i + width + 1], diagLen]);
constraints.push([particles[i + 1], particles[i + width], diagLen]);
}
// Bend
if (x < width - 2) constraints.push([particles[i], particles[i + 2], bendLen]);
if (y < height - 2) constraints.push([particles[i], particles[i + width * 2], bendLen]);
}
}
return constraints;
}Pinning and Interaction
Ropes and cloth are anchored by locked particles — particles never modified by integration or constraint solving. For a hanging rope, only the topmost particle is locked. For a flag, the entire left column is locked. For a tablecloth, corner or edge particles are locked to furniture geometry.
To implement grabbing, treat the mouse cursor as a temporary teleport target. On each frame, force the grabbed particle to the cursor's world position and copy that to its previous position (zeroing implicit velocity):
function applyMouseDrag(particle, targetWorldPos) {
// Zero velocity by matching prev to target, then set pos to target
particle.prev.copy(targetWorldPos);
particle.pos.copy(targetWorldPos);
}When released, the particle resumes from wherever it was dropped with zero velocity and settles naturally.
Stability and Performance
Verlet-based simulations have a few common failure modes:
- Tunneling: With too-large $\Delta t$ or strong gravity, particles can leap through geometry in one step. Fix with sub-stepping.
- Energy gain: Insufficient damping lets floating-point rounding accumulate into visible oscillation. Use damping of at least 0.99.
- Explosions: If constraint corrections overshoot (from a large timestep), the simulation blows up. Reduce gravity or increase iterations.
- Performance: For dense cloth (100×100 = 10,000 particles), use flat
Float32Arraybuffers instead ofVector3objects to eliminate garbage-collection pressure and improve cache locality.
Sub-stepping
Sub-stepping runs multiple small physics steps per rendered frame instead of one large step. Rather than integrating at $\Delta t = 16ms$ once, run four steps at $\Delta t = 4ms$ each. This is the single most effective stability improvement and allows stiffer constraints without oscillation:
const SUBSTEPS = 4;
function physicsUpdate() {
for (let step = 0; step < SUBSTEPS; step++) {
integrateVerlet(particles, gravity, damping);
solveConstraints(constraints, ITERATIONS);
}
}
// Called once per rendered frameReal-World Examples
- Spider-Man (Insomniac Games): Miles Morales' web is a multi-anchor rope with world-collision response. The grapple point is a locked particle; the swinging hero is a dynamic particle at the other end.
- Zelda: Tears of the Kingdom: Link's tunic, the Paraglider sail, and Zonai device tethers all use cloth-like simulation with pinned attachment points.
- INSIDE (Playdead): Rope-and-weight puzzle sequences use 2D Verlet rope simulation where players swing a weight to reach platforms.
- Unity Cloth Component: Unity's built-in cloth exposes damping, stiffness, and gravity multipliers — all tuning parameters of a PBD solver under the hood.
- Unreal Chaos Physics: Epic's Chaos cloth solver, used for character clothing in Fortnite, is a sophisticated PBD implementation running on the GPU for dense cloth meshes.
Beyond the Basics
- Collision detection: After constraint solving, push particles inside colliders back to the surface — a pure position correction, consistent with the PBD philosophy.
- Tearing: Remove constraints whose stretch exceeds a maximum threshold, creating satisfying cloth-tear effects used in games like Conan Exiles.
- Aerodynamics: Compute per-triangle normals and apply a wind force proportional to the dot product of the normal and wind direction for realistic billowing.
- GPU simulation: Move integration and constraint solving into compute shaders for cloth with hundreds of thousands of particles — the level seen in modern AAA character rendering.
Verlet-based rope and cloth have stayed in use because the algorithm fits real-time games well. A handful of rules — integrate positions forward, project them back onto length constraints, repeat — produces behaviour that reads as physically convincing without requiring expensive continuum mechanics. The algorithm degrades gracefully: a rope with fewer solver iterations looks like a stiffer rope, not a broken simulation. It is a rare case where the engineering constraint of real-time performance leads to a more robust solution than the physically exact one.
Comments