Ray and Shape Intersections
Intersection converts a ray into a hit record. You need the nearest valid parameter t, the point, the geometric normal, and enough data to shade. Everything else is secondary.
Parametric ray:
r(t) = o + t * d
This text assumes unit d. If you leave directions unnormalized, scale the formulas accordingly and stay consistent.
A ray may miss, graze, or intersect a sphere twice. The renderer keeps the nearest t inside the valid interval.
Sphere
For center c and radius R:
(o + t d - c) · (o + t d - c) = R^2
This is a quadratic equation a t^2 + b t + c = 0. With unit d:
oc = o - c
a = 1
b = 2 (oc · d)
c = (oc · oc) - R^2
discriminant = b^2 - 4 a c
No real root means a miss. Two roots are entry and exit. Test the smaller positive root first, then the larger, against [t_min, t_max].
p = o + t d
n = normalize(p - c)
For transmission through a hollow sphere you must know whether the ray is inside. Store face orientation explicitly. Do not rediscover it later by folklore.
Plane and triangle
Plane through p0 with normal n:
t = ((p0 - o) · n) / (d · n)
If the denominator is nearly zero, the ray is parallel. Reject t outside range.
A triangle is the plane test plus a point-in-triangle condition. Möller–Trumbore is the standard compact solution and yields barycentric coordinates. You need barycentrics as soon as normals or UVs are interpolated.
// conceptual form
edge1 = v1 - v0
edge2 = v2 - v0
// solve o + t d = v0 + u edge1 + v edge2
// accept if t in range and u >= 0, v >= 0, u + v <= 1
Geometric normal: normalize(cross(edge1, edge2)). Winding defines the front face.
Nearest hit over a scene
closest = t_max
hit = none
for each shape:
if shape.hit(ray, t_min, closest, temp):
closest = temp.t
hit = temp
return hit
Shrink t_max immediately after a hit. That prunes farther work and is the seed of BVH traversal.
Hit records
Minimum fields:
t,p, geometric normalfront_face- material reference
u, vwhen textured
Set front_face from dot(d, outward_normal) < 0. Shaders often want a normal facing the ray. Shadow tests may still need the geometric side. Keep both concepts available.
Robustness
Reject candidates outside the interval before writing the hit. Grazing rays and needle triangles are numerically hostile. On large scenes, a pure absolute epsilon can fail; scale tolerances with scene size when needed. Drop zero-area primitives at load time rather than hoping intersection code survives them.
Shadow rays should use an any-hit path that can exit early. Closest-hit traversal is for shading. Binary visibility only needs yes or no.
Common errors
- Taking the farther sphere root first
- Leaving
t_min = 0and immediately re-hitting the shaded surface - Inconsistent winding across meshes
- Forgetting a dedicated
t_maxon shadow rays - Allocating full shading records when a boolean occlusion query would do
Prove spheres and triangles with a normal-colored debug view under camera motion. If that view is clean, shading has a foundation.
Why unit directions simplify teaching
With unit d, sphere a = 1 and many t values are true distances. That helps debugging. Production code sometimes keeps non-unit directions for transform convenience. Both are valid. The error is mixing them inside one intersection routine. Document the invariant at the top of the file.
Shared edge policy
Two triangles sharing an edge may both claim a ray. Either winner is acceptable if the choice is stable. Sparkling silhouettes under tiny camera motion usually come from numerical t noise or secondary-ray bias, not from the existence of a shared edge. Stabilize the interval and bias before inventing geometric topology hacks.