Collision Detection: Broad and Narrow Phase
Drop a thousand bouncing crates into a physics world and, before you can push a single one of them
apart, you have to answer one deceptively simple question: what is touching what? The
lazy answer is to test every crate against every other crate. With n = 1000
objects that is \binom{1000}{2} = 499{,}500 exact overlap tests
every single frame — half a million tests sixty times a second, and it only gets worse: the
naive cost grows like O(n^2), so ten thousand objects is a hundred times
more expensive again. No real engine can afford that.
The way out is to split the work into two phases. A cheap, near-linear
broad phase throws away the overwhelming majority of pairs that could not possibly be
touching, leaving a short list of candidates; then an exact but more expensive
narrow phase tests only those survivors and, for the real hits, produces the
contact data the response step needs. This page is about that division of labour —
why it exists, the acceleration structures that make the broad phase fast, and the sharp edges (like a
bullet passing clean through a wall) that catch the unwary.
Two phases, two jobs
- Broad phase — cheaply and conservatively cull the
O(n^2) possible pairs down to a small set of potentially
colliding pairs, using loose bounding volumes (typically axis-aligned boxes) and a spatial
acceleration structure. It may report false positives, but it must never miss a true
overlap.
- Narrow phase — run an exact intersection test on each surviving pair
(sphere–sphere, box–box, or a convex test such as the
GJK
algorithm), and for genuine contacts compute the contact manifold: contact
point(s), a surface normal, and a penetration depth.
The split works because the two jobs have opposite economics. The broad-phase test is crude and cheap,
so you can afford to run it on every object. The narrow-phase test is precise and pricey, so
you want to run it on as few pairs as possible. Feed the cheap filter first, and the expensive test
only ever sees the handful of pairs that actually matter.
The broad phase must be near-linear
Here is the trap the two-phase structure is built to avoid. Suppose your broad phase itself compares
every object's bounding box against every other object's box. That is still
O(n^2) work — you have made each test cheaper, but you have not changed the
growth rate, so at large n the whole thing collapses back into the
quadratic wall you were trying to escape.
A useful broad phase must be sub-quadratic — close to
O(n \log n) or O(n) — so that the total cost is
dominated by the narrow phase acting on the few real candidates rather than by the culling itself.
Three families of acceleration structure achieve this:
- Sweep-and-prune (SAP) — project every box onto each coordinate axis to get an
interval [\text{min}, \text{max}], sort the endpoints along each axis,
then sweep through in order. Two boxes can only overlap if their projections overlap on
every axis, so you report a candidate pair only where all axes agree.
- Uniform grid / spatial hashing — chop space into cells of a fixed size, hash
each object into the cells its box touches, and only test objects that share a cell. Nearby objects
land in the same bucket; distant ones never meet.
- Bounding-volume hierarchy (BVH) — nest the bounding volumes into a tree so that
a parent volume encloses its children. Descend both trees together; whole branches whose parent
volumes miss are pruned in one comparison. Ideal for static geometry and uneven object sizes.
These all lean on cheap bounding
volumes and spatial
partitioning — the same game-dev toolkit reused, now in service of physics.
Seeing sweep-and-prune
In the figure below, three axis-aligned bounding boxes (AABBs) sit in the plane. Beneath them, each box
is projected onto the x-axis as an interval. Boxes A and
B have overlapping x-intervals, so sweep-and-prune
keeps (A, B) as a candidate to hand to the narrow phase. Box
C's interval is disjoint from the others, so every pair involving C
is pruned immediately — no exact test is ever run on it.
The trick that makes this fast is sorting. Sort the interval endpoints once, sweep through
them in order keeping a small "active set" of currently-open intervals, and you find every overlap in
roughly O(n \log n) for the sort plus O(n + k) for
the sweep, where k is the number of overlapping pairs actually reported.
Worked example: 1000 objects
Put numbers on it. With n = 1000 objects the naive all-pairs approach does
\binom{n}{2} = \frac{n(n-1)}{2} = \frac{1000 \cdot 999}{2} = 499{,}500 \text{ tests per frame.}
At 60 frames per second that is about 30 million exact overlap tests every
second — and each one might be a full convex intersection. Now suppose the objects are spread over a
uniform grid so that, on average, only a handful share any cell. The grid visits each object roughly
once and checks it against only its neighbours, giving work proportional to
n plus the number of genuine near-pairs — call it a few thousand candidate
tests instead of half a million.
- Naive: \sim n^2/2 = 5 \times 10^5 exact tests per frame.
- Grid / SAP broad phase: \sim O(n) = 10^3 cheap box tests, leaving only
the few candidate pairs for exact testing.
- Roughly a hundredfold cut at n = 1000, widening
without bound as n grows — that gap is why the broad phase is
not optional.
The narrow phase: exact tests and contact data
Once the broad phase hands over a short candidate list, the narrow phase runs the exact
geometry. For two spheres of radii r_1, r_2 at centres
c_1, c_2 the test is a single distance comparison:
\lVert c_1 - c_2 \rVert \;\le\; r_1 + r_2 \quad\Longleftrightarrow\quad \text{overlap.}
For two AABBs it is three interval-overlap checks, one per axis; for general convex shapes it is a
separating-axis or GJK-style test. But detecting the hit is only half the narrow phase's job. To
respond to the collision — push the bodies apart, apply impulses — the solver needs a
contact manifold:
- Contact point — where the bodies touch (or the deepest point of overlap).
- Contact normal \hat{n} — the unit direction along
which to separate them; for two spheres, \hat{n} = (c_2 - c_1)/\lVert c_2 - c_1
\rVert.
- Penetration depth d — how far they have
interpenetrated; for two spheres, d = r_1 + r_2 - \lVert c_1 - c_2 \rVert.
Push each body back by d/2 along \hat{n} and
they just touch.
A boolean "yes, they overlap" is enough for a trigger volume, but a physical response is blind without
the normal and the depth. Producing that manifold cleanly — especially for face-to-face contact with
multiple contact points — is most of what makes the narrow phase intricate.
Temporal coherence: objects barely move
Here is the observation that makes real-time broad phases practical: between two consecutive frames,
almost nothing moves very far. A crate that was left of another crate this frame is overwhelmingly
likely to still be left of it next frame. So the sorted endpoint order that sweep-and-prune built last
frame is almost already correct — a few neighbours may have swapped, but the list is nearly
sorted.
Re-sorting a nearly-sorted list with insertion sort costs about
O(n), not O(n \log n), because each element only
drifts a slot or two. This temporal coherence — reusing last frame's structure and
patching the small changes — is why incremental sweep-and-prune is a workhorse of real engines, and why
the same idea speeds up grids (objects mostly stay in their cell) and BVH refits (boxes mostly stay
put).
Discrete vs continuous detection
Everything so far is discrete collision detection: sample each object's pose at frame
t, then again at t + \Delta t, and test the two
snapshots. That is fine when objects move less than their own size per step. But a small, fast object
can leap past a thin obstacle entirely: it is on one side at frame t
and already on the far side at t + \Delta t, and neither snapshot overlaps
the wall. The engine reports no collision, and the object tunnels straight through.
Continuous collision detection (CCD) fixes this by testing the swept volume —
the region the object sweeps out along its path between the two frames — rather than just its endpoints.
Instead of asking "do the two poses overlap?" it asks "does the moving shape's trajectory cross the
obstacle at any moment in [t, t+\Delta t]?", solving for the earliest
time of impact. It costs more, so engines reserve it for the objects that need it —
bullets, thin fast debris — and leave slow, chunky objects on cheap discrete tests.
You could shrink \Delta t until even a bullet only moves a millimetre per
step — but the cost is brutal. Halving the timestep doubles the number of full physics
updates (broad phase, narrow phase, and solver) per second of animation, and it still does not
guarantee safety: there is always some faster object or thinner wall that defeats whatever
fixed step you pick. Continuous detection sidesteps the whole race by reasoning about the entire
path in one test, so it catches the impact regardless of speed. Tiny timesteps buy you
robustness you can never quite finish paying for; swept tests buy it outright, only where you need it.
The classic failure of discrete collision detection is tunnelling. A fast, thin
object — a bullet, a sword blade, a thrown knife — can pass entirely through a thin wall
between two frames because at frame t it is in front of the wall and at
frame t + \Delta t it is already behind it, and neither snapshot
overlaps. The broad phase never even flags the pair, the narrow phase never runs, and the bullet
sails through solid stone with no complaint.
The fix is continuous collision detection or swept-volume tests for exactly those
fast/thin objects: test the object's trajectory, not just its endpoints, and solve for the time of
impact. Do not reach for a smaller global timestep as the cure — it is expensive and never fully safe.
Flag your bullets as CCD objects and leave the rest discrete.