What Goes Into a Lighting

So, it seems the perfectionist in me didn't let me sleep, before the lighting system was ridded of fakery and mere illusion-based shenanigans!

After finishing a couple of iterations of improvements, including a revamp from a sort of 2.5D to fully 3D representation of the global illumination, I'm quite happy with the results and no longer haunted by ray marching related ideas at nights.

It's time to recap some of the techniques put into use in the current lighting model of Payback Time.

Below is a short sequence of screenshots representing each major step of the lighting computations:

Lighting Steps

Pipeline Basics

The rendering pipeline is completely deferred, for now. This means that the scene geometry is rendered independently of lights and the final lighting is produced per-pixel in a deferred shading pass computing the lighting equation for a fullscreen quad.

The current pipeline could be summarized with the following general steps: geometry, SSAO, lighting, bloom, color grading, anti-aliasing. Here's what each of the steps involve, in short:

Global Illumination

The global illumination (GI) part is what I have been spending most time with lately. I am planning to make lighting a significant part of the gameplay itself, so having something that both looks alright and is controllable is quite important.

While SSAO handles local shadows nicely, I wanted to have a smooth / low-frequency shadows produced by the GI. Although there are various ways this can be achieved, I'll describe the approach I am using here.

First of all, the scene (i.e. the entire playable area) is sub-divided to cells. The cell size can be adjusted - I am using 8x8x32 sized cells. This means the XY-plane is higher resolution and the Z-axis (from ground towards skies) is less accurate. To give some kind of indication of this resolution, the floor object seen in the screenshots is 16x16 in XY, so it contains 2x2 cells for GI. Having a low resolution for GI makes having smooth shadows easier but on the other hand makes having very sharp shadows unobtainable. In addition to using a low-resolution lightmap for GI, I am sampling it with a cubic sampler for extra smoothness...

To produce even remotely realistic GI, a couple of factors should be taken into account:

To tackle the three requirements, I settled for an approach that uses two 3D-textures as the output of the GI computation phase: a HDR RGB lightmap and an incidence map. The latter being a term I coined up for my purposes (there might be something similar out there, who knows).

The HDR lightmap is merely an RGB value which represents each scene cell's light intensity and color at that specific location. The incidence map is quite a bit more interesting: it represents the 3D light incidence vector of the cell, i.e. answering the question "from which direction was most of the light incoming to the cell?" The incidence vector is accumulated to contain a attenuation-weighted vector from each of the nearby emitting cells affecting the receiver. While this approach works very nicely, it has the edge case of multiple light sources creating a boundary area where the incidence vector is cancelled out - this can be worked around by examining the resultant vector length and scaling the incidence effect accordingly.

To add some actual algorithmic meat to this dev post, here's the outline of the GI computation:

Compute the lightmap and incidence map for the scene. The algorithm in high level:

For each cell:

After the above steps the two 3D textures are ready to be consumed by the deferred lighting shader.

Wrap up

It's worth noticing that using this system has the advantage of light/emitter sources being very cheap - in fact, after the pre-computation, each light source has zero cost to the engine.

If you are interested in the minute details of the actual shading process, don't hesitate to ask about them!