RT Graphics Programming
Lesson 14 of 28

Part 1 · Ray tracing by hand

14. A Complete Scene and the Cost of Seeing It

Assemble the renderer, measure the questions it asks and prepare the model that the Vulkan half will implement.

Graphics glossary 37 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.

A completed picture is not the end of the renderer. It is the first point at which all of its responsibilities can be examined together. The camera generates samples, geometry supplies candidates, closest-hit traversal selects visibility, materials create local and secondary contributions, and the pixel averages the results. If any one of those stages is hidden behind the final scene, the course has produced an image without producing the understanding it was intended to develop.

We shall therefore finish by making an account. The laboratory separates primary, shadow, reflection and transmission rays, then counts primitive and bounding tests. It also compares linear sphere traversal with two simple bounded groups. The grouped version is not a full production bounding volume hierarchy. It is the smallest experiment that demonstrates why an acceleration structure can reduce work without changing the required closest hit.

What you should be able to account for

  • Describe the complete route from pixel sample to final encoded colour.
  • Use separate counters to explain where ray and intersection work originates.
  • Apply an axis-aligned bound to reject a group and state what would be required to generalise the idea into a BVH.

The complete route

ResponsibilityInputOutput or decision
Pixel samplingInteger pixel and sample offsetNormalised image coordinates
CameraSample coordinates and camera basisPrimary origin and direction
Primitive geometryRay and intervalCandidate hit record or miss
Scene traversalCandidate recordsClosest accepted record or any-hit answer
Material and lightHit record, lights and path stateLocal contribution and possible secondary rays
Path terminationRemaining depth and contribution policyContinue or return a bounded result
ReconstructionLinear sample contributionsAverage linear pixel colour
Display encodingLinear pixel colourStored RGB bytes for Canvas

This table is a debugging route. A wrong pixel can be followed backwards to its sample positions, rays, hit records and material decisions. The stages are not independent in execution, but they have distinct criteria. Keeping those criteria visible prevents a plausible final colour from excusing an unexplained intermediate state.

Linear traversal has a clear cost

With R rays and N primitives, testing every primitive gives up to R × N primitive tests. Early any-hit exits and shrinking closest-hit intervals may reduce actual work, but the traversal still attempts many objects that are nowhere near the ray.

Adding samples multiplies primary routes. Shadows and recursive materials add secondary routes according to what those samples hit. The lesson counters therefore distinguish ray categories before reporting primitive tests. Render time alone is not enough: it varies with browser, device, background work and warm-up. The operation counts describe the algorithmic workload of this particular scene more directly.

Complete-renderer laboratory

Compare the Work of Two Traversals

Keep scene and sampling settings fixed, switch between linear and bounded sphere traversal, and compare the complete ray and intersection accounts.

Change this, then watch this: Change the traversal and render again. A correct result keeps the picture fixed while the primitive and bounding-test counts change.

Complete sceneThe assembled teaching scene rendered with sampling, shadows, reflection, refraction and the selected traversal.Ready
The assembled teaching scene rendered with sampling, shadows, reflection, refraction and the selected traversal.
Renderer work flowThe camera, intersection, shading, secondary-ray and pixel stages of the complete renderer.Calculation
The camera, intersection, shading, secondary-ray and pixel stages of the complete renderer.
Primary rays
0
Shadow rays
0
Reflection rays
0
Transmission rays
0
Primitive tests
0
Bounding tests
0
Total traced rays
0
Render time
0 ms

Total rays and intersection work will be calculated after rendering.

What this establishes:

Reject a group using an axis-aligned box

An axis-aligned bounding box stores minimum and maximum coordinates on X, Y and Z. A ray can intersect the allowed parameter interval for each pair of slabs. If those three intervals do not overlap, the ray cannot reach anything wholly contained by the box.

function hitBounds(ray, box, tMin, tMax) {
    for (let axis = 0; axis < 3; ++axis) {
        const direction = ray.direction[axis];
        if (Math.abs(direction) < 1e-12) {
            const outside = ray.origin[axis] < box.minimum[axis]
                         || ray.origin[axis] > box.maximum[axis];
            if (outside) return false;
            continue;
        }

        const inverse = 1 / direction;
        let near = (box.minimum[axis] - ray.origin[axis]) * inverse;
        let far  = (box.maximum[axis] - ray.origin[axis]) * inverse;

        if (inverse < 0) {
            [near, far] = [far, near];
        }

        tMin = Math.max(tMin, near);
        tMax = Math.min(tMax, far);
        if (tMax < tMin) {
            return false;
        }
    }
    return true;
}

