Article · Game Dev · Physics · Algorithms
📅 July 2026 ⏱ ≈ 20 min 🎯 Intermediate – Advanced · Last updated: 5 July 2026

XPBD: Extended Position-Based Dynamics

Position-Based Dynamics (PBD) made cloth, ropes, and soft bodies easy to simulate — but its stiffness secretly depended on iteration count and timestep, so a "steel cable" in a 60 fps game could turn to rubber the moment a player's machine dropped frames. XPBD (Extended Position-Based Dynamics), introduced by Macklin, Müller and Chentanez in 2016, fixes this by grounding every constraint in a real physical quantity: compliance. It is now the algorithm behind Unity's cloth solver, NVIDIA PhysX 5, and Blender's cloth/soft-body simulation.

1. Why Plain PBD Wasn't Enough

Classic Position-Based Dynamics (Müller et al., 2007) simulates constraints — distance, bending, volume — by directly projecting particle positions so that a constraint function C(x) = 0 is satisfied, then iterating this projection a fixed number of times per frame (Gauss-Seidel style). It's fast, unconditionally stable, and trivial to implement. The catch: nothing in the projection step corresponds to a real stiffness value in N/m. The "stiffness" you get is an artifact of two unrelated numbers — the number of solver iterations and the timestep — multiplied together.

Halve the timestep or double the iteration count, and a rope that used to stretch visibly suddenly behaves like a rigid rod. This made PBD unusable for anything that needed to match real material behaviour, and it meant tuning a cloth sim on one machine could look completely different on another with a different frame rate.

The core problem: plain PBD's constraint projection is equivalent to running an implicit solver with an infinitely stiff constraint, then artificially softening it by under-relaxing over iterations. There is no way to dial in "aluminium" vs "rubber" — only "more iterations" vs "fewer".

2. Compliance and Lagrange Multipliers

XPBD reformulates the projection as the solution of a constrained optimization problem straight out of Lagrangian mechanics. Each constraint C_j(x) = 0 gets a compliance value α_j — the inverse of physical stiffness, measured in m/N (metres per Newton, i.e. how much the constraint "gives" per unit force):

α = 1 / k (k = stiffness in N/m, α = compliance in m/N) α = 0 → perfectly rigid constraint (original PBD behaviour) α > 0 → soft / elastic constraint with real physical units

The total potential energy of a constraint is U(x) = ½ · α⁻¹ · C(x)². Minimizing this energy subject to the equations of motion introduces a Lagrange multiplier λ per constraint — physically, λ is the accumulated constraint force impulse. XPBD's key trick is updating λ incrementally alongside the position update, instead of solving the full linear system:

Δλ = ( −C(x) − α̃·λ ) / ( Σᵢ wᵢ·|∇Cᵢ|² + α̃ ) where α̃ = α / dt² (compliance rescaled by the substep timestep) and wᵢ = 1 / mᵢ (inverse mass of particle i) Δxᵢ = wᵢ · Δλ · ∇Cᵢ(x) λ ← λ + Δλ

Set α = 0 and this reduces exactly to classic PBD's projection formula — XPBD is a strict superset. Set α > 0 and you get a spring with a physically meaningful, iteration-count-independent stiffness.

3. The XPBD Solver Loop

The full per-frame algorithm interleaves substeps, each of which predicts positions, solves all constraints once (with λ reset to zero at the start of the substep, not the frame), and derives velocity from the position delta:

function xpbdStep(particles, constraints, dt, substeps) {
  const h = dt / substeps; // substep timestep

  for (let s = 0; s < substeps; s++) {
    // 1. Predict positions with external forces (gravity, wind...)
    for (const p of particles) {
      if (p.invMass === 0) continue; // pinned/static
      p.vel.y += -9.8 * h;
      p.prevPos.x = p.pos.x; p.prevPos.y = p.pos.y;
      p.pos.x += p.vel.x * h;
      p.pos.y += p.vel.y * h;
    }

    // 2. Reset Lagrange multipliers for this substep
    for (const c of constraints) c.lambda = 0;

    // 3. Solve every constraint once (Gauss-Seidel sweep)
    for (const c of constraints) solveConstraint(c, h);

    // 4. Derive velocity from the position change (PBD's core idea)
    for (const p of particles) {
      if (p.invMass === 0) continue;
      p.vel.x = (p.pos.x - p.prevPos.x) / h;
      p.vel.y = (p.pos.y - p.prevPos.y) / h;
    }
  }
}

Notice there is no separate "velocity solve" pass like in impulse-based engines — velocity is always a byproduct of the position change divided by h. This is what makes PBD and XPBD unconditionally stable: you can never diverge to infinite velocity, because velocity is bounded by how far a position can actually move in one substep.

4. Deriving the Distance Constraint

The workhorse constraint for cloth and ropes is the distance constraint, which keeps two particles at rest length L₀:

