Floors, walls, ceilings, the still surface of water — the flat world of a game is made of
planes.
Firing a
ray
at one is the cheapest intersection in the whole tracer: where the
sphere
gave us a quadratic, the plane gives us a single linear equation, solved with one
division. No discriminant, no \pm — just one
t.
From plane equation to one division, line by line
A plane is fixed by a normal n and a constant
d: a point P lies on it exactly when its
projection onto the normal equals d.
n\cdot P = d.
Step 1 — substitute the ray. A point is on the plane when it satisfies this,
and on the ray when it equals O + tD. Demanding both, put the ray in
for P:
n\cdot(O + tD) = d.
Step 2 — distribute the dot product. The dot product is linear, so it splits
the sum and pulls the scalar t out:
n\cdot O + t\,(n\cdot D) = d.
Step 3 — isolate the only unknown. Everything except
t is a known number. Move n\cdot O across
and divide by n\cdot D:
t = \frac{d - n\cdot O}{n\cdot D}.
That is the entire intersection — one dot product on top, one on the bottom, one division.
Step 4 — watch for n\cdot D = 0: the parallel case.
If the ray's direction is perpendicular to the normal, the ray runs flat along the
plane and the denominator vanishes. There is no single crossing point: either the ray never
meets the plane (it floats above or below it forever) or it lies entirely within it. Geometrically
n\cdot D = 0 means the ray is parallel to the surface —
the one case the formula refuses to answer, so we test for it first and report "no hit".
n\cdot D = 0 \quad\Longrightarrow\quad \text{ray parallel to the plane — no intersection.}
Step 5 — reject hits behind the origin. A solved
t < 0 sits behind the camera — the plane is in the rear-view
mirror, not in front of the ray. Keep it only when t \ge 0.
Step 6 — recover the hit point. As always, feed the surviving
t back into the ray:
P_{\text{hit}} = O + t\,D.
For a ray P(t) = O + tD and a plane
n\cdot P = d:
-
One linear solve — the hit parameter is
t = \dfrac{d - n\cdot O}{n\cdot D}.
-
Parallel ⇒ no hit — when n\cdot D = 0 the ray is
parallel to the plane; there is no single crossing (test this first).
-
Forward only — keep the hit when t \ge 0;
t < 0 is behind the origin.
-
Hit point — the surface point is
P_{\text{hit}} = O + t\,D.
The formula t = (d - n\cdot O)/(n\cdot D) looks like ordinary
arithmetic, so it's tempting to just compute it and move on. But when the ray runs exactly
parallel to the plane, n\cdot D = 0, and dividing by
it doesn't raise a helpful error in most languages — it quietly produces
Infinity, -Infinity, or NaN, depending on the sign of the
numerator.
A renderer that skips the check will pass one of those non-values downstream as if it were a
real hit distance, corrupting depth comparisons or crashing far from the actual bug. The fix is
to test n\cdot D against a small tolerance before dividing:
treat it as no intersection (or, in the rarer case where the ray also lies within the plane, as
infinitely many intersections), and only divide once the denominator is safely nonzero.
The formula intersects an infinite plane: a sheet of glass stretching forever in
every direction. Solving for t always finds the crossing (unless
the ray is parallel), even if that crossing is a mile off the edge of the actual floor tile.
Real geometry is bounded — a floor, a quad, a window. So a ray–plane hit is usually only
step one: compute P_{\text{hit}} = O + tD on the infinite plane,
then test whether that point lies inside the bounded patch. For an
axis-aligned rectangle that is a pair of range checks on two coordinates; for an arbitrary
quad or
triangle
it's a containment test in the plane. The plane gives you the where on the infinite
sheet cheaply; the boundary test decides whether you actually landed on the surface or
shot past its edge into the void.