Skip to content
Henry de Jongh edited this page Sep 22, 2023 · 3 revisions

The Technique

It's a technique created by Tim Sweeney in 1996. It was used in Unreal Gold and UT99. I spent quite some time researching it and figuring out how Unreal is able to draw virtually infinite lights with all the effects, flickering, changing colors, turning on and off, changing the radius all while having raytraced shadows. If every light were to have its own lightmap texture, it would take gigabytes of memory! What Tim did for this engine is not possible with modern GPUs. He was rendering every polygon himself using a BSP tree. Every light source in the scene that could affect the polygon gets associated with it. Then lightmap pixels on the polygon would be raycasted to check whether the light shines on that pixel or not. This would be stored as bits "shadow bits" associated with the polygon. During rendering of the polygon, you iterate over the lights associated with the polygon, create a texture, draw the lights onto the texture each checking the shadow bits to see which pixels should be skipped (shadows). Then you'd have your lightmap texture to be rendered with bilinear filtering.

Now that's cool, but I tried doing this and telling a modern GPU that it has to render 1 polygon with a texture, then stop and render 1 polygon with a texture, then stop and render 1 polygon with a texture. Well the framerate dropped to zero.

Then I spent days if not weeks trying more things. Then one night I suddenly had the idea of using bits in a shader. It turns out ComputeBuffer can be used in fragment shaders these days with a uint type including bitwise operators. That means for every pixel on a lightmap I could set and read up to 32 bits. I could have shadow bits for 32 lights. I got to work and sure enough that worked. All lights have a maximum radius where it's guaranteed to stop. A light in the bedroom and one in the bathroom will never interfere and can use the same bit. If no more than 32 lights ever overlap at one point we can store shadows for infinite light sources.

The lighting (i.e. a radius of color at a world position with attenuation) is always calculated in the fragment shader. The shadows are what get traced on the CPU, provided as a bitmask (similarly using UV1). Light sources execute a bit AND-operation for a specific bit, which reveals, in its radius, which fragments should be in shadow or in light, allowing a single fragment to store shadows for 32 lights within radius. If the light never moves, i.e. is "static" then this information is accurate. But in terms of shader optimizations, every light is always calculated. It's very good at this though and you can have hundreds, on modern GPUs thousands of lights.

Clone this wiki locally