Task 5 of 5
You have watched the noise go. Now measure it, because the rate it goes at is the
single most important number in rendering — and it is the same 1/√n that
Monte Carlo Methods derives for a dart-throwing estimate of π. A path tracer is
that estimator with a much more interesting integrand.
Measuring it needs a reference, and the honest trick is to build one out of thin air: run the accumulation twice, with two different slices of the random stream. Neither run is the truth, but their errors are independent, so the RMS gap between them is √2 times either one's own error. Halve the square and you have measured your own noise with no ground truth anywhere in sight.
Plot that against errs[0] / √n on a log axis. If the two curves lie on top
of each other, the estimator is behaving exactly as theory says — and the shape of them is
the bad news in the theory: halving the noise costs four times the samples. That
single fact is why production renderers spend their effort on importance sampling and
denoisers instead of simply waiting longer.
The samples dial is that trade in your hand. Drag it from 24 to 96 and the
whole run happens again with four times the paths per pixel: both curves grow, the picture
quietens, and the noise at the far end lands on half what 24 frames managed. Four times the
work for one halving — and this is the cheap end of the curve, where the frames are still
making a visible difference.
sqDiff — half the squared difference between
the two runs at this pixel — and build the 1/√n reference curve to plot
against it.sqDiff, return 0.5 * d * d for d = a - b at this pixelerrs[0] / Math.sqrt(f + 1) into ideal each passplot on a log axis (already wired)Run A is off by eₐ and run B by e_b, independently.
The gap between them has variance var(eₐ) + var(e_b) = twice one run's.
So the square of the gap, halved, estimates the square of one run's error — and the
square root of the mean of that is the RMS error you want.
const d = a[this.thread.y][this.thread.x] - b[this.thread.y][this.thread.x];
return 0.5 * d * d;
Squaring in the kernel and square-rooting once in JavaScript is the usual split: the per-pixel work is parallel, the final scalar is not.
Anchor it to the measurement you already have, so the two lines start together and any drift between them is real:
ideal.push(errs[0] / Math.sqrt(f + 1));1/√n because the exponent itself cannot be moved; low-discrepancy
(quasi-Monte Carlo) sequences buy a slightly better exponent on smooth integrands; and
OptiX's and OIDN's neural denoisers give up unbiasedness altogether to get the picture
twenty times sooner. The measurement you just made — two independent runs, no reference
image — is also how those denoisers are evaluated in practice.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.