Site experiment · Light transport

Light Paths in the Onsen

Around midday in the 3D home, a light well drilled from the clifftop throws a beam of sun into the onsen, and the ripples bend it onto the pool floor. Here two renderers draw that light in your browser with the same minute each: path tracing and Metropolis light transport.

Why

Sunlight on the pool floor is the hardest light in the house to render. It arrives only by bending through rippled water, so a renderer cannot aim at it.

What

A small renderer in plain JavaScript draws the same scene with path tracing and with Kelemen's Metropolis light transport, next to a reference that took 24 minutes on 18 threads.

How

Both methods share one path sampler. Only the source of its random numbers differs, so the chart compares two ways of spending the same time.

Race the two renderers

Press Run. Each renderer gets its own thread, and the race pauses after a minute. It also pauses when you scroll away or switch tabs. Until then, the panels show still frames from the same renderer after a minute on my laptop.

Path tracing after one minute: a dark grotto scattered with isolated white specks, and a loose cluster of dots where sunlight should gather on the pool floor
Path tracingStill frame after one minute · error 626%
Metropolis light transport after one minute: a bright patch of sunlight on the pool floor, a glowing teal pool, and grainy, blotchy walls
Metropolis light transportStill frame after one minute · error 158%
Reference render: a dim grotto, a faint patch of reflected sunlight on the back wall, and a teal pool with a bright patch of refracted sunlight on its floor
Reference24 minutes on 18 threads

Why this light is hard

A renderer estimates each pixel by following paths of light backward from the camera. Most light in this room is easy to find. A path lands on a wall, and the renderer checks whether the sun or the sky is visible from that point with a straight shadow ray.

The light on the pool floor is different. A point on the floor sees the sun only through the water, and the water bends every ray that crosses it. A straight shadow ray toward the sun hits the surface and turns, so it cannot be aimed. The only way to reach the sun from the floor is to pick a random direction, bend it through the ripples, and hope it lands inside the sun’s disc. In this scene about one random path in a million does, and each one carries about a thousand times the brightness of an average pixel.

Path tracing averages those rare, huge values with everything else. They show up as isolated white specks, called fireflies, and they fade only with the square root of the time spent.

Two ways to spend a minute

Path tracing draws every path independently. It is simple and unbiased, and a path that finds nothing costs as much as one that finds the sun.

Metropolis light transport (Veach and Guibas, 1997) treats the image as a distribution over paths and explores it with a Markov chain. Once a chain finds a bright path, it proposes small changes to it, and a path through the same ripple tends to be bright too. Each proposal is accepted with probability min(1, new brightness / old brightness), the Metropolis–Hastings rule, so a chain spends time on each path in proportion to its brightness. Every proposal still adds to the image, weighted by its chance of acceptance, so rejected work is not wasted.

I used the version by Kelemen and colleagues (2002), which changes the random numbers a path tracer would read instead of the path itself. That keeps the comparison fair: one function turns numbers into a path in both renderers. Path tracing feeds it fresh numbers. Metropolis feeds it small changes to the numbers behind the last accepted path, and a completely fresh set on 30% of its steps, so a chain does not stay in one place.

What the first five seconds show

Path tracing after five seconds: fine white specks across the walls and the pool, and no visible sunlight
Path tracing, 5 secondsError 2,783%
Metropolis after five seconds: the sunlit patch on the pool floor is already visible, with orange blotches on the walls where chains linger
Metropolis, 5 secondsError 295%

After five seconds, path tracing has not found the sunlit patch on the pool floor; the bright specks are single paths that happened to reach the sun. Metropolis already shows the patch, because a chain that finds it stays nearby. The orange blotches on its walls are chains lingering on rare paths that reflect off the water.

Error here is the root-mean-square difference in brightness from the reference on an 80 × 60 grid, divided by the reference’s average. Above 100%, the typical error is larger than the average brightness itself. In the browser run behind the dashed lines, path tracing fell from 3,097% at five seconds to 984% at a minute, and Metropolis from 288% to 180%. Across three seeds, Metropolis finished the minute 4 to 15 times closer to the reference.

The onsen rendered twice and split down the middle: scattered path-traced specks on the left, and on the right a network of refracted sunlight on the pool floor from Metropolis
Given eleven minutes each at 800 × 600, about the same time per pixel as a minute in the lab: path tracing on the left, Metropolis on the right. On the same 80 × 60 grid, their errors were 212% and 23%.

Where Metropolis struggles

It knows where the light is, but not how much. A chain visits paths in proportion to their brightness, so it learns the image’s shape but not its total. The total comes from independent samples, which face the same rare event that makes path tracing noisy. Here the first 100,000 samples usually contain no path to the sun, so Metropolis starts at about a quarter of the right brightness and jumps whenever a fresh sample finds the sun. I added an optional fix that takes the total from the skylight, which is easy to find, and from the share of time the chains spend in skylight. It removes most of that luck, but it is my addition, not part of Kelemen’s method.

It starts in the wrong place. Chains start from those first samples, so they begin on skylit paths and need about 20,000 changes each to settle into sunlit ones. Early frames show blotches where a chain lingers, and they fade as the chains move on.

It does more work per path. Here Metropolis completes about half as many paths per second as path tracing, and its paths are correlated. Where light is easy to reach, that overhead buys nothing.

How I checked it

Thirteen automated tests cover the parts that are easy to get subtly wrong: reflection and refraction at the water against Fresnel’s equations and Snell’s law, the ripple normals against finite differences, water hits within 10 nanometers of the surface, and a closed white box where the exact answer is known. The two renderers agree within 0.6% on skylight alone and within 6% on an enlarged 20° sun, and the same seed always gives the same image.

The reference needed its own care. For pixels where the camera first sees a dry surface, it traces 748 million particles forward from the light well. For pixels that see the water, it runs Metropolis restricted to those paths for 1.63 billion steps, then corrects slow drifts in brightness against plain path tracing. Its brightness in the pool is uncertain by a few percent; the walls are much tighter.

The scene, simplified

This is a simplified onsen: a box grotto, a round pool two meters across, and a round opening in the ceiling instead of the house’s tilted shaft. The sun stands 62° high. Its disc is three times wider than the real sun, so a minute is enough to see the difference; pick the real size to watch both renderers struggle. The ripples are frozen, and there is no steam.

What I would try next

Two methods are built for exactly this light. Manifold exploration (Jakob and Marschner, 2012) moves a chain along the paths that stay connected through the water, instead of hoping a random change does. Specular manifold sampling (Zeltner, Georgiev and Jakob, 2020) solves for the path through the water directly, so even a path tracer can aim at the sun through ripples.

Sources and credits

Open the source folder