Path Tracing in Practice

Path Tracing in Practice

Path tracing is where the previous chapters stop being isolated drills. Cameras, intersections, materials, sampling, and acceleration meet inside one estimator of the reflection equation.

The discipline remains the same: estimate in linear space, accumulate many samples, convert for display only at the end.

A path carrying throughput from the camera to a light

Throughput is the running weight of the path from the camera to the current vertex.

Integrator loop

for each sample:
    ray = camera ray
    throughput = 1
    color = 0
    for bounce = 0..max_depth:
        hit = scene.intersect(ray)
        if no hit:
            color += throughput * environment(ray)
            break

        color += throughput * emission(hit)

        wi, pdf, f = sample_material(hit, -ray.dir)
        if pdf == 0 or f == 0: break
        throughput *= f * cosθ / pdf

        if bounce >= 3:
            q = clamp(max_component(throughput), 0.05, 0.95)
            if random() > q: break
            throughput /= q

        ray = Ray(offset(hit), wi)

    accumulate(color)

Throughput is the product of BRDF weights along the path. It answers how much a light found later still matters to the camera.

Next event estimation

Pure path tracing collects light only when a path happens to hit a lamp. The algorithm is simple and noisy. Next event estimation samples a light from the current hit and adds that contribution with a shadow ray. Practical scenes almost always want it.

Perfect mirrors and glass still require BSDF sampling. Light sampling alone will not find a Dirac reflection of a lamp.

Russian roulette

After a few bounces, terminate the path with probability and compensate:

throughput /= continueProbability

Without compensation the image darkens as soon as roulette begins. That missing division is a classic bias. Do not enable roulette on the first bounces; early vertices still carry large energy.

Accumulation and display

buffer[pixel] += sample
display = tone_map(buffer[pixel] / sample_count)

Progressive rendering displays buffer / count while count grows. Fireflies are rare high-contribution samples. Investigate sampling and materials before clamping. Clamping is bias.

Module boundaries that survive contact with reality

  • Camera: only generates rays
  • Scene / BVH: only answers hits
  • Material: samples and evaluates
  • Integrator: owns the bounce loop
  • Film: owns accumulation and output

Keep transport estimation separate from display conversion. When the picture is wrong, that boundary tells you which side to interrogate.

Acceptance checks

  1. Normal-colored primary hits remain correct under the same camera.
  2. A diffuse box converges without brightness that depends on sample count.
  3. A mirror shows sharp reflected lamps and geometry.
  4. Glass shows reflection, refraction, and total internal reflection without black rims from missing side logic.
  5. Raising samples from 16 to 256 reduces noise without shifting the mean.

If brightness drifts with sample count, the estimator is sample-count dependent. Search for a missing PDF, a bad average, or early tone mapping.

Common errors

  • Tone mapping every bounce
  • Russian roulette without throughput compensation
  • Next event estimation alone on pure specular surfaces
  • No maximum depth in dark cavities
  • Comparing denoisers before the raw integrator converges

When the path tracer is honest, noise is the remaining adversary, not confusion in the mathematics. That is the correct problem. Better samplers, multiple importance sampling, and denoising come after the estimator tells the truth.

Specular and rough surfaces

As roughness goes to zero, a microfacet lobe approaches a Dirac mirror. Sampling must follow that limit. A pure light-sampling path will fail. A pure BSDF path remains correct for pure specular transport. This is why production integrators combine strategies. Learn each strategy alone before mixing them.

Reference discipline

Keep a slow CPU reference integrator. When a faster SIMD or GPU path disagrees, the reference settles the argument. Pretty and wrong is still wrong. The textbook goal is an estimator you can defend, not a preview that merely impresses at 16 samples.