You Don’t Know The Power of the Light’s Side

Form Factors – The Light Already Knows: An Elegant Idea That Failed

Hey Look, Free Disco Ball

Today’s episode, like so many, begins with a Cornell box. This one holds three glass spheres, rendered by a radiosity solver. Something is wrong with this retro radiosity render, and the culprit is the most fundamental part of a radiosity solve:

form fac·tor  /ˈfôrm ˌfak·tər/ · noun, radiometry
1. the computed fraction of one surface’s light that reaches another.
2. (reciprocal) the same fraction, measured from the light’s side.

Blotchy direct lighting from under-sampled form factors on the walls of a Cornell box
The starting point: 2×2 receiver hemicubes — twelve form-factor samples per patch, direct light included — 20 s. The patch-to-patch blotch is the artifact this article is about.

Great soft blotches of light and shadow sparkle across surfaces that should be smoothly lit and shaded. It’s a dance party! Or some kind of terrible bug? But the geometry is fine, the materials are ok, and the light is set up correctly. This artifact is sampling noise, aliasing, from the single most important number in a radiosity solve: the form factor.

Twelve samples for the most important form factor

A radiosity solver works by computing, for every pair of surface patches in the scene, what fraction of the light leaving one arrives at the other. That number is the form factor. If you build a full matrix of form factors for every combination of two patches you have the whole simulation. Once you have it, light bounces around the room by repeated matrix application until it converges.

The classical way to measure a form factor – Cohen and Greenberg’s hemicube, 1985 – is beautifully direct: put a camera at the patch, render the scene onto a little half-cube of pixels, and count. Whatever fraction of those pixels a given surface covers, weighted by a precomputed cosine table, is the form factor to that surface. It is solving the harder rendering problem (global illumination) by using a simpler rendering technique (scanline render), which has always made the graphics person in me happy.

The catch is resolution. The image above was rendered with a hemicube two pixels on a side; a top face of 2×2 and four side faces of 2×1, for twelve form factor samples total.

For indirect bounce light, twelve form factor samples may be survivable; the energy is diffuse, low-contrast, and errors average out. But the patch-to-light coupling is the highest-energy and highest-frequency term in the entire solve. Quantizing that through only twelve pixels is what gets the party going. Some patches catch the lamp in a pixel and blaze; their neighbours miss it entirely and fall dark. The blotch is not noise in the path tracing sense, but it is in the radiosity solver’s sense.

Our hemicube inspector can highlight, in the 3D view, the surface each hemicube cell actually landed on. Here are two neighbouring patches highlighted in a white outline – a few centimetres apart on the same surface – with all twelve of their cells mapped:

Two neighbouring patches and where their twelve form factor samples land in the room

White marks the patch being inspected; cyan marks where its twelve form factors land. On the left, patch 2311 samples the room twelve times and every single cell comes back with a form factor pointing to a wall, the floor, or a sphere. It never finds the lamp. Its neighbour on the right, patch 2715, casts the same twelve cells into the same room – and exactly one of the form factors (yellow) lands on the light.

And here are the two hemicubes themselves, straight from the inspector – each patch’s complete view of the world, all twelve cells of it:

The two patches' actual 2x2 hemicubes from the inspector

Every coloured cell is one form factor sample. This is the entire dataset from which each patch must estimate its illumination – the readout names the difference: one cell of patch 2715’s grid on emitter 5131; none in patch 2311’s twelve.

That one form factor is worth 6.4× the total energy: 1.489 against 0.232. Two patches you could cover with a thumbnail, differing by a factor of six, because a single form factor out of twelve happened to find the ceiling. Scatter that coin-flip across every patch in the room and you have the blemish we started with.

The obvious fix, and its price

The obvious fix is to stop being cheap. Here is the same scene with the receiver hemicube raised to 32×32 – 3,072 form factor samples per patch instead of twelve.

