So we recently added day/night-cycles to the game, and immediately encountered a problem.. it’s difficult to see in the dark 🙂
We had lights working since before, as shown in a previous post, but those were pseudo-dynamic – they were rendered in a 3D light-grid on demand, but they were locked to quantized grid positions and couldn’t be moved without recalculating all the light in a chunk of the world’s lightmap. Since we had implemented the possibility to build cars and boats and other mechanical moving things, it felt a bit stupid not being able to equip them with completely free moving lights as well.
Backing up a bit, there are many ways to implement lights in 3D graphics and games. One of the oldest tricks was to hardcode the rendered pixel to light position comparison for 1 or a few light positions into the GPU shader (or, in the old days, into the hardware). A scene often consisted of a single object and this provided some acceptable level of realism.
As the scene complexity grows, the lighting equation coded into the pixel rendering that just considered the distance and angle to a few hardcoded lightsources starts to look unrealistic – it doesn’t consider shadows, that is, that other parts of the scene could obstruct the view between the lightsource and the pixel to be rendered.
There are a few tricks to handle this, generally involving going through the scene geometry additional times for each light to calculate the volume of space which is in shadow (called shadow volumes) or registering what parts of the scene are visible from the light and hence are not in shadow (called shadow mapping) and the information is then used when rendering the actual scene. See this excellent tutorial on shadow mapping and its implementation issues and the simple example video below.
There are a few problems with this technique. As the scene complexity grows even more, there is a need for many lights, and for each light you have to go through the geometry to find the shadows which makes it difficult to use for scenes with dozens or hundreds of lights or where the geometry is costly to traverse. In our game here, the scene geometry can consist of millions of triangles and there can be hundreds of lights visible.
Techniques such as deferred shading can be used to decouple the complexity of the scene with the complexity of the light-sources when it comes to actually calculating the light equation for each rendered pixel, but to render shadows, it still requires a geometry rendering pass from each light.
A more gameplay-specific issue is that lighting and shadow-casting isn’t only a rendering problem – in some cases it should control the game-logic, or in our case, sensors, brains and NPCs should be able to sense light around them. In most games this is not really important but we find it intriguing to be able to add lighting and shadows properly to the gameplay and puzzle-solving.
Actually, the fairly simple lighting and shadow calculations done for the static voxel terrain by propagating sunlight down from the sky through the voxel grid already provided this possibility for involving sunlight into the gameplay. So, we had to find an adaptation of this to use for our static and dynamic point-lights as well that also should handle occlusion by the terrain geometry just as for the skylight propagation. One major difference is that if a moving light is quantized to the voxel grid and then its light propagated, it looks really bad as it moves from voxel to voxel. So we had to try a lot of different methods to interpolate and smooth the light insertion into the lightmap.
There are some examples in the literature of propagating light through voxel grids for use in global illumination, such as the CryEngine Light Propagation Volumes technique, a very interesting read with some example videos.
One fundamental choice that has to be done when handling light on a voxel grid is how many degrees of freedom to keep for each voxel. In this game we use a single value currently. Keeping a single value means an object to be rendered using the light sampled from the light grid will experience the same lighting no matter how it turns, that is, we lose the directional information of the lightsource. We can’t either use the direction of the light inside the voxel for knowing in what direction to keep propagating the light in. Keeping more values per voxel, in either a cartesian way or in a spherical-harmonics sort of way (like in the CryEngine paper) gives some very interesting opportunities but we simply can’t afford to store this data – basically we make the tradeoff that we rather have a larger visible part of the world than better lighting.
Just by looking at the resulting lighting of a single object it doesn’t look as good as conventional modern lighting techniques, but it scales to a large amount of lights and as we discussed above, we can then use it for gameplay logic as well so we think it’s worth it in this case. It still takes some CPU-time to propagate every light through the voxel grids of course.
One important omission in the currently implemented method is that it doesn’t scan the dynamic objects for occlusion when propagating light, so there will be no occlusion-shadowing inside wholly dynamic vehicles, buildings or spaceships yet.
In case you missed it, you can see some examples in the embedded video at the top of the post! As always, music is by our great music creative Peter Mörck.