RT Graphics Programming
Lesson 13 of 28

Part 1 · Ray tracing by hand

13. Sampling the Pixel

Replace the convenient centre of a pixel with several measured positions and explain what the extra work buys.

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

Every image so far has used the centre of each pixel. The rule is deterministic, cheap and easy to inspect. It is not a complete measurement of the pixel region. A surface boundary, thin triangle or distant checker pattern can change within that region. One centre ray sees one side of the change and stores it as though the whole pixel agreed.

Anti-aliasing is sometimes presented as a filter applied after rendering. A blur can hide steps, but it cannot recover visibility information that was never sampled. We shall instead take several positions inside the pixel, trace each independently and average their contributions. This increases work in direct proportion to sample count, so the improvement must be understood alongside its cost.

What you should be able to account for

  • Explain why a centre sample is an estimate of a pixel region rather than the pixel itself.
  • Generate repeatable stratified sample positions and average their linear colour contributions.
  • Distinguish spatial aliasing from sampling noise and relate both to measured ray count.

Sample positions belong inside the pixel

The primary camera mapping used x + 0.5 and y + 0.5. Replace those fixed offsets with offsetX and offsetY in the interval from zero to one:

const u = (x + offsetX) / width;
const v = (y + offsetY) / height;
const ray = cameraRay(u, v);

Four samples could all be chosen independently at random. That may leave one part of the pixel unmeasured while clustering samples elsewhere. Stratification first divides the pixel into regions and takes one jittered position from each. For four samples we use a two by two arrangement; for nine, a three by three arrangement.

function stratifiedOffsets(side, random) {
    const offsets = [];
    for (let row = 0; row < side; ++row) {
        for (let column = 0; column < side; ++column) {
            offsets.push([
                (column + random()) / side,
                (row + random()) / side
            ]);
        }
    }
    return offsets;
}

The jitter breaks repeated alignment with scene detail, while the strata guarantee basic coverage. This does not make four samples exact. It is a more deliberate estimate than four unconstrained positions and a more informative experiment than claiming the pixel centre represents the whole area.

Average before display encoding

Trace each sample and accumulate its linear RGB contribution. Divide by the number of samples, then apply the display transfer used by the renderer. Averaging already gamma-encoded byte values gives a different and generally incorrect result because the encoding is non-linear.

let sum = [0, 0, 0];

for (const [offsetX, offsetY] of offsets) {
    const ray = cameraRay(
        (x + offsetX) / width,
        (y + offsetY) / height
    );
    sum = add(sum, trace(ray, bounceLimit));
}

const linearPixel = scale(sum, 1 / offsets.length);
writeDisplayColour(pixel, linearPixel);

Our compact renderer applies an approximate power of 1/2.2 before converting to bytes. A complete colour pipeline would define colour space, transfer function, exposure and tone mapping more carefully. The ordering remains the important lesson: combine light-like values in their linear representation, then encode for display.

Pixel-sampling laboratory

Count Samples, Not Pixels

Keep the 240 by 150 raster fixed, change the samples evaluated inside each pixel and account for the resulting primary camera work.

Change this, then watch this: Change the sample count. The marks inside one pixel and the primary-ray work change; the raster remains 36,000 stored pixels.

Reconstructed imageThe same scene reconstructed using the selected number of samples for each pixel.Ready
The same scene reconstructed using the selected number of samples for each pixel.
One pixel regionOne pixel divided into strata with every evaluated sample position marked.Calculation
One pixel divided into strata with every evaluated sample position marked.
Raster
240 × 150 = 36,000 pixels
Samples per pixel
4
Primary samples
0
Relative camera work

36,000 pixels × 4 samples = 144,000 primary samples

What this establishes:

Compare error and work together

Use one sample and inspect sphere silhouettes, triangle edges and the distant checkerboard. Then choose four or nine. Boundary pixels begin to represent mixtures of the surfaces covered by different sample positions, so edges become less stair-stepped. Fine checker detail may become noisy because a small finite set now reports varied coverage instead of the stable but systematically wrong centre result.

The primary-ray counter should multiply by the sample count. Secondary-ray counts increase according to what those new primary rays hit. A four-sample image is not merely four times the camera work; it may generate four families of shadows, reflections and transmissions. The primitive-test counter reveals that consequence more honestly than sample count alone.

A boundary pixel

Four stratified samples are traced. Three hit a red sphere and return (0.8, 0.2, 0.1). One misses and returns sky (0.2, 0.4, 0.8). The linear average is ((3 × 0.8 + 0.2)/4, (3 × 0.2 + 0.4)/4, (3 × 0.1 + 0.8)/4) = (0.65, 0.25, 0.275). The pixel now represents estimated coverage rather than choosing one side of the silhouette.

Repeatability matters while debugging

If sample positions change on every render, the image changes even when the code and scene do not. That is useful when accumulating samples over time, but it makes a single-frame comparison harder to diagnose. The laboratory uses a deterministic pseudo-random value derived from pixel address and sample index. The pattern has the same inputs and therefore repeats.

Determinism does not make a poor generator statistically good. It establishes reproducibility. A renderer intended for high-quality sampling needs better sequences, multidimensional decorrelation and tests for bias. We first need to distinguish a code change from a different random draw.

Aliasing and noise are different failures

Aliasing is structured error caused when the sample pattern cannot represent or distinguish higher-frequency variation. Noise is visible variance from an estimator that changes with finite samples. Jitter can turn conspicuous structured aliasing into less structured noise, which is often easier to reduce by adding samples. Calling both problems jaggies does not help us choose a remedy.

A post-process denoiser can estimate a smoother image from noisy inputs and auxiliary data. It does not make the original estimator free or prove that missed thin geometry was sampled. We are not adding one here because the purpose is to see what the sample positions themselves establish.

Account for the average

Nine samples are used. Six return linear colour (0.6, 0.3, 0.1) and three return (0.0, 0.3, 0.7). What linear colour is stored before display encoding?

Reveal the calculation

The red sum is 6 × 0.6 + 3 × 0 = 3.6, green is 9 × 0.3 = 2.7, and blue is 6 × 0.1 + 3 × 0.7 = 2.7. Dividing by nine gives (0.4, 0.3, 0.3).

Measure one difficult edge

Choose a pixel on a triangle edge. Record every sample position, hit object and linear contribution for one, four and nine samples. Draw the pixel region and mark the positions. Explain the final averages using those records, then compare primitive-test counts. The smoother image is the consequence; the sampled routes are the evidence.

Keep this model for Vulkan

GPU execution makes many samples practical, but it also makes random-state layout, accumulation images and synchronisation important. A Vulkan implementation may distribute samples across invocations or frames. It must still generate a known sample position, trace the corresponding route, combine linear contributions and expose when the accumulated value is safe to read.

We now have all of the visual mechanisms required for the manual scene. Lesson 14 assembles them, separates the different ray budgets and asks whether testing every object for every ray is still an appropriate traversal.