The same scene with 32x32 receiver hemicubes - the blotches are gone but fine stripes appear near the ceiling
The same scene, receiver hemicubes raised to 32×32 (3,072 form factor samples per patch) — 33 s.

The blemish is gone – but look along the top of the room. Fine horizontal stripes have taken its place. The lamp is now a handful of pixels in every receiver’s view instead of a rounding error, but those pixels still round, patch by patch, and the rounding marches across the wall as a moiré. The party didn’t end, it just changed into more formal clothes.

Here’s 64×64 – 12,288 form factor samples per patch, five times the render time of the picture that had the blemish:

The same scene with 64x64 receiver hemicubes - still faintly striped near the top, at five times the render time
64×64 receiver hemicubes (12,288 form factor samples per patch) — 1 m 47 s.

Better. Also still striped. Here is the top of that frame, magnified, with local contrast amplified so print does it justice – on a good monitor you can even see these whorls in the render directly:

The top of the 64x64 render magnified with contrast amplified: moire whorls and stripes across the ceiling and upper walls
The top of the 64×64 frame, magnified, local contrast amplified: the moiré whorls the stripes make.

One more rung. 128×128 – 49,152 form factor samples per patch:

The same scene with 128x128 receiver hemicubes - finally clean at viewing contrast, at twenty-nine times the render time of the blemished frame
128×128 receiver hemicubes (49,152 form factor samples per patch) — 9 m 43 s.

Clean – at viewing contrast. (Under the same amplification that exposed the 64×64 whorls, a ghost of the pattern still answers.) And now the full bill is on the table. On this scene – twenty thousand patches, meshed the way the later benchmarks use it – the ladder runs from 20 seconds to 583: twenty-nine times the blemished frame, for one artifact. The bill also scales the wrong way: it is paid per patch, and a scene with more surfaces pays it more times. The 128×128 frame serves as the article’s ground truth reference from here on.

So: can I get the 128×128 hemicube picture, or better, at the 2×2 hemicube picture’s price?

The Light’s Side of the Forms

Here is the thing that started this work. Our solver already renders the scene from the light’s point of view, at high resolution, every single time.

It does this because the light is a patch like any other, and every patch gets its hemicube and calculates form factors. But the solve is a gathering solve: a receiver i reads only row i of the form factor matrix. The light’s own row – the record of exactly which surfaces the lamp can see and how much of its output each one receives – is consulted only when gathering light onto the lamp, which is pretty pointless.

And that data is precisely what we are missing. A receiver’s twelve-sample hemicube is a bad instrument for finding a small bright lamp. But the lamp’s own view of the room is a rendered image of every surface it illuminates, and it is already sitting in memory. Also, light hemicubes get rendered with more form factor resolution by setting a multiplier. There are only a few of them, so the difference on the GPU is actually so small it’s hard to measure.

What each side actually shoots

It is worth pausing to draw the two techniques, because the costs in the table below fall straight out of the pictures. The seeded path asks the question from the receiver’s side: every patch fires a fan of shadow rays at stratified cells of the lamp.

Reciprocal form factor measurement from the light's side: the square light cut into sixteen sub-squares, each rendering a small hemicube - only sixteen for the whole scene

Sixteen rays is a modest fan – but there are thousands of these, one per patch, and the bill grows with every patch the mesh gains.

The reciprocal path asks the same question from the light’s side using form factors: the lamp is cut into sixteen sub-squares, and each one renders the scene through a small hemicube – a 16×16 raster through the top face, 8×16 through each of the four side faces. One rasterized view of the room, per face, occlusion included.

Diagram: the square light cut into sixteen sub-squares, each with a small hemicube; one hemicube magnified showing 16x16 rays through its top face and 8x16 through each side face - only sixteen of these for the whole scene

And that is the whole trick: 16 of these – total, for the entire scene, no matter how many patches the mesh grows.

Here is that view – the lamp’s own hemicube 64×64, unfolded, from the same scene, shows the form factors:

