AnyLearn
All lessons

Ray Tracing and the Cost of Asking Anywhere

Ray tracing takes the opposite loop: for each pixel, find the geometry it hits. That buys visibility queries from any point in any direction, which is what shadows and reflections need. It also costs a scene-wide data structure, and the quality of that structure decides whether the renderer is usable.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 9

Walk the pixels, not the geometry

Swap the two loops from the previous lesson. For each pixel, construct a ray from the eye through it, and find the nearest surface that ray meets. The outer loop is now over pixels and the inner loop is over geometry.

The immediate consequence is that visibility stops being tied to the camera. A ray is just an origin and a direction, so once the machinery exists it answers the same question from anywhere: from a surface point toward a light, from a mirror in the reflected direction, from a shading point in a random direction.

That generality is the entire reason the architecture exists. Arthur Appel introduced ray casting for visibility and shadows in 1968, and Turner Whitted made it recursive in "An Improved Illumination Model for Shaded Display", Communications of the ACM, volume 23, issue 6, 1980, pages 343 to 349, spawning rays at each hit to follow reflection and refraction.

Full lesson text

All 9 steps on one page, for reading, reference, and search.

Show

1. Walk the pixels, not the geometry

Swap the two loops from the previous lesson. For each pixel, construct a ray from the eye through it, and find the nearest surface that ray meets. The outer loop is now over pixels and the inner loop is over geometry.

The immediate consequence is that visibility stops being tied to the camera. A ray is just an origin and a direction, so once the machinery exists it answers the same question from anywhere: from a surface point toward a light, from a mirror in the reflected direction, from a shading point in a random direction.

That generality is the entire reason the architecture exists. Arthur Appel introduced ray casting for visibility and shadows in 1968, and Turner Whitted made it recursive in "An Improved Illumination Model for Shaded Display", Communications of the ACM, volume 23, issue 6, 1980, pages 343 to 349, spawning rays at each hit to follow reflection and refraction.

2. The naive cost, and why it is fatal

Write the loop honestly and the problem is immediate. Testing every ray against every triangle is O(rays x triangles).

Put numbers on it. A 1920 by 1080 image is about two million primary rays. A modest scene has a million triangles. That is two times ten to the twelfth intersection tests for the first bounce, before a single shadow or reflection ray. At a billion tests per second that is over half an hour per frame.

And the exponent is the wrong shape. Doubling scene detail doubles the cost of every ray, which is not how the rasteriser behaved: there, extra triangles that end up hidden cost triangle setup and nothing more.

So ray tracing is not usable as stated. Everything practical about it comes from never performing most of those tests, and that requires knowing in advance which regions of space a ray cannot possibly hit.

3. The bounding volume hierarchy

The fix is a tree over the geometry. Wrap groups of triangles in simple volumes, usually axis-aligned boxes, and nest those volumes in larger ones up to a single root. That is a bounding volume hierarchy.

The traversal rule is one line of logic. Test the ray against a node's box. If it misses, every triangle beneath that node is discarded at once, with no further work. If it hits, descend into the children. At a leaf, test the handful of triangles directly.

A box test is cheap: for an axis-aligned box it is a few comparisons of interval overlaps along the three axes, with no square roots and no division beyond the reciprocal of the ray direction, which is computed once per ray.

The result turns the linear scan into something closer to logarithmic in scene size, which is the difference between half an hour and milliseconds.

4. Rejecting a subtree in one test

The leverage is entirely in the right-hand branch. One box test removes every triangle under that node, however many there are, at a cost independent of that number.

That is why the shape of the tree matters more than the speed of the intersection code. A hierarchy whose boxes overlap heavily forces the ray to descend both children repeatedly, and the rejections stop happening. A hierarchy with tight, well-separated boxes rejects early and often.

So the real engineering question is not how to traverse a BVH. It is how to build one whose boxes are worth testing.

flowchart TD
A["Root box: whole scene"] --> B["Left child box"]
A --> C["Right child box"]
B --> D["Leaf: a few triangles"]
B --> E["Leaf: a few triangles"]
C --> F["Ray misses this box"]
F --> G["Entire subtree skipped"]

5. The surface area heuristic

Splitting a node at the midpoint of its longest axis is easy and often poor. The standard answer estimates the cost of a proposed split before making it.

The key geometric fact: for rays with uniformly distributed origins and directions, the probability that a ray passing through a box also passes through a child box is proportional to the ratio of their surface areas. So a split's expected cost is SA(left)/SA(node) * N(left) + SA(right)/SA(node) * N(right), plus the cost of the box tests themselves.

Evaluate that for many candidate split positions and take the cheapest. This is the surface area heuristic, from J. David MacDonald and Kellogg S. Booth, "Heuristics for ray tracing using space subdivision", The Visual Computer, volume 6, 1990, pages 153 to 166.

Note what it optimises: not tree balance, but expected traversal cost. It will happily produce a lopsided tree if that isolates a dense cluster of geometry from empty space.

6. Traversal, in code

Traversal is a stack walk with one early exit that matters.

def trace(bvh, origin, direction, t_max=float("inf")):
    best_t, best_hit = t_max, None
    stack = [bvh.root]
    while stack:
        node = stack.pop()
        t_box = node.box.intersect(origin, direction)
        if t_box is None or t_box > best_t:
            continue                 # miss, or entirely behind a closer hit
        if node.is_leaf:
            for tri in node.triangles:
                t = tri.intersect(origin, direction)
                if t is not None and t < best_t:
                    best_t, best_hit = t, tri
        else:
            stack.append(node.far)   # push far first, pop near first
            stack.append(node.near)
    return best_hit, best_t

