Projects

Physically Based Path Tracer

Computer Graphics, UCL · 2024

Overview

A path tracer estimates the light reaching each pixel by following random paths from the camera into the scene. The light leaving a surface is the integral of the incoming light over the hemisphere, weighted by the material's BRDF and the cosine of the incoming angle. This integral has no closed form, so it is estimated with Monte Carlo by sampling a direction, evaluating f · L · cos θ / pdf, and averaging. The estimate is unbiased as long as the pdf matches the distribution the direction was actually drawn from.

Everything is implemented without third-party libraries: sampling and its densities, a BVH built with the surface area heuristic, Lambertian and GGX materials, next event estimation, multiple importance sampling and Russian roulette.

A rendered image can look correct even when the estimator is wrong, so I also wrote 24 statistical checks that test the estimator directly. They are part of the renderer's own binary instead of a separate test suite, so the checks and the images always come from the same build.

Sampling and variance reduction

Every sampler comes in a pair, a function that draws a direction and a function that gives the probability density of drawing it. Lambertian surfaces are sampled with a cosine-weighted distribution, and GGX surfaces by sampling their distribution of microfacet normals.

Next event estimation samples a point on a light at every bounce, instead of waiting for a path to hit one by chance. This works well on diffuse surfaces but badly on glossy ones, where the reflection lobe is narrow and most light samples land where the surface barely reflects. BRDF sampling has the opposite problem. Multiple importance sampling combines the two, weighting each sample by how likely the other strategy was to produce it, so it works well on both kinds of surface.

Russian roulette terminates paths at random once they become dim, and scales up the ones that survive by the inverse of their survival probability. This keeps the estimate unbiased while spending less time on paths that contribute very little.

Acceleration structure

Testing every ray against every triangle is too slow, so the triangles are grouped into a bounding volume hierarchy. Each split is chosen with the surface area heuristic, which estimates the cost of a split from the surface area of the two child boxes and the number of triangles in each, since a ray is more likely to hit a larger box. As a result, a ray only tests about three and a half triangles on average, even as the amount of geometry grows.

The white furnace test

A surface with albedo 1 inside an environment of uniform radiance 1 should render to exactly 1, because every path, no matter how many times it bounces, keeps the energy it started with. Any error in the BRDF, the cosine term or the pdf shows up as a value different from 1.

However, this only tests anything if paths bounce more than once. Every path hits a convex sphere only once, so a sphere would never test interreflection or Russian roulette. I use a deep open-topped well viewed from inside instead, so that paths bounce many times before escaping.

Convergence

Monte Carlo error falls as one over the square root of the number of samples, so on log-log axes it should be a straight line with a slope of −0.5. A biased estimator converges to the wrong answer, so its error flattens out instead. Measuring this slope against an analytic reference, the exact value of 1 from the white furnace, catches bias that comparing one image against another would miss.

A flake scene rendered with multiple importance sampling
The flake scene, rendered with multiple importance sampling.