Cornell box with 8x8 receiver hemicubes and no direct seeding: the form factor blotch returns on the direct light

Where the receiver had twelve cells, the light has over twelve thousand. Every coloured cell is a surface patch the lamp measured, occlusion included – you can see the three glass spheres sitting in its field of view. This image is computed on every solve, at real expense, and then consulted by nothing.

The bridge between them is two hundred and sixty years old. Lambert’s reciprocity relation says that form factors between two patches are symmetric once you weight by area:

F_j→L · A_j  =  F_L→j · A_L

Which rearranges to something almost embarrassingly convenient:

F_j→L  =  F_L→j · A_L / A_j

Every receiver’s coupling to the light can be read off the light’s own form factor row, inverted through areas, with no new rendering at all. The expensive measurement is already paid for. I simply have to use it rather than discard it.

There is a further elegance to it. A hemicube rendered from the light has occlusion built in for free: a shadow is exactly the set of pixels the lamp cannot see. Where a receiver must ask “can I see the light?”, the light has already answered “here is everyone I can see.”

It works

First I need to admit that the hemicube needs to be at least 8×8 just to capture the indirect and other effects in this scene. But even at 8×8 the direct light is still problematic — still having a party.

The same scene with direct light from reciprocal form factors - the light's own row - clean in 24 seconds
Fixed 20k-patch mesh, 8×8 receiver hemicubes, no direct seeding — 26 s. Indirect light and the glass are captured, but each patch’s direct light still rides its own few samples: the blotch is back.

And finally here’s the result with the form factors from the Light’s Side used for direct illumination.

The same mesh and the same 8×8 hemicubes, with direct light taken from the light’s own high-resolution form factor row instead — 24 s. Two seconds faster, and the blotch is gone.
receiver hemicubedirect light fromrender timewhat you see
128×128 (reference)receiver hemicube9 m 43 sclean
2×2receiver hemicube20 sthe blotch
8×8receiver hemicube26 sblotch on the direct light
8×8the light’s own row24 sclean

In the original 2×2 measurement run, that swap came out eighty-seven times faster than buying the same quality by refining the receiver.

This is the point in the article where, if I were selling something, I would stop.

The question we should ask before celebrating

The honest question is not “is this better than the artifact?” It is “is this better than what the renderer would have done anyway?”

Because our solver already has a mechanism for this problem, and it is on by default. Rather than transporting the lamp’s light through form factors at all, it shades each patch directly using importance sampling – fire shadow rays at the light source, compute the illumination analytically, and seed the solve with the answer. Form-factor transport then handles only the bounces, where twelve samples is possibly enough.

So here is the production answer: the same 8×8 receiver, with seeded direct lighting doing what it does every day.

The same scene with seeded direct lighting - also smooth
The production answer: the same receivers with seeded direct lighting — shadow rays fired at the lamp — also smooth.

versiondirect light fromtimeerror vs reference
reciprocitythe light’s own row24 s0.0058
seeded directshadow rays23 s0.0042

Same cost. Slightly better. And it was already there.

Four form factor strategies side by side: the 2x2 artifact, reciprocity at 8x8, the 128x128 reference, and seeded direct at 8x8

I then went looking for the conditions where reciprocity would pull ahead. I did not find them.

Sample budget. Seeded direct’s quality is bought with shadow rays, so I swept the budget from 400 rays per patch down to one:

shadow rays per patch40010025941reciprocity
error vs reference0.02910.02900.02890.02950.02900.02970.0327
render time5 s6 s5 s5 s5 s5 s5 s

A single shadow ray per patch beats the reciprocal form factor approach, and the cost curve is flat – shadow rays are so cheap against the cost of the form-factor solve that four hundred of them per patch does not move the wall clock.

Scene complexity. I subdivided the room, reasoning that the per-patch ray cost should eventually dominate:

patches per wall1,0244,09616,384
reciprocity3 s38 sover budget
seeded direct2 s34 sover budget

