Aim at the Light, Mostly
I wrote a small path tracer: a Cornell box, a matte ball, a shiny ball and a square light in the ceiling, about 260 lines of JavaScript. A path tracer makes an image by following random light paths backwards from the camera and averaging them. Every pixel is an average of random guesses, so a fresh render is grainy, and the grain fades as more paths pile up.
There are three textbook ways to find the light from a point on a surface, and the textbook says which wins when. I wanted numbers, so I measured all three on the same scene across a range of light sizes and shininess. Here they are, live. All three are rendering the same image. Only the grain differs.
Samples per pixel: 0. The number under each image is its noise power (variance) relative to the blended one, measured over every pixel that isn't the light itself. Noise power is what sets the cost: twice the noise power takes twice the samples to clean up. Stops at 4,096 samples.
The three ways
- Bounce. At each surface, pick a random new direction, the way the surface itself would scatter light, and keep going. If the path happens to hit the light, it carries light back. Otherwise it carries nothing.
- Aim at the light. At each surface, also pick a random point on the light and fire a ray at it. If nothing's in the way, add that light directly. Bounced paths that hit the light are ignored, since the aimed ray already counted it. Renderers call this next event estimation.
- Both, blended. Do both and keep both answers, but weight each by how good its method is at finding that particular bit of light. That's multiple importance sampling, from Eric Veach's 1997 thesis. Each sample's weight compares how likely the two methods were to produce it, and the weights always add up to one, so nothing gets counted twice.
Each method is unbiased: on average it gives exactly the right image, and only the noise differs. That's also a free correctness test, since three independent ways of computing the same thing have to agree. Across 63 runs of all three (7 light sizes, 3 levels of shininess, 3 seeds each), I made 378 comparisons of average brightness between methods. The differences were sized just as the noise predicts: 5.3% landed beyond two standard errors, where pure chance gives about 4.6%.
Small lights: aim
With a small light, bouncing is hopeless. A random bounce has to land on a target that fills a tiny part of the sky. The light's power stays fixed as it shrinks, so the rare hit carries a huge value. That's what fireflies are: single bright pixels that are bounces getting lucky.
On the matte surfaces, with the light at 2% of the ceiling's width, bouncing needed about 14,000 times as long as aiming to reach the same noise. Each halving of the light's width roughly quadrupled it.
Big lights on shiny things: bounce
The other way round, aiming loses badly. A shiny surface reflects in a narrow cone. Aim at a random spot on a big light and you almost always pick a spot outside that cone, so the sample is worth almost nothing. Once in a while it lands inside and is worth a fortune. Bouncing instead follows the cone, so it hits the light whenever the cone points at it.
On the mirror-ish ball under a light that fills the ceiling, aiming needed about 1,000 times as long as the best method.
Two things surprised me here.
First, the shiny ball made the walls noisier for aiming. Light that reaches a wall by way of the ball has to go through that same narrow cone at the ball. With the mirror-ish ball and a light 40% of the ceiling's width, aiming was 16 times noisier than blending on the matte surfaces, not just on the ball.
Second, aiming loses even on plain matte walls when the light is huge. With the light filling the ceiling, aiming was about 44 times noisier there. I mapped where that noise was: 79 to 91% of it, depending on the seed, came from the band of wall just under the ceiling (the top 5% of the room's height). A point in that band is almost touching the light, and aimed light gets divided by the distance squared. Pick a spot on the light a hair away and the sample explodes. Bouncing doesn't care: from there, the light fills half the sky, so half of all bounces hit it.
Both: never far from the best
Time to reach the same noise as the best method, for each of the three. Bouncing is cheaper per sample (no extra ray), about 0.66 times the cost, and that's counted.
| Light width | Bounce | Aim | Both |
|---|---|---|---|
| Matte surfaces, with a satin ball | |||
| 2% | 14,047× | 1.00× | 1.03× |
| 10% | 566× | 1.00× | 1.01× |
| 40% | 28× | 1.03× | 1.00× |
| 80% | 3.9× | 2.1× | 1.00× |
| 99% | 2.3× | 44× | 1.00× |
| The mirror-ish ball | |||
| 2% | 20× | 1.00× | 1.15× |
| 10% | 1.2× | 2.2× | 1.00× |
| 20% | 1.02× | 11× | 1.00× |
| 40% | 1.3× | 86× | 1.00× |
| 80% | 1.5× | 683× | 1.00× |
| 99% | 1.5× | 1,014× | 1.00× |
Each cell is the time to reach equal noise, divided by the best of the three for that row. Averaged over 3 seeds at 96×96 pixels and 512 samples per pixel, with up to five bounces.
Over all 42 cases (7 light sizes, 3 levels of shininess, matte surfaces and the shiny ball), blending was within 15% of the best method every time. Its worst case was the tiny light reflected in the mirror-ish ball, where aiming alone did a little better. Bouncing alone lost by up to 19,000×, and aiming alone by up to 1,000×.
The tidy story says aiming beats bouncing for small lights and loses for big ones. Mostly true, but the crossover depends on where you look, and both pure methods are exposed to cases where they're catastrophically bad, often in a corner of the image you weren't watching. Blending gives up at most a few percent anywhere to be bad nowhere. That's why every serious renderer does it.
The renderer is one file. Most of what makes real renderers hard is missing: glass, textures, many lights, anything smarter than random for picking directions. But this part, deciding where to look for light, is the core of it, and the blend is only about ten lines.