RT Graphics Programming
Lesson 05 of 28

Part 1 · Ray tracing by hand

5. The Closest Visible Surface

Add several objects and make a deliberate decision about which result a pixel is permitted to keep.

Graphics glossary 26 terms mentioned in this lesson

Select any term for a clear definition. The return button brings you back to the exact term link you used.

One sphere allowed the intersection function to return a distance. Several spheres expose a decision that was previously invisible: a pixel may receive more than one valid hit, but only the nearest visible surface is allowed to determine the first result. The array order is not depth. The last function called is not the front object. Visibility must be earned by comparing valid ray parameters.

This sounds obvious when two spheres are clearly separated. It becomes less obvious when they overlap in the image, one contains another or a later secondary ray begins inside a surface. A reliable renderer does not infer front and back from how the scene was assembled. It gives every object the same interval query and retains the smallest valid answer.

What you should be able to account for

  • Define a hit record containing the distance and surface information required by later stages.
  • Traverse a scene while shrinking the maximum accepted distance.
  • Demonstrate that changing object storage order does not change the visible result.

Return a record, not an isolated number

The hit distance is sufficient for a silhouette. Shading will need the world position, surface normal and material as well. Returning separate pieces from separate functions encourages them to disagree. We therefore create one record at the point where the geometry has established a valid root.

function makeHit(ray, sphere, t) {
    const position = pointOnRay(ray, t);
    const outwardNormal = scale(
        subtract(position, sphere.centre),
        1 / sphere.radius
    );

    return {
        t,
        position,
        outwardNormal,
        material: sphere.material,
        object: sphere
    };
}

The normal and material are not used to decide which sphere is closest. They are retained because the successful geometric answer must carry enough context into the next stage. This is a contract. A future plane or triangle may calculate the record differently, but callers should not need a different record shape merely because a different primitive was hit.

Shrink the interval as evidence improves

Begin with no record and an upper limit representing the farthest useful distance. Test each object using that upper limit. When a hit is found, store it and replace the upper limit with its t. Any later object must now produce a smaller valid value to replace the record.

function closestHit(ray, objects, tMin, tMax) {
    let closest = tMax;
    let best = null;

    for (const object of objects) {
        const hit = object.intersect(ray, tMin, closest);
        if (hit !== null) {
            closest = hit.t;
            best = hit;
        }
    }

    return best;
}

The shrinking maximum does two jobs. It states the visibility rule and lets each later intersection avoid returning a result already known to be hidden. It does not yet avoid attempting the primitive test. Lesson 14 will measure that remaining cost and introduce bounds that can reject groups before their individual tests run.

Three valid roots, one visible surface

Suppose the first sphere reports t = 6.2. The current maximum becomes 6.2. The second reports t = 2.7, so it replaces the record and the maximum becomes 2.7. The third has a root at 4.1, but its intersection function is asked to search only up to 2.7 and therefore reports no acceptable hit. The visible result is the second sphere. Reversing the object order may change the sequence of temporary records, but it must still finish at 2.7.

Visibility laboratory

Keep the Smallest Valid Distance

Move the blue sphere along the ray, compare its near hit with the red sphere's near hit and retain the smaller acceptable value of t.

Change this, then watch this: Move only the blue sphere. Watch its near value of t cross the red value and change which object is retained.

Overlapping surfacesOverlapping spheres whose visible colour is selected by the nearest valid hit.Ready
Overlapping spheres whose visible colour is selected by the nearest valid hit.
Compare two valid hitsOne ray with red and blue candidate hit distances and the retained nearest result.Calculation
One ray with red and blue candidate hit distances and the retained nearest result.
Red near hit
2.340
Blue near hit
1.900
Retained object
Blue sphere
Stored closest t
1.900

min(2.340, 1.900) = 1.900

What this establishes:

Use distance without pretending it is colour

The laboratory gives each sphere an albedo so that overlapping silhouettes can be distinguished. It also applies a slight distance diagnostic, making farther unlit objects darker. This is not lighting. It is a temporary way to make the retained t visible while we check occlusion.

Move the blue sphere along Z. When it passes behind the red sphere, some camera rays continue to intersect both. The red hit wins only where its accepted distance is smaller. Around the non-overlapping part of the blue silhouette, the red sphere provides no valid root and the blue record remains. One object is not globally in front of another; visibility is decided independently for each ray.

Intervals give rays different purposes

A primary ray usually searches from a small positive minimum to a far limit. A shadow ray will search only from a surface to the light. A reflection ray will again search forward from a surface, but it may have a remaining bounce budget. The same scene traversal can serve each purpose when the ray interval is an explicit input.

Using positive infinity as the maximum is mathematically convenient. A practical renderer may use a finite scene limit, both to express the problem and to avoid allowing absurd values to leak into later calculations. The important point is not which constant we choose for this small scene. It is that the caller owns the interval and the primitive honours it.

Find the storage-order error

A loop assigns best = hit every time any object reports a valid positive root. The image changes when the scene array is reversed. What criterion is missing, and where should it be enforced?

Reveal the diagnosis

The loop is retaining the last valid hit rather than the closest valid hit. It must compare against the current closest distance, preferably by passing that value as the primitive's tMax and replacing the record only when a result lies inside the reduced interval. The final answer should then be independent of array order, apart from genuinely equal-distance ties that need their own policy.

Equal values still require a policy

Two surfaces can occupy the same distance, either by design or because the scene contains overlapping geometry. The loop above keeps the later hit when equality is accepted. Changing the comparison to exclude the current maximum keeps the earlier hit. Neither policy repairs coincident geometry, and neither should be mistaken for a physically meaningful choice. The renderer must document the tie behaviour, while the scene author should avoid unintended coincident surfaces.

This qualification matters because deterministic output does not prove the scene is sensible. A consistent arbitrary tie may be useful for debugging, but it does not tell us which of two identical surfaces is really visible. Geometry that does not define a unique nearest surface cannot obtain one merely from a loop.

Prove order independence

Create three spheres whose image projections overlap. Record the centre pixel's candidate distances and final object. Reverse the array, then rotate it by one position. The candidate order should change and the final smallest distance should not. Add two coincident spheres afterwards and state the tie rule you observe.

Keep this model for Vulkan

Hardware ray tracing uses an acceleration structure and a defined traversal process to find candidates, but the conceptual result remains a closest accepted hit. In a compute implementation, we may perform the loop ourselves. In either case, the shader consuming the result needs a stable hit contract rather than a collection of primitive-specific accidents.

We can now say which surface a primary ray sees. Lesson 6 turns the stored hit into a surface description by orienting its normal and separating geometric data from material data.