Two lines carry the performance. t_box > best_t prunes a node whose entire volume lies beyond the closest hit found so far. Pushing the far child first means the near child is visited first, which finds a close hit early and makes that prune fire more often.

A shadow ray can do better still: it does not need the nearest hit, only whether any hit exists, so it returns on the first one.

7. Where the time actually goes

Two costs dominate, and neither is the arithmetic of ray against triangle.

The first is memory. Traversal is a pointer chase through a tree whose nodes are scattered, and each step depends on the result of the previous one. It is latency bound rather than compute bound, which is the opposite of the rasteriser's inner loop.

The second is divergence. On hardware that executes many rays in lockstep, rays in the same group take different branches through the tree. Once they diverge, the group runs the union of both paths, and the effective width collapses. Primary rays from neighbouring pixels stay coherent; reflection rays off a bumpy surface do not, and randomly scattered rays diverge almost immediately.

This is why hardware ray tracing units exist: they implement box tests and traversal directly, with memory layouts designed for the access pattern, rather than leaving it to general shader cores.

8. The cost of a scene that moves

The BVH is a global structure built from the whole scene, which is exactly what the rasteriser avoided, and the bill arrives when the scene changes.

Moving one object invalidates every box containing it, up to the root. Rebuilding a high-quality tree with a full surface area heuristic is far too slow to do per frame for a large scene.

The standard resolution is a two-level structure. Each object gets its own BVH in its own coordinate frame, built once, and a small top-level tree holds one entry per instance with its transform. Moving an object updates only the top level, and a ray entering an instance is transformed into that object's local space rather than the object being rebuilt.

Deforming geometry, such as an animated character, is the case this does not solve. Its bottom-level tree must be refitted or rebuilt, and refitting preserves the topology while letting box quality decay as the pose drifts from the one it was built for.

9. What the second architecture bought

PropertyRay tracing
Outer loopPixels, then rays
Cost driverRay count times traversal depth
Scene scalingRoughly logarithmic, given a good tree
VisibilityNearest hit along any ray
Query availableFrom any origin, in any direction
NeedsA scene-wide acceleration structure
Hurts whenRays diverge, or geometry deforms

The fifth row is what the previous lesson could not offer at any price, and it is why shadows, reflections, refraction and indirect light fall out of this architecture rather than being bolted on.

The sixth and seventh rows are what it gave up. A rasteriser needs no global structure and does not care whether anything moves.

Neither list dominates the other, which is the actual state of the field and the subject of the final lesson.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Why is the naive ray tracing loop unusable, beyond simply being slow?
    • It cannot compute shadows without a second pass
    • Cost is rays times triangles, so doubling scene detail doubles the cost of every ray
    • It requires the scene to fit in a single memory page
    • Ray-triangle intersection has no closed-form solution
  2. What does a single failed box test in a BVH accomplish?
    • It rejects every triangle beneath that node at a cost independent of how many there are
    • It rejects one triangle and advances to the next
    • It moves the ray to the next pixel
    • It marks the node for rebuilding
  3. What does the surface area heuristic actually optimise?
    • The balance of the tree, so both children hold equal triangle counts
    • The total memory used by the hierarchy
    • Expected traversal cost, using surface area ratio as the probability a ray enters a child
    • The number of triangles tested per leaf
  4. In the traversal code, why is the far child pushed onto the stack before the near child?
    • To keep the stack depth bounded
    • Because far nodes have larger boxes and cost more to test
    • So that leaves are always reached in the same order
    • So the near child is popped first, finding a close hit early and making the distance-based prune fire more often
  5. Why do incoherent rays hurt performance on wide parallel hardware?
    • They require more triangles to be stored per leaf
    • Rays executed in lockstep take different branches, so the group runs the union of both paths and effective width collapses
    • They cannot use the surface area heuristic
    • They force the BVH to be rebuilt each frame

Related lessons

Computer Science
advanced

Conditioning and Stability: Whose Fault Is the Error

Two different things can make a numerical answer inaccurate: the problem may amplify any input perturbation, or the algorithm may introduce its own. They are separate quantities, one belongs to the problem and one to the code, and only the second is fixable. This lesson separates them and shows what follows.

8 steps·~12 min
Computer Science
advanced

Cancellation: Where the Digits Actually Go

Every operation is correctly rounded, yet results can be wrong in the first digit. The reason is that subtracting nearly equal numbers is exact and still catastrophic: it does not create error, it promotes error that was already there. This lesson builds that mechanism and the summation techniques that recover the lost accuracy.

8 steps·~12 min
Programming
advanced

Undo: Reset, Revert, Restore, and Getting Work Back

Git's undo commands are notoriously confusing because they are usually memorised as recipes. Read against the object model they separate cleanly: each acts on a different one of the three places content lives. This lesson maps them, covers recovery from the accidents that feel unrecoverable, and states what genuinely cannot be undone.

8 steps·~12 min
Programming
advanced

Merge and Rebase: Two Answers to Divergence

Every operation so far moved a pointer. Merging is the first that has to decide something: two branches changed the same project and one result must come out. This lesson covers the three-way merge and the merge base it depends on, what a conflict actually is, and why rebase produces different commits rather than moving them.

8 steps·~12 min