Motecraft

A verlet integrator under 100 lines

One small physics engine runs Ragdoll, Curtain, Spider Web, Rope Bridge, and Elastic Mesh. The engine fits in 80 lines of Swift. Here's why it works.

If you throw a doll at a wall in a video game and it tumbles convincingly, there's a good chance the engine doing the work is verlet integration — specifically, the position-based variant Thomas Jakobsen wrote up in his 2001 paper Advanced Character Physics. That paper is responsible for an entire generation of cloth, ropes, hair, and ragdolls in games. It's also the smallest physics that genuinely feels right for visual simulation. The whole engine — integrator, constraints, the gesture binding that lets a finger drag a particle around — fits in under 100 lines of Swift.

I'm going to walk through it because Motecraft uses one engine for five different experiences (Ragdoll, Curtain, Spider Web, Rope Bridge, Elastic Mesh), and the engine is small enough that you should be able to read the whole thing, in this post, without scrolling past your patience.

What verlet integration actually is

The classical way to advance a physics simulation by one timestep looks like this:

position += velocity * dt
velocity += acceleration * dt

You store position and velocity for each particle. Apply forces, integrate twice. Done.

Verlet does it differently. There's no velocity variable. Instead, each particle stores its previous position, and the integration step computes new position from current and previous:

new = current + (current - previous) + acceleration * dt²
previous = current

Velocity is implicit — it's the gap between previous and current. If you want a particle to keep moving in the direction it was moving, you don't have to do anything; the gap carries the motion forward. If you want to stop it, you set its previous position equal to its current. The next step's gap is zero. No velocity needs to be zeroed.

This sounds like a parlour trick. It's not. The trade-off it buys you is the entire reason the technique works.

Euler vs verlet — same motion, different state

EULERp: CGPointv: CGVectorexplicit velocityp += v · dtv += a · dtVERLETp: CGPointpp: CGPointimplicit velocity = p − ppnew = p + (p−pp) + a·dt²pp = p; p = new
Both schemes describe the same motion at the limit. Verlet stores no velocity; the implicit velocity is the gap between previous and current position. Constraints that move position directly therefore consume their own velocity automatically — no need to integrate the velocity update around them.

The integration step

Here's the entire integrator. Eight lines.

struct Particle {
  var p: CGPoint
  var pp: CGPoint
  var pinned: Bool = false
}
 
func integrate(dt: CGFloat, gravity: CGVector) {
  for i in particles.indices where !particles[i].pinned {
    let cur = particles[i].p
    let vel = CGVector(dx: cur.x - particles[i].pp.x,
                       dy: cur.y - particles[i].pp.y)
    particles[i].pp = cur
    particles[i].p.x = cur.x + vel.dx + gravity.dx * dt * dt
    particles[i].p.y = cur.y + vel.dy + gravity.dy * dt * dt
  }
}

That's a free-fall simulation. Drop a particle, it accelerates, it keeps accelerating. No collision, no rope, no ragdoll yet. But it's stable for as long as you'd like to run it, because there's no velocity term to accumulate floating-point drift.

Constraints, the part that does the actual work

The magic isn't in the integrator. It's in constraints.

A constraint is a relationship two particles must satisfy. The simplest useful one is the distance constraint: particles A and B must be exactly rest apart. If they're not — too close, too far — the constraint fixes them. Verlet handles this by moving the positions directly, half on each particle, no force calculation:

struct Stick {
  let a: Int        // particle index
  let b: Int        // particle index
  let rest: CGFloat
}
 
func relaxStick(_ s: Stick) {
  let pa = particles[s.a].p
  let pb = particles[s.b].p
  let dx = pb.x - pa.x
  let dy = pb.y - pa.y
  let dist = sqrt(dx * dx + dy * dy)
  guard dist > 0.0001 else { return }
  let diff = (dist - s.rest) / dist * 0.5
  let mx = dx * diff
  let my = dy * diff
  if !particles[s.a].pinned {
    particles[s.a].p.x += mx
    particles[s.a].p.y += my
  }
  if !particles[s.b].pinned {
    particles[s.b].p.x -= mx
    particles[s.b].p.y -= my
  }
}

One pass over every stick is rarely enough — fixing one stick usually breaks another. So you iterate. Four to eight passes per frame is the sweet spot for what Jakobsen called the "looks right" zone.

Constraint relaxation, four iterations

iter 0iter 1iter 2iter 4rest R100pt200pt260pt~320pt
Two particles, one stick, rest length R. Initial state: too close. Each iteration moves each particle half the distance toward satisfying the constraint. After four passes, the configuration is visually correct. More iterations = stiffer cloth, denser ropes.

