I recently ventured back into the topic of 2D lighting systems and wanted to share a couple of ways to simulate light in your game. This will be technical but not hard to grasp if you have basic graphics knowledge. First I will detail the classic blend mode approach and its benefits, and then I will demonstrate a modern shader based approach to lighting a 2D scene.
Both methods require your game engine to support some sort of render texture (canvas, surface, whatever). Shader support is not required for the first approach but highly recommended for the second. Technically this could all be done on the CPU but it goes without saying it is many times more efficient to use the GPU whenever we can.
Now, this guide will be written into three parts. We won't touch the second approach in this part but stay tuned for the follow ups which I think are more exciting.
Part 1: The blending approach
This method is the most straight forward and can be easily mocked up in Photoshop. You start out with a base layer (your scene) and a seperate layer group combining all the light sources additively over a dark background. The result of this group is then multiplied over the scene for a basic diffuse-like lighting system.
What the layers might look like in a hypothetical Photoshop document.
On the left, the raw scene. In the center, the calculated lighting map. On the right, the combined result.
From a programmer's perspective, this can all be achieved with a single extra render texture and access to blend modes when drawing. You can write a lighting event (separate from the usual draw event) that draws light sources additively to the lighting texture. After they are collected, they can be drawn (multiply blend mode) over the scene with results just as expected from the mockup.
An example from one of my games.
This system has many benefits of its own. For instance, various light styles can be stored in different textures or even as sprite sheets for animated lights. It is also possible to build a shadow system into this by using an intermediate render texture for each shadow casting light. On the intermediate texture, you blacken the shadow volumes over the original light texture and then additively draw the resulting texture to the lighting layer like any other light. There's quite a bit more work involved to actually calculating the shadows, and I consider that beyond the scope of this guide. Here is a great resource if you are interested in soft shadow casting.
I experimented with shadow casting early on in this never completed game. You can see the crudeness in how the shadow casting lights appear to only light the floor and not the walls.
So the nice thing about this render texture/blend mode approach is that you maintain a lot of control of the lighting state. You can draw anything you want to the lighting layer making effects possible such as emissive lighting (parts of your sprites can glow in the dark) or simple shaped light sources like flashlights (the light texture can be anything). And if you are using hardware render textures, you won't see a whole lot of performance overhead (especially if you batch your lights), and your lights can be tinted, scaled, and rotated with minimal cost.
A white rectangle and a short gradient on the lighting layer ensure the sky and top grass always appear fully lit.
This system is perfect if you want to take the familiar route to lights by treating them essentially like any other sprite. The glaring downside is that these lights don't react realistically to the scene in a physical sense. Sure, the colors are additively blended much like real light but the shape of the light is dependent on the texture used. For instance, if you wanted lights to accurately appear to move closer and further from the scene, it would be up to the artist to provide varying attenuations to represent lights at different distances. This is because light doesn't always react linearly so raw scaling of textures is just a very rough approximation of distance. Furthermore, you can't do much to emphasize the depth of the scene as every pixel gets equal treatment. Realistically, sprites closer to a light would be brightened more than the background which is further away. You can approximate this by making your backgrounds darker the further back they are, but this ventures away from the physical model which tells us the attenuation would be affected as well.
In many cases, especially for hobbyists looking to stick within their comfort zone, the upsides outweigh the downsides and this is indeed a fair approach to lighting. After all, the "downsides" I brought up sound almost nitpicky. However, they incidentally make a great segue into the next parts, which will actually bring something new (and yet also very old) to the table, so stick around! I will post the next installment shortly (as soon as I write it :P). In the meantime, if you have questions or want me to elaborate on something, leave a comment.
Until next time…