Collision response
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 settle | No slop, correction at 100%, or no sleeping |
| Stacks sink into the floor | Too few solver iterations, or correction percent too low |
| Bodies gain energy and go wild | Restitution applied per iteration instead of against the initial approach velocity |
| Small bounces that never die out | No restitution threshold; below ~1 m/s treat e as 0 |
| Objects stick to walls instead of sliding | Friction impulse not clamped to µ * j, so it over-corrects |
| Statics get shoved by dynamics | Positional correction applied without weighting by inverse mass |
| Correct impulse, objects still overlap visually | Impulse fixes velocity only; you still owe the positional correction |