It doesn’t. Both are dominated by the shared cost of rendering tens of thousands of receiver hemicubes; the direct-lighting method is noise against it. At the finest subdivision neither finished inside a fifty-minute budget, which tells you where the real cost lives.

Heavy occluders. A Stanford bunny between the light and the walls, to make each shadow ray expensive: reciprocity 14 s, seeded direct 12 s.

Where the error lives. Perhaps the whole-image number was hiding a regional win – reciprocity ought to shine in penumbrae, where a rendered view of the light beats point sampling. It doesn’t, here:

regionreciprocityseeded direct @ 1 ray
shadow / penumbra0.670.59
lit surfaces10.099.02

Why the elegant idea loses

The reasoning was sound and the implementation does what it claims. The premise was wrong, and the premise was this: that measuring direct light using shadow rays is more expensive.

For a hemicube-based gathering solve, it genuinely is – that is the artifact we opened with. But the renderer is not restricted to hemicubes. It is a ray tracer, and asking “can this point see that lamp?” is the single cheapest, most optimised query in the entire system. Reciprocity is an elegant way to recover an expensive measurement. Ray tracing is an inelegant way to avoid needing it, and it wins because the thing it avoids was never more expensive in the first place.

There is a general lesson in that, and it is why this article exists rather than a triumphant one. An optimisation must be benchmarked against the best solution we already have, not against the problem it was designed to solve. The artifact is real and the technique genuinely fixes it – every number in the first half of this article is true. But that artifact is not what our renderer produces, because the renderer has had importance sampling of area lights for years. Had I never rendered that production image, I would have shipped a correct, well-tested, thoroughly verified solution to a problem the renderer had already solved in a better way, and I would have believed I had improved it eighty-seven fold.

What survives

Two things, and one open question.

The analytic machinery built along the way is independently useful. Implementing this required an exact closed-form solution for the form factor between a point and a polygon – Lambert’s contour integral, 1760 – which now lives in the renderer with a fifteen-case test suite and no dependence on rasterization at all. Any future work needing ground-truth form factors, or a check on the hemicube, now has it.

The diagnostic is real. The reciprocal path reports exactly how many receiver-light couplings the hemicube missed entirely – 56,601 out of 82,160 in the original 2×2 run. That is a direct, per-scene measure of whether a given hemicube resolution is adequate, and we did not have one before.

And the open question: every scene here has a small, compact lamp, which is precisely the case a single shadow ray handles almost exactly. The original argument for this technique concerned a large emitter, partially occluded and subdivided as a way of stratifying the sampling – something that benefits both shadow rays and reciprocal form factors. A broad softbox behind a complex silhouette, subdivided into nxn smaller lights. That one is still on the bench.

Epilogue: the idea comes back

This article sat in drafts for weeks, and in that time the story evolved in a way we did not see coming.

The verdict above is honest for the scenes it measured: compact lamp, diffuse room, easy geometry. But I kept sweeping scene classes, and on mirror-heavy scenes the tables turned. Light that arrives via a mirror is caustic transport and belongs to a different pipeline entirely; but that is not what changed. What changed is the wall behind the mirror: specifically the grazing slivers along mirror frames and edges. The receiver’s hemicube (the red wall below) sits behind the mirror. Hemicube rays are fired from the patch center and they get blocked by the back of the mirror, speaking for the whole patch. The light’s side, though, finds the edges exposed and measures them cleanly. I put the estimators in front of a Monte-Carlo referee (hundreds of area samples per pair, visibility included) on the eleven hardest disagreement light-patch pairs: the light’s view came out closest to ground truth on eight of the eleven. Not just more efficient, but more accurate!

Three panels: the mirror-wall Cornell scene (1,668 patches) under seeded direct lighting, the same scene under reciprocal direct transport - visually identical - and their difference amplified twelve times, which lights up along the red mirror frame's edges

Those two renders differ by an RMSE of 0.0036 – visually identical – but the amplified difference is not noise: it clings to the mirror frame’s edges.

I tuned both techniques to their own honest best and measured again, on an intentionally heavier version of the scene – 22,000 patches. The seeded path required twenty shadow-ray samples per patch to smooth the noise, a value I found out by experimentation. The reciprocal path, on the other hand, wanted the light split into sixteen sub-patches so it could sample from more positions. And a higher-resolution hemicube. The table below is the two cost curves.

tuned techniquestage+ its light atlastotal
seeded direct (20 samples)3,539 ms3 ms~3.5 s
reciprocity (subdiv 2, 32× view)20 ms248 ms~0.27 s

Thirteen times cheaper, just on the direct light evaluation. The seeding bill grows with every patch and every sample; the reciprocal pass reads a measurement the light already made. A curious footnote from the tuning: doubling the light’s view resolution as a safety margin improved the results noticeably at a cost of 190 milliseconds of GPU rasterisation. In a 224-second frame – increasing resolution on the light’s side is very nearly free, which is itself a small architectural lesson.

It also found its limit, and a second referee session named the price with the same bluntness. On densely self-occluding geometry – a mesh bunny hugging its own crevices – the technique’s analytic repair path over-injected energy, because a single lucky confirmation ray was granting fully un-occluded credit to mostly-blocked couplings. The fix was to make the confirmation visibility-weighted: stratified shadow rays across the lamp’s area now scale the analytic coupling by the fraction that actually gets through. Re-adjudicated against the same Monte-Carlo truth, per coupling:

rescued couplingbefore (vs MC truth)after v-weighting
worst3.67×1.84×
2nd2.89×1.40×
3rd2.55×1.24×
4th2.31×1.14×
median of the flagged band~2.2×~1.2×

The clean rematch

One confound remained in every timing we had published: the mesh itself. Under adaptive subdivision the two techniques do not even solve on the same patches – the reciprocal field is raster-exact at the mirror edges, so it shows the subdivider sharper gradients and refines earlier, spending seconds on a finer intermediate mesh that the seeded run never builds. Fixing that meant removing adaptivity from the experiment entirely: the same scene, uniformly meshed to an identical 20,496 patches for both sides, each technique at its tuned best on the same mesh yielded these results:

The identical 20,496-patch scene rendered twice: reciprocal direct in 20 seconds, seeded direct in 23 - visually identical output

Same mesh, same solver, same picture: reciprocal 20 seconds, seeded 23. Three seconds – thirteen percent of the whole frame – and this time it is not a stage-level curiosity but wall-clock you can feel, on exactly the kind of patch count that real scenes grow into.

So the lesson from the middle of this article was correct, but incomplete. An optimisation must be benchmarked against what the system already does – per scene class. The elegant idea lost in the first scenes we tested, but now we see it wins in many others. Enough to earn a place in future testing as we continue development.

I set out to optimise away a three-and-a-half-second stage inside a three-minute-forty-seven-second frame. On one scene the frame barely noticed – about a second and a half of every hundred. On the twenty-thousand-patch rematch above, three seconds in twenty-three is a more significant percentage. What actually survives the arithmetic is worth more than the seconds though: a direct-transport estimator that is closer to ground truth on the geometry where sampling struggles, at a cost of milliseconds; the discovery that resolution on the light’s side costs almost nothing; and a method whose bill doesn’t grow as much with the patch count – which matters, because patch counts only ever grow. Sometimes optimisation reduces the render time, but I have to remember that being closer to ground truth can be even more valuable.

The conclusion is that there is no conclusion. At least not yet. I’ve declared victory and defeat multiple times just in this article, and I expect the pendulum to keep swinging as development continues. What I do know is that DeluxeRender now has another unique tool in its arsenal, and it has already helped me understand the shape of the problem better.

Leave a Reply

Your email address will not be published. Required fields are marked *