The idea
Games approximate light. This one
simulates it, and the grain is the proof.
For every pixel, a ray leaves the camera and enters the scene. It hits a surface, picks a new direction according to how that material actually scatters light, and bounces again — until it finds a light source, or it doesn't. That single path contributes one noisy guess at the pixel's true colour.
Average thousands of those guesses and the estimate converges on the correct answer. This is Monte Carlo integration, and it is the honest tool here rather than a shortcut: the equation governing light transport has no closed-form solution for a real scene, so sampling it is the method, not an approximation of one.
Which is why the image above arrives grainy and settles. That grain is the statistical error, and it falls as one over the square root of the sample count — four times the work for half the noise. Everything else in the project exists to make that loop faster, or to make each sample count for more.
Finding the light
A small lamp is almost impossible to hit by chance.
Bounce at random in a Cornell box and you wander into the ceiling light roughly once in a thousand tries, so the picture is mostly black speckle. Aim directly at the light instead and that problem disappears — but now mirrors break, because the only direction that matters to a mirror is the one the reflection came from.
Multiple importance sampling runs both strategies at every bounce and weights each by how well it explains the result, so whichever is better in that situation dominates. It beats both, rather than landing between them.
- 2.4×less noise than BSDF sampling alone
- 1.9×less noise than light sampling alone
Finding the geometry
368,640 triangles, and a ray touches about four of them.
Testing every ray against every triangle is hopeless at this scale. So the geometry is wrapped in a tree of nested boxes: miss a box and every triangle inside it is skipped in one step. There are two builders — one on the CPU that optimises the tree's quality, and one that builds the whole thing in compute shaders using Morton codes, a radix sort and a parallel hierarchy construction.
- 31box tests per ray, on the heaviest scene
- 3.7triangle tests per ray
Verification
A renderer that looks right and is right
are different claims.
Almost every bug in this project produced an image that still looked like the scene. A transposed counter, a normal transformed by the wrong matrix, a sampler reading a stale index — each one renders something entirely plausible. Screenshots cannot catch that.
So there are two independent implementations: a reference tracer on the CPU in Rust, and the GPU version in WGSL. They draw from bit-identical random streams, which makes comparing them meaningful — same samples, same order, different machine — and they are diffed automatically on every test run.
The rest of the suite checks physics rather than pixels. Put an object inside a uniformly glowing sphere and a correctly energy-conserving material makes it vanish; any energy lost or invented shows up immediately. Chi-squared tests verify statistically that every sampler matches the probability density its own maths claims.
- 261tests, all measured against an independent reference
- 8.17e−5browser vs. native GPU, mean relative error
- 1.00001white furnace mean, where exactly 1.0 is perfect energy conservation
- 0rendering, ray-tracing or BVH libraries used
What it cost
Three bugs worth the telling.
Silent, for two build steps
The browser allocated 71% of the memory it then indexed, because a buffer size was a literal in TypeScript while the struct it described grew by 32 bytes in Rust. Nothing crashed — the graphics API clamps out-of-bounds writes — so paths overwrote each other and the renderer produced a plausible, converging, 446% wrong image. Found by auditing, not by failing.
The instrument agreed with the fault
The convergence meter did not notice, and could not have. It measures how noisy an image is, not whether it is the right image — and a scrambled render is perfectly capable of being smooth.
Sometimes the test is wrong
One test asserted a scene with a mirror and a glass ball must be noisier than a plain box, because caustics. It failed, and the renderer was right: nearly every pixel there is well lit, and the caustic is a minority an image-wide average barely feels.