The white furnace test: like the Chūnin exams, handing in a blank page is how you pass.

There’s a sphere in that picture. Can you see it? No, you can’t. If you could, my renderer would be wrong, and I wouldn’t like that.
What is the furnace test?
Put a sphere of albedo 1 (a surface that absorbs nothing) inside an environment of radiance in every direction. Every camera ray either misses the sphere or hits it.
- : a miss reads . It escapes straight to the environment.
- : a hit bounces until it misses, so it reads too. It keeps everything on the way, .
- : the square is flat grey. Every pixel reads , so the sphere has no edge.

The test runs it backwards. If I can see the sphere, is false, so or is. can't be, so is: the renderer is losing energy if the sphere is darker, and making it if the sphere is brighter. If the furnace test is working, you see nothing. Naruto would be proud.
To see a real-world example of this, I highly recommend Julien Guertault's "The white furnace test". It opens with a ski-slope photo that matches what predicts.
One place to look
In my renderer, every material has the same three functions, the same split as pbrt's BxDF (f, PDF, Sample_f):
// How much light goes from `out` to `view_dir`: the BRDF, f_r.
glm::vec3 Eval(const glm::vec3& view_dir, const HitRecord& hit,
const glm::vec3& out) const;
// The density Sample() draws `out` with.
float Pdf(const glm::vec3& view_dir, const HitRecord& hit,
const glm::vec3& out) const;
// Draw a direction, and report its pdf and BRDF.
glm::vec3 Sample(Rng& rng, const glm::vec3& view_dir, const HitRecord& hit,
float* pdf_out, glm::vec3* brdf_out) const;
Each bounce, the renderer multiplies the ray's energy by one number built from them:
- : the weight, the fraction of the ray's energy that survives the bounce.
-
: the BRDF,
Eval(). How much light the surface sends from the new direction toward the old one. - : the cosine of the angle between the new direction and the surface normal. Light arriving at a slant is spread over more surface.
-
: the pdf,
Pdf(). How likelySample()was to pick that direction.
That's the only way a material touches a ray, so it's where every energy bug lives. For a white diffuse surface, should be exactly 1.
What failing looks like
Seeing a correct test isn't enough, because a correct test shows literally nothing. So I broke the diffuse material to see what failure looks like:

A cosine counted twice makes average 2/3, so the sphere should read 128 × 2/3 = 85. The render says 85.
A forgotten 1/π makes , so the sphere should read > 255. A byte stops at 255, so it clips to white.
The last panel is the same bug at full brightness. The background is already 255, the sphere clips to 255 too, and the bug hides. That's why I render at half brightness, where the target is 128 with room on both sides. Bland 2D Grey Squares Are Important to My 3D Renderer
Every material gets the same test
Nothing in or says what the sphere is made of, so the same scene checks every material.

- Mirror: vanishes exactly. It sends all the light one way, and every way looks the same.
- Rough metal: a dark rim, and that's correct. At grazing angles some reflections get nudged into the surface and are absorbed. It can lose energy but never make any, so its test is "nothing brighter than 128".
- Glass: vanishes on average (127.9). Its radiance goes up by η² going in and back down coming out, so the two cancel.
Limits of the furnace
It only measures how much light comes back, not where it went. A material that scatters the right amount in the wrong directions still vanishes. I found that out with a hexagon-shaped sampler in a previous post.
What I do now
- Render the furnace for every new material. One scene, a few seconds.
- Render it below the clip, or a too-bright bug hides.
- Predict the failure. If the math says 85, the render should too.
Just for fun: the Byakugan (because of all the Naruto references)

References
- Julien Guertault, "The white furnace test", 21 October 2023.
From a path tracer I'm building in C++.
Top comments (0)