C(x₁, x₂) = |x₁ − x₂| − L₀ ∇C₁ = n̂ where n̂ = (x₁ − x₂) / |x₁ − x₂| ∇C₂ = −n̂ denom = w₁ + w₂ + α̃ Δλ = ( −C − α̃·λ ) / denom Δx₁ = w₁ · Δλ · n̂ Δx₂ = −w₂ · Δλ · n̂
function solveDistanceConstraint(c, h) {
  const { p1, p2, restLength, compliance } = c;
  const dx = p1.pos.x - p2.pos.x;
  const dy = p1.pos.y - p2.pos.y;
  const dist = Math.hypot(dx, dy);
  if (dist < 1e-9) return;

  const C = dist - restLength;
  const nx = dx / dist, ny = dy / dist;

  const alphaTilde = compliance / (h * h);
  const denom = p1.invMass + p2.invMass + alphaTilde;
  if (denom < 1e-9) return;

  const dLambda = (-C - alphaTilde * c.lambda) / denom;
  c.lambda += dLambda;

  p1.pos.x +=  p1.invMass * dLambda * nx;
  p1.pos.y +=  p1.invMass * dLambda * ny;
  p2.pos.x -=  p2.invMass * dLambda * nx;
  p2.pos.y -=  p2.invMass * dLambda * ny;
}

The exact same skeleton — compute C, compute ∇C per particle, solve for Δλ, apply weighted position deltas — works for bending constraints (dihedral angle between two triangles), volume constraints (tetrahedron signed volume), and area constraints. Only C and its gradient change.

5. Why Substeps, Not Solver Iterations

The original PBD paper fixed the timestep at the frame rate (e.g. 1/60 s) and increased the iteration count to improve convergence and stiffness. XPBD's authors found this converges poorly — a Gauss-Seidel sweep over many constraints propagates corrections only one link per iteration, so a long cloth strip needs dozens of iterations to feel taut.

Instead, XPBD favors many small substeps with a single constraint solve each, over one big frame with many iterations. For the same total compute budget, substepping converges dramatically faster because the compliance term α̃ = α / h² shrinks as h shrinks — smaller substeps make every constraint stiffer and more accurate automatically, without retuning any parameter.

Rule of thumb: 20–30 substeps with a single iteration each converges better than 1 substep with 100 iterations, for roughly the same cost, and — crucially — the resulting stiffness stays correct regardless of how many substeps you pick, because α is a physical constant, not a tuning knob for "looks stiff enough".

6. Damping Without Energy-Loss Bugs

Naively damping velocity after the solve (v *= 0.98) removes energy non-uniformly and can make simulations frame-rate-dependent again. XPBD instead recommends constraint-based damping: add a small extra position correction proportional to the relative velocity along the constraint gradient, scaled by a damping compliance β and the substep h:

Δx_damp = −β · h · (∇C · v_rel) · ∇C / (Σ wᵢ|∇C|² ) applied in the same substep, right after the elastic correction

This keeps damping proportional to the actual relative velocity at the constraint (like a physical dashpot/dampener), so a cloth sim run at 20 substeps and one run at 30 substeps settle to the same resting shape at the same rate.

7. Full Implementation: A Hanging Chain

Putting it together — a chain of particles linked by distance constraints, with the first particle pinned (invMass = 0):

class Particle {
  constructor(x, y, invMass = 1) {
    this.pos = { x, y };
    this.prevPos = { x, y };
    this.vel = { x: 0, y: 0 };
    this.invMass = invMass;
  }
}

class DistanceConstraint {
  constructor(p1, p2, compliance = 0) {
    this.p1 = p1; this.p2 = p2;
    this.restLength = Math.hypot(p1.pos.x - p2.pos.x, p1.pos.y - p2.pos.y);
    this.compliance = compliance; // 0 = rigid, higher = stretchier
    this.lambda = 0;
  }
}

// Build a 12-link chain, first link pinned in place
const chain = [];
for (let i = 0; i < 12; i++)
  chain.push(new Particle(i * 0.2, 2, i === 0 ? 0 : 1));

const links = [];
for (let i = 0; i < chain.length - 1; i++)
  links.push(new DistanceConstraint(chain[i], chain[i + 1], 0.0001)); // slightly compliant steel

// Main loop (call once per rendered frame, dt ≈ 1/60)
function simulate(dt) {
  xpbdStep(chain, links, dt, 24); // 24 substeps
}
Tuning compliance: steel cable ≈ 1e-8 – 1e-7 m/N, nylon rope ≈ 1e-6 – 1e-5, soft rubber band ≈ 1e-4 – 1e-3. Set α = 0 for an inextensible constraint identical to classic PBD.

8. Applications: Cloth, Rods, and Soft Bodies

9. Common Pitfalls

Further reading: Macklin, Müller, Chentanez, "XPBD: Position-Based Simulation of Compliant Constrained Dynamics" (MIG 2016), and the follow-up "Small Steps in Physics Simulation" (SCA 2019), which formalizes the substep-vs-iteration argument in section 5 above.