Static vs. Dynamic Lighting

Overview

CryENGINE is performing all lighting calculations, fully dynamic, without using any traditional pre-baked lighting techniques. This article gives some insight into the reasons for that design decision.

No Static Lightmaps

For a long time in real-time 3D rendering lightmaps have been the state of the art. Classic lightmaps store the accumulation of all lighting on a surface in some low resolution texture. This texture is a unique (world position to texture position is a 1:1 mapping) unwrapping of the static world geometry. Computing a texel in the texture needs to consider all nearby lights with their distances, occlusion (shadows), colors and the surface orientation (normal). The major benefit of lightmaps is to have the render performance independent of the lighting complexity (light count, shadow quality, indirect lighting quality).

All aspects of the lightmap computation have been improved over the years (unwrapping, performance, soft shadows, compression, ambient occlusion or indirect lighting) but there are fundamental problems the method cannot overcome.

In CryENGINE 1 we had Dot3Lightmaps implemented which allowed us to get diffuse per pixel lighting (normal mapping) with the lightmap-typical constant performance characteristics. However, lightmaps limited us to static geometry, static light position and static shadows. We implemented many other lighting and shadowing techniques (per pixel lighting, stencil shadows, object shadow maps, cube mapped shadows, projected shadows) so that we were able to overcome the limitation where required. In some areas we even disabled shadows for performance. All that resulted in a somehow inconsistent look and in complex performance characteristics (some techniques run faster depending on hardware).

In CryENGINE 2 we unified the shadow system (using exclusively shadow maps), dropped stencil shadows and experimented on improving our lightmap solution. In the end we aimed for a more dynamic lighting.


Screenshot from CryENGINE 1. Note the inconsistency between soft static shadows and the hard dynamic shadows from the chair.

No Static Occlusion Maps

Lightmaps can support global illumination but the computation time for that can be huge. For faster results and more control, many lightmap implementations stick to direct lighting only. Now the pre-processing time is dominated by unwrapping, shadow generation and texture compression. Texture compression of lightmaps is tricky and to fight quality loss on better hardware, we already had to use uncompressed textures. This was leading to the idea of occlusion maps: We used the lightmap channels to store the shadow occlusion of multiple lights in the same texture that we used before to sum up the colored lighting. Assuming uncompressed textures, this allows to store up to 4 lights per texel.

By smartly using the attenuation radius, we were able to render lights very efficiently with per-pixel lighting (bump mapping, specular) with dynamic color and dynamic intensity. Quality and performance were very good and we extended the method to work with real-time shadows and supported even more lights per texel. However, the approach showed to be tricky in combination with dynamic geometry. Even characters walking on stairs that have occlusion maps are a difficult case as stairs and character need to be rendered into the shadowmap but should be kept separate to avoid double shadowing. As a consequence, we completely dropped this method when our shadow map solution became more mature.

No Dynamic Occlusion Maps

We prototyped updating the occlusion maps in real-time but quality wasn't as good as expected (aliasing from shadow map and from unwrapping), so we dropped this idea as well. The following shows a dynamic occlusion map with two lights (red/green) and how different resolutions and sample counts during the shadow map lookup affect the result. We experimented with other unwrapping techniques to fix UV borders but aliasing from the shadowmap and the unwrapping was always apparent.