XPBD Lab.

One solver, four toys. Compliance is a real number in metres per newton, so the softness you ask for is the softness you get, whatever the timestep. The old way was a stiffness between zero and one that quietly meant something different every time you changed the substep count. Switch between them and watch the cloth change its mind.

Prove it

The claim under test: an XPBD constraint with compliance a holding a load F settles at a stretch of exactly a × F, no matter how the timestep is chopped up, while classic PBD settles wherever the arithmetic happens to leave it. This runs both solvers headless on the same one kilogram weight, once per substep count, and compares each result against the closed form derived below. Nothing here reads the picture on the canvas; it re-simulates from scratch.

Not run yet.
XPBD measured PBD measured closed form substeps across, settled stretch in metres up the side, both log scaled
The arithmetic, if you want it

The solver, in full

For a distance constraint C = |x1 - x2| - L with compliance alpha and substep h, set at = alpha / h^2 and iterate

dLambda = (-C - at * lambda) / (w1 + w2 + at), then x_i += w_i * gradC_i * dLambda, then lambda += dLambda.

lambda starts at zero every substep. It is the force the solver has already spent on this constraint, and carrying it is the difference between XPBD and PBD. w is inverse mass, so a pinned particle has w = 0 and never moves.

Why the weight settles at compliance times load

One particle of mass m hanging from a pin. At rest the prediction step drops it by g h^2, so the gap the solver sees is C* + g h^2. With a single constraint the problem is linear, so one iteration lands exactly on the fixed point, which is C = C_predicted * at / (at + w). Set the two equal and the steady state is

C* = g h^2 * at / w = g h^2 * (alpha / h^2) * m = alpha * m * g.

The h^2 cancels. That cancellation is the entire reason the compliance slider means something. Take the / h^2 out of at and it does not cancel any more, and the softness of your cloth goes back to being a property of your framerate.

And why the old way does not

Classic PBD moves each end by k * C per iteration, so n iterations multiply the gap by (1-k)^n. The same steady state argument gives

C* = g h^2 * (1-k)^n / (1 - (1-k)^n)

which is proportional to h^2, so it falls with the square of the substep count. Run the sweep and watch the orange line dive while the teal one sits flat.

The blob's area constraint

Shoelace: A = 1/2 * sum(x_i*y_next - x_next*y_i). Differentiate and the gradient at vertex i is (y_next - y_prev)/2 in x and (x_prev - x_next)/2 in y, which is the perpendicular of the line joining its two neighbours, at half length. The button above checks that analytic gradient against a central difference on a random polygon, because a gradient you derived and a gradient you tested are different things.