A mirror does one thing, perfectly: it takes an incoming ray and sends it back out at the
same angle, on the other side of the surface. A raytracer needs that bounce as a formula —
given the direction a ray is travelling and the
surface normal
it strikes, what direction does it leave in? The whole answer is one line,
\vec{R} = \vec{D} - 2(\vec{D}\cdot\vec{N})\,\vec{N},
where \vec{D} is the incoming direction,
\vec{N} is the unit surface normal, and
\vec{R} is the reflected direction. Let's not assert it — let's
build it, and watch the angle come out equal on its own.
Deriving the bounce, line by line
Step 1 — split the incoming ray into two parts. Any vector hitting the
surface can be written as the part that points along the normal (straight into the
surface) plus the part that lies in the surface (sliding sideways):
\vec{D} = \vec{D}_{\parallel} + \vec{D}_{\perp},
where \vec{D}_{\parallel} runs along
\vec{N} and \vec{D}_{\perp} runs across
it (perpendicular to \vec{N}, i.e. tangent to the surface).
Step 2 — measure the normal part with a dot product. Because
\vec{N} is a unit vector, the
projection
of \vec{D} onto it is just the
dot product
\vec{D}\cdot\vec{N} times \vec{N}:
\vec{D}_{\parallel} = (\vec{D}\cdot\vec{N})\,\vec{N}.
Step 3 — the surface part is whatever's left over. Rearranging Step 1,
\vec{D}_{\perp} = \vec{D} - \vec{D}_{\parallel} = \vec{D} - (\vec{D}\cdot\vec{N})\,\vec{N}.
Step 4 — reflect by flipping only the normal part. Here is the entire idea
of a mirror. A bounce reverses the component going into the wall while leaving the
sideways slide untouched — the ray skids along the surface the same way, but its "into the
wall" motion becomes "out of the wall". So negate
\vec{D}_{\parallel} and keep
\vec{D}_{\perp}:
\vec{R} = \vec{D}_{\perp} - \vec{D}_{\parallel}.
Step 5 — substitute the two pieces back in. Putting Step 2 and Step 3 into
Step 4,
\vec{R} = \big[\vec{D} - (\vec{D}\cdot\vec{N})\,\vec{N}\big] - (\vec{D}\cdot\vec{N})\,\vec{N}.
Step 6 — collect the two identical terms. Both carry the factor
(\vec{D}\cdot\vec{N})\,\vec{N}, so they add to twice it:
\vec{R} = \vec{D} - 2(\vec{D}\cdot\vec{N})\,\vec{N}.
That's the reflection formula — three multiplies and a subtract per axis, and it's the
beating heart of every mirror in every raytraced frame.
Step 7 — check the angle really is preserved
A bounce should obey "angle in equals angle out". First, reflection keeps the ray's length:
squaring \vec{R} and expanding the dot product,
\vec{R}\cdot\vec{R} = \vec{D}\cdot\vec{D} - 4(\vec{D}\cdot\vec{N})(\vec{D}\cdot\vec{N}) + 4(\vec{D}\cdot\vec{N})^2\,(\vec{N}\cdot\vec{N}).
Since \vec{N} is a unit vector,
\vec{N}\cdot\vec{N} = 1, so the last two terms cancel and
\lVert\vec{R}\rVert = \lVert\vec{D}\rVert. Next, the angle each ray
makes with the normal is read off by dotting with \vec{N}:
\vec{R}\cdot\vec{N} = \vec{D}\cdot\vec{N} - 2(\vec{D}\cdot\vec{N})(\vec{N}\cdot\vec{N}) = \vec{D}\cdot\vec{N} - 2(\vec{D}\cdot\vec{N}) = -(\vec{D}\cdot\vec{N}).
Same magnitude, opposite sign: the outgoing ray leans away from the normal by exactly the
angle the incoming ray leaned toward it. Angle of incidence = angle of
reflection, proven, not assumed.
Let \vec{D} be an incoming direction striking a surface with
unit normal \vec{N}. The reflected direction is
\vec{R} = \vec{D} - 2(\vec{D}\cdot\vec{N})\,\vec{N},
and it has these properties:
-
It flips only the normal component of \vec{D}
(the part along \vec{N}) and leaves the tangential (in-surface)
part unchanged.
-
Angle of incidence = angle of reflection, since
\vec{R}\cdot\vec{N} = -(\vec{D}\cdot\vec{N}) and
\lVert\vec{R}\rVert = \lVert\vec{D}\rVert.
-
It needs a unit \vec{N}. The
\vec{N}\cdot\vec{N} = 1 step is what makes the formula exact;
an un-normalized normal scales the bounce wrongly.
Two silent bugs hide in this one line. First, \vec{N}
must be unit length — Step 7's proof of "angle in equals angle out" leaned
directly on \vec{N}\cdot\vec{N} = 1. Feed it a normal that's
twice as long and the reflected ray doesn't just come out twice as long: the term
2(\vec{D}\cdot\vec{N})\,\vec{N} gets scaled by the length
twice (once in the dot product, once in the trailing
\vec{N}), so the bounce is silently wrong in both size and
direction. Always renormalize a normal you compute yourself — say, from a cross product of
two edges — before reflecting with it.
Second, mind the sign convention of \vec{D}.
This page's formula assumes \vec{D} points into the
surface — the direction the ray is actually travelling. Some libraries instead hand you the
direction pointing back toward the ray's origin. Mix the two conventions — say, by
plugging in a flipped \vec{D} without flipping the formula — and
every reflected ray comes out pointing backwards. Pick one convention and stay consistent
with it everywhere in your raytracer.
The same bounce powers two things that look different on screen. A mirror
(a perfectly smooth surface) reflects the scene: the raytracer follows
\vec{R} off into the world and shows whatever it hits — this is
the recursive reflection ray you'll meet later.
A specular highlight — that bright glint on a glossy apple — is the same
reflection applied to the light rather than the scene. The surface looks brightest
where the reflected light direction points straight at your eye, so the glint sits exactly
where \vec{R} lines up with the view. Both are
\vec{R} = \vec{D} - 2(\vec{D}\cdot\vec{N})\,\vec{N} wearing
different costumes; we'll meet the highlight again in
the lighting equation.