Why not spring-mass-damper?

The classical alternative is to model each constraint as a spring with a damper: F = -k(distance - rest) - c * velocity_along_axis. You sum forces on each particle, integrate using Euler or RK4, repeat.

This is the textbook approach. It is also genuinely painful to make look like cloth.

The problem is stiffness. A spring with low k feels rubbery — particles bounce around their constraint, the cloth has visible jiggle. A spring with high k — the kind you need for "this feels like silk, not a trampoline" — requires a tiny timestep to remain stable. At 60fps with dt = 16ms, a stiff cloth simulation will blow up within a second. Either the system explodes outward or it oscillates into noise.

You can fix that with implicit integration. Implicit integration is a matrix solve. You're now linking the cloth mesh into a sparse system of equations and solving it per frame. The performance and complexity are no longer in the "two-evening project" category. Game engines that ship cloth via SMD all pay this cost.

Verlet sidesteps the entire question. Constraints move positions directly, so they consume their own velocity automatically. There's no SMD to integrate, no stiffness parameter, no oscillation. The cost of "stiffer" is just more iterations — six passes instead of four. The numerical scheme stays stable at any stiffness because there's no springy term to amplify.

The trade-off is honesty. Verlet doesn't model real springs. The visual physics it produces are physics-shaped without being physics- true. For a calm visual app where the goal is "feels like cloth", that's a feature. For a structural engineering simulation, it's a problem.

Same engine, two experiences

Ragdoll and Curtain share the same particle/stick/integrate code. They differ entirely in the topology — which particles exist, and which sticks connect them.

A ragdoll is sparse. You build a body out of maybe 15 named particles (head, neck, shoulders, hips, elbows, knees, wrists, ankles) connected by ~20 sticks. The visual character — "this is a body falling, not a rope" — comes from the sticks forming the right graph: arms branching off the shoulders, legs off the hips, the spine connecting both.

Curtain is dense and regular. The default in Motecraft is a 14×12 grid: 168 particles, ~600 sticks. Horizontal and vertical sticks between adjacent cells, plus two diagonals per inner cell (the diagonals are what stop the mesh shearing into a parallelogram under load). The top row is pinned. Drag any particle, the constraint network propagates the disturbance.

Same engine, opposite topologies

RAGDOLL · 15 particles · ~20 sticksCURTAIN · 14×12 = 168 particles · ~600 stickspinned
Ragdoll: 15 particles, ~20 sticks, hand-placed to form a body skeleton. Curtain: 168 particles, ~600 sticks, regular grid with diagonal bracing. The integrator code that runs both is identical.

The same relaxStick function works on both. Ragdoll runs at 60fps with six iterations and a handful of sticks. Curtain runs at 60fps with four iterations and 600 sticks. The engine doesn't know or care which one it's serving.

The whole engine, by line count

Putting it together — particle struct, stick struct, integrate function, relax function, render — the entire engine is around 80 lines of Swift. The breakdown:

particle + stick structs ......... 12 lines
integrate(dt:gravity:) ........... 12 lines
relaxStick(_:) ................... 18 lines
solve(iterations:) ............... 6  lines
gesture binding (drag-to-pin) .... 14 lines
draw(into:) ...................... 16 lines
                                   --
                                   78 lines

Add the experience-specific topology code (where to put the particles, which sticks to create, what to pin), and Ragdoll is 30 extra lines. Curtain is 35. Spider Web is 50 because it has radial and spiral constraints.

Every other piece of cloth, rope, hair, ragdoll physics in Motecraft is the same 80 lines with a different topology layer on top. That's why physics in this app is something I can iterate on between breakfast and lunch instead of a quarter-long engineering investment.

If you'd like to read the engine itself, the canonical implementation is in apps/ios/MotecraftApp/Experiences/Ragdoll/RagdollRenderer.swift on the iOS side, and a near-identical Kotlin version is at apps/android/.../ragdoll/RagdollRenderer.kt. The five experiences listed at the top of this post all share the same core. If you ever read Jakobsen's paper after this, it'll feel familiar — the paper is about 12 pages and it's the most worthwhile twelve pages of physics reading I know for visual work.

Verlet integration is not a sophisticated technique. That's what makes it correct for this kind of app. The smallest physics that feels right is usually the best physics, because what we actually want isn't real motion. It's recognisable motion. And recognisable is short.

◆ See them open

Experiences in this post

The post references 5 experiences from the Motecraft catalog. Tap to open the detail page or just install the app and find them under Cloth or Creative.