The laboratory puts spheres into two groups and computes a bound around each. A ray tests both bounds. Only a group whose box is hit has its member spheres tested. Planes remain outside because an infinite plane does not fit a finite world-space box. Triangles could be grouped in the same way, but the small demonstration keeps the before-and-after count easy to inspect.

When a bound earns its cost

A group contains ten spheres. Testing its box costs one bounding test. If the ray misses the box, ten primitive tests are avoided. If the ray hits the box, the bound has added one test and all ten spheres may still be examined. Acceleration is therefore not free. It depends on bounds that reject enough work and a hierarchy that narrows candidates efficiently.

From two groups to a hierarchy

A bounding volume hierarchy places bounds in a tree. The root encloses a large set. Child bounds divide it, and leaves reference small primitive groups. A traversal tests the root, descends only through intersected children and can order children by near distance to find a close hit earlier. A good build tries to balance the cost of more bound tests against the primitive tests they are likely to reject.

Our two groups demonstrate the rejection mechanism but not a general BVH builder, surface-area heuristic, dynamic update policy or robust traversal stack. Calling it a complete BVH would overstate the result. It is a bounded-group precursor whose counters let us see the operative idea before the data structure becomes elaborate.

Correctness remains the first gate

Switch between linear and bounded traversal. The rendered image should remain the same for the same sample positions and scene. Primitive-test counts should change; visibility should not. If the accelerated image differs, the lower count is not an optimisation result. It is evidence that a required candidate was incorrectly rejected or visited with the wrong interval.

This is the necessary order of judgement. First establish agreement with the simple traversal across deliberately difficult rays, including box boundaries, zero direction components and objects touching bounds. Then measure whether work or time improves. Speed obtained by skipping visible geometry is merely a faster wrong answer.

Decide whether the bound helped

A group contains 20 primitives. Across 1,000 rays, its box is missed 800 times and hit 200 times. Assume a hit tests all 20 members. Compare the primitive and bounding test counts with testing all group primitives for every ray.

Reveal the account

Linear traversal performs 1,000 × 20 = 20,000 primitive tests. The grouped traversal performs 1,000 bounding tests and 200 × 20 = 4,000 primitive tests. It replaces 16,000 primitive tests with 1,000 simpler bound tests in this account. Whether wall-clock time improves still needs measurement on the target implementation.

Read the manual renderer as source

The live course uses one cumulative source file so every lesson stage can be compared without loading a hidden service or external library. Open the browser ray tracer source and locate the vector operations, camera construction, three primitive tests, scene traversal, local shading, secondary-ray creation, sampling loop and metrics. The code is intentionally compact. Compactness is useful only while each responsibility can still be identified.

Canvas receives an ImageData buffer. It does not intersect a sphere, choose a triangle, cast a shadow or refract a ray. That boundary was stated in Lesson 1 and remains true in the completed scene. The browser API displays our answer; it has not answered the ray-tracing questions for us.

Perform the Part I audit

Select one ordinary diffuse pixel, one shadowed pixel, one mirror pixel and one glass pixel. For each, record the sample position, primary direction, first hit record and every secondary ray category it creates. Reconcile your four traces with the counters. Then switch traversal mode and confirm the final hits and colours remain unchanged while the test account changes.

Finally, add one small sphere outside both manual group bounds without rebuilding them. Observe the incorrect accelerated result, explain exactly which bound rejected the required primitive and repair the construction. This controlled failure is a better test of your understanding than another attractive arrangement of spheres.

The contract for the second half

The next fourteen lessons will implement graphics responsibilities through Vulkan rather than extend this JavaScript renderer. They should return to the artefacts we can now name: image coordinates, camera state, rays and intervals, triangle data, hit attributes, material parameters, secondary-ray state, sample accumulation and acceleration structure traversal. Vulkan will introduce explicit resources, shaders, command recording, synchronisation and hardware execution. Those mechanisms are not substitutes for the manual model. They are ways of organising and executing it at a different scale.

No Vulkan page is being filled with placeholder material here. Its sequence requires its own code baseline, device assumptions and validation plan. Part I ends with a working reference and an account against which that later implementation can be compared.

You have now constructed a ray traced scene by hand. The important result is not the arrangement of spheres. It is that a pixel can be challenged: which sample created it, which ray carried that sample, which interval admitted the hit, which surface record survived, which secondary routes contributed and what work was spent. When those questions have answers, the image is no longer a convincing black box.