Impulse-Based Collision Response
Your collision detector has just told you two things touched: a ball and a floor, at a
contact point, along a contact normal \mathbf{n},
overlapping by a small penetration depth. Now what? The naive instinct is to push them
apart with a force. But a real contact happens in essentially zero time — a finite force acting
over zero time changes nothing (\Delta \mathbf{p} = \mathbf{F}\,\Delta t \to 0).
To flip a velocity instantly we need a force that is infinite for an instant, and the only sane
way to talk about that is its integral: an impulse.
An impulse is a sudden, finite change of momentum, \mathbf{j} = \Delta \mathbf{p},
delivered along the normal. This page is the engine of every game physics response step: how big
an impulse to apply, how bouncy to make the result, how spin and friction enter, and how to keep a
stack of boxes from sinking through the floor. It builds on
momentum and collisions and the wider
computer-animation tree.
The one number that sets bounciness: restitution
Before any formula, meet the star of the show. The coefficient of restitution
e is a single dimensionless number, e \in [0, 1], that
answers "how much of the closing speed survives as separating speed?" It relates the relative velocity
after the collision to the relative velocity before, measured along the normal:
v_{\text{rel}}^{\,+} = -\,e\,v_{\text{rel}}^{\,-},
where v_{\text{rel}} = \mathbf{n}\cdot(\mathbf{v}_A - \mathbf{v}_B) is the
normal component of relative velocity (a superscript - is pre-collision,
+ is post-collision). The minus sign flips approach into separation.
- e = 1 — perfectly elastic: all the closing speed comes
back, no kinetic energy is lost, the ball rebounds to the same height forever.
- e = 0 — perfectly inelastic: the separating speed is
zero, the objects stick (or slide along the surface), maximum energy lost to the collision.
- 0 < e < 1 — the realistic middle: a rubber ball is near
0.8, a billiard ball near 0.95, a lump of clay
near 0.
The normal-impulse formula
We want a scalar impulse magnitude j such that applying
\mathbf{j} = j\,\mathbf{n} to body A (and
-j\,\mathbf{n} to body B, by Newton's third law)
produces exactly the desired post-collision normal velocity -e\,v_{\text{rel}}^{-}.
Each body's velocity changes by \Delta \mathbf{v} = \mathbf{j}/m. Working the
algebra through for two bodies with masses m_A, m_B gives the classic result:
j = \frac{-(1 + e)\,v_{\text{rel}}^{-}}{\dfrac{1}{m_A} + \dfrac{1}{m_B}}.
The denominator is the combined inverse mass: a heavier pair resists the impulse. Fixing
an object in place — an immovable floor or wall — is just the limit
m_B \to \infty, i.e. 1/m_B = 0, so the whole impulse
acts on the moving body. (Storing 1/m as "inverse mass" makes static objects a
clean 0 rather than a division by infinity — that's why engines store inverse
mass, not mass.)
Off-centre hits impart spin: the angular terms
Real bodies rotate. If the contact point is not on the line through the centre of mass, the same impulse
also produces a torque, and the object gains angular velocity — an off-centre whack spins
the object. Let \mathbf{r}_A be the vector from body
A's centre of mass to the contact point. The impulse changes the angular
velocity through the inertia tensor
I:
\Delta \boldsymbol{\omega}_A = I_A^{-1}\,(\mathbf{r}_A \times \mathbf{j}).
Because the point of contact is now also moving from the body's spin, the "effective mass" seen along the
normal picks up rotational terms. The full two-body normal impulse becomes:
j = \frac{-(1+e)\,v_{\text{rel}}^{-}}{\dfrac{1}{m_A} + \dfrac{1}{m_B} + \mathbf{n}\cdot\big(I_A^{-1}(\mathbf{r}_A\times\mathbf{n})\times\mathbf{r}_A\big) + \mathbf{n}\cdot\big(I_B^{-1}(\mathbf{r}_B\times\mathbf{n})\times\mathbf{r}_B\big)}.
Set the rotational terms to zero (point-masses, or a central hit) and this collapses back to the tidy
linear formula above. The relative velocity v_{\text{rel}} must now be measured
at the contact point, including each body's \boldsymbol{\omega}\times\mathbf{r}
surface velocity.
Worked example: a ball hits the ground
Drop a ball of mass m = 0.5\ \text{kg} onto a rigid floor. Just before contact it
is moving down at 6\ \text{m/s}, so with an upward normal
\mathbf{n} the pre-collision normal velocity is
v^{-} = -6\ \text{m/s}. The floor is immovable
(1/m_B = 0) and central, so no spin. Take restitution
e = 0.8.
Post-collision velocity comes straight from the restitution law:
v^{+} = -e\,v^{-} = -0.8 \times (-6) = +4.8\ \text{m/s} — the ball rebounds
upward at 4.8\ \text{m/s}, having lost 1.2\ \text{m/s}
of speed to the bounce. In one clean stroke, v' = -e\,v.
The impulse that achieved it:
j = \frac{-(1+e)\,v^{-}}{1/m} = -(1+0.8)(-6)(0.5) = 5.4\ \text{kg·m/s}.
Check: \Delta v = j/m = 5.4/0.5 = 10.8\ \text{m/s}, and indeed
-6 + 10.8 = +4.8\ \text{m/s}. The impulse turned a
6\,\text{m/s} descent into a 4.8\,\text{m/s} rebound in
a single instant — exactly what a force never could over zero time.
Sideways too: the friction impulse
The normal impulse handles bouncing. But contacts also grip sideways. We apply a second,
tangential impulse j_t along the surface (perpendicular to
\mathbf{n}) that tries to kill the relative sliding velocity — this is what
makes a ball roll instead of skid, and lets a box come to rest on a slope. But friction is not unlimited.
The Coulomb model caps it in proportion to how hard the surfaces press together:
|j_t| \le \mu\,j_n,
where \mu is the friction coefficient and j_n the
normal impulse we just computed. If the tangential impulse needed to fully stop the sliding is within the
cone (\le \mu j_n), the contact sticks (static friction, no
slide). If it would exceed the cap, we clamp it to \mu j_n and
the surfaces slide (kinetic friction). Harder normal press → more grip: the same reason a heavy crate is
harder to shove than a light one.
Many contacts at once: sequential impulses
A single bounce is easy. A stack of boxes, or a pile of debris, is not — every box touches its
neighbours, and each contact's correct impulse depends on all the others (push the top box down and the
bottom contact must push back harder). Solving that coupled system exactly is expensive. The trick that
powers modern engines (Box2D, Bullet, PhysX) is sequential impulses: don't solve it all
at once — iterate.
- Loop over every contact and apply its normal (and friction) impulse independently, using the
current velocities.
- Each impulse disturbs the others, so one pass is not enough — repeat the loop
several times (typically 4–20 iterations per frame).
- The velocities converge toward the state where all contacts are simultaneously satisfied —
this is Gauss–Seidel relaxation applied to the contact constraints.
The beauty is that it's cheap, incremental, and degrades gracefully: run more iterations for a tall,
wobbly stack; fewer for a couple of loose objects. Accumulate each contact's total impulse across
iterations and clamp the accumulation (impulses can only push, never pull) for stability.
Fixing the overlap: positional correction
Impulses fix velocities, but by the time we detect a collision the objects usually already
overlap a little (they moved into each other during the timestep). Left alone, that
penetration lingers and objects visibly sink. The fix must nudge them apart without secretly injecting
energy (or the sim gains speed from nowhere). Two standard cures:
- Baumgarte stabilisation — feed a fraction \beta of the
penetration depth back as a small extra bias velocity in the impulse solve, so contacts gently
push out over a few frames. Cheap, but the added velocity is real energy, which can cause jitter.
- Split impulse — solve position separately from velocity: use a pseudo
velocity to move bodies out of overlap, but do not let that pseudo-velocity feed back into the
real motion. Removes the penetration with no energy added to the actual dynamics — the modern default.
Both correct only a fraction of the penetration per frame (a "slop" tolerance leaves a tiny
overlap unresolved) so resting contacts don't buzz. The velocity solver makes things bounce right;
the positional solver makes them sit right.
Interactive: how restitution eats bounce height
Each bounce keeps a fraction e of the vertical speed. Since the height
a ball reaches goes as the square of its launch speed
(h \propto v^2), each bounce keeps e^2 of the
height. After n bounces:
h_n = e^{\,2n}\,h_0 \quad\Rightarrow\quad \text{a geometric decay with ratio } e^2.
Drag the e slider and watch the middle curve. Near
e = 1 it barely droops (a superball bounces for ages); near
e = 0.5 it collapses almost at once (each bounce keeps only a quarter of the
height). The faint reference curves are e = 0.9 and
e = 0.5.
The starting height is h_0 = 1. Notice the decay is geometric, not
linear: the drop from bounce 0 to 1 dwarfs the drop from bounce 5 to 6 — which is exactly why a real ball's
bounces get rapidly closer together in time and it seems to "buzz" to a stop.
Because they answer two different physical questions. The impulse answers "what should the velocities
become so the objects separate at the right speed?" — a genuine dynamics question with real
momentum exchange. The penetration, by contrast, is a numerical artefact: it only exists because
we advanced time in a finite step and let the objects overlap. If you tried to fix the overlap by adding
velocity, you'd be injecting momentum the physics never called for — the objects would ping apart faster
each frame. Splitting the two (a "split impulse") lets you shove bodies out of overlap geometrically while
leaving their real velocities untouched. It's the difference between correcting a bookkeeping error
and changing the actual bank balance.
Two classic ways to blow up your simulation:
Restitution above 1. The formula happily accepts e > 1, and
it will faithfully make each bounce come back faster than it arrived — the ball climbs higher every
time, kinetic energy grows without bound, and within a few seconds the object rockets off the screen. An
e > 1 is a perpetual-motion machine. Always clamp
e \in [0, 1]; energy is conserved only at e = 1 and
lost for anything less.
One pass over a stack. Resolving contacts one at a time with a single sweep
leaves a tall stack unsolved: fixing the top contact un-fixes the bottom one, so boxes sink into each other,
jitter, and slowly settle. The cure is not a bigger impulse — it's iteration. Run the
sequential-impulse loop several times per frame so the coupled contacts converge together. A stack that
sinks or vibrates is almost always starved of solver iterations.