Ray and Shape Intersections

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.

Ray intersecting a sphere at two values of t

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 normal
  • front_face
  • material reference
  • u, v when 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 = 0 and immediately re-hitting the shaded surface
  • Inconsistent winding across meshes
  • Forgetting a dedicated t_max on 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.