Collision response

11-collision-response.md · updated 2026-10-15

Detection told you two bodies overlap, by how much, and along which normal. Response has two jobs, in this order: get them un-overlapped, then fix their velocities so they stop moving into each other. Those are separate operations, and mixing them up is the usual source of mush.

Separate first, with slop

Push bodies apart along the contact normal, but only most of the way, and only if they're overlapping by more than a hair.

pen  = depth - slop                  // slop ≈ 1 cm, ignore less
move = percent * max(pen, 0) / (invMassA + invMassB)
A.pos -= n * move * invMassA
B.pos += n * move * invMassB

slop means a resting body is allowed to sink slightly and be left alone — without it, the correction nudges it up, gravity pulls it back, and it buzzes forever. percent (0.2–0.8) means each frame removes only part of the error, so an over-corrected body doesn't get launched. This moves positions directly and deliberately does not touch velocity.

Then the impulse

An impulse is an instant change in velocity, which is what a collision actually is. A collision happens in microseconds; expressing it as a force over your 16 ms frame would let the bodies interpenetrate for the whole frame. So you solve directly for the velocity change.

rv = (vB - vA) · n                   // relative velocity along the normal
if (rv > 0) return                   // already separating — nothing to do
j  = -(1 + e) * rv / (invMassA + invMassB)
vA -= n * j * invMassA
vB += n * j * invMassB

e is restitution: 0 is a dead thud, 1 is a perfect bounce that keeps all its energy. Note that everything is in terms of inverse mass. That's not a micro-optimisation — it's how static bodies work. A wall has invMass = 0, i.e. infinite mass: the impulse divides by the pair's inverse-mass sum, the wall's velocity change is multiplied by zero, and the dynamic body alone absorbs the whole response. No branching on "is this static".

Friction is the same impulse, sideways

Tangential impulse, capped by the normal impulse — that's Coulomb friction. Take the tangent t perpendicular to n, solve for the impulse that would kill all sliding, then clamp its magnitude to µ * j. Under the cap, sliding stops (static friction); at the cap, you get a constant drag (kinetic friction). Without the clamp, friction can reverse a body's motion, which looks like it hit glue.

Why solvers iterate

Each contact is solved in isolation, but fixing contact A changes the velocities that contact B assumed. So you loop over all contacts several times (4–10) and let them converge. With one iteration a stack slumps and never stops twitching, because the bottom box is pushed from above and from below and only ever gets told about one of them.

Even a converged solver leaves resting stacks faintly alive. The production fixes are warm-starting (seed each contact with last frame's impulse, so iteration continues rather than restarts) and sleeping (a body below a velocity threshold for N frames stops being simulated until something touches it).

Demo

Bouncing circles with every response knob exposed — drop a stack at 8 solver iterations and it dies; drop it at 1 and it slumps and twitches forever. The kinetic-energy readout keeps restitution honest.

Gotchas

Symptom Cause
Resting bodies buzz and never settleNo slop, correction at 100%, or no sleeping
Stacks sink into the floorToo few solver iterations, or correction percent too low
Bodies gain energy and go wildRestitution applied per iteration instead of against the initial approach velocity
Small bounces that never die outNo restitution threshold; below ~1 m/s treat e as 0
Objects stick to walls instead of slidingFriction impulse not clamped to µ * j, so it over-corrects
Statics get shoved by dynamicsPositional correction applied without weighting by inverse mass
Correct impulse, objects still overlap visuallyImpulse fixes velocity only; you still owe the positional correction

Open the demo →

← all notes & demos