Post 109

Glitter With a Leash

Glossy normal-mapped surfaces stop firefly-sparkling under motion. The Kaplanyan specular anti-aliasing module that has been sitting in the shader library since the design phase is now wired into both mesh shaders, widening the highlight exactly where the pixel can't resolve the bumps.

There has been a module in the shader library called normal_aa since the PBR system was designed. It implements Kaplanyan 2016 specular anti-aliasing: when one pixel’s footprint covers more normal-map detail than it can resolve, widen the roughness by the unresolved variance so the highlight stays a highlight instead of becoming a lottery. Eighteen modules load at startup and seventeen of them get used. This release wires in the eighteenth.

What it fixes

Take a glossy surface with a detailed normal map and move anything: the camera, the object, a light. At the right distance the bumps shrink below a pixel, the renderer samples whichever normal happens to land under the pixel center this frame, and a tight highlight either catches it or misses. The result is the familiar shimmer of fireflies crawling over car paint and brushed metal in every engine that skips this step.

The fix follows the textbook: measure how fast the shading normal changes across the pixel with screen-space derivatives, convert that to variance the highlight cannot resolve, and add it to the GGX roughness before any lobe gets evaluated. The widened value feeds everything that consumes roughness, including direct lighting, area-light lookups, and the environment reflection mip, so all the specular paths stay consistent. Both mesh shaders get the identical edit, per house rules.

The part that ate the afternoon

Proving this works turned out to be harder than doing it. The obvious test, render a spinning glossy thing and assert the flicker number went down, is wrong in both directions. Move fast and real bump motion swamps the metric in any build. Move slow and the broken version scores better, because its needle-thin highlight misses everything and a black image is a very stable image. We measured our own fix as a regression three fixtures in a row before the picture clicked.

The test that landed asserts the actual contract. On a deliberately unresolvable surface, raw noise as a normal map, the widening must fire: the broken build lights 35 pixels of lucky pinpoints, the fixed one resolves a 387-pixel sheen, and the gate sits between with a temporal-stability ceiling on top. And on surfaces a pixel can resolve, the filter must do nothing: the damaged helmet renders one channel in two million differently, chrome is untouched, and every PBR snapshot passed without a threshold change. Anti-aliasing you can see when it’s needed, and can’t when it isn’t.

That closes out the AA and temporal epic: motion vectors feeding TAA, a render-resolution dial, and now stable glitter. Three releases, one theme: stop throwing away information the renderer already computed.

← All posts