Using Normal Maps to Light Your 2D Game

Ross Manthorp
22nd April 2021

PolyCrunch Games are the developers of Pyramid Plunge, a cartoony rogue-lite, action-platformer with handcrafted pixel-art. They join us on the blog to share the way they use normal maps to bring awesome lighting their upcoming game.


Using 3D shading techniques when making 2D games is an interesting one to create immersive environments that react to dynamic light sources. I'm using it in my game Pyramid Plunge for lighting the environment. However, I'm not using it on the characters, as I wanted to keep them looking cartoony and thus flat shaded.

How it works

You will need a simple shader that calculates shading similar to how its done in 3D, but instead of reading normal information from the 3D object or normal maps applied to 3D objects, we supply the normal maps as sprites. At the end of the article you'll find a sample project to play with.


But what are normal maps?

In 3D, a normal is just the direction perpendicular to the object's face or polygon. In 2D it will be the normal direction for that particular pixel being shaded. This information helps the shader to calculate how the light bounces on the object towards the camera.
Source: Wikipedia 
A normal map is just an image/texture/sprite which has the normals information for each pixel, encoded in the Red, Green and Blue (RGB) channels of the image. RGB info correspond to XYZ coordinates for the vectors representing the normal.

Left: Sprite      Right: Normal map
So when the shader is going to calculate the color (shade) of a pixel, it will get the normals information from the normal map. It will combine it with color information and according to the lights setup, it calculates the resulting lit color according to some formula (e.g. Phong shading).

We can also add specular lighting which are those highlights typically found on metals. You would need another map called the specular map, and feed it to the shader too. But to keep it simple, we'll avoid specular for now.

So how can we achieve this in GameMaker?

First we need draw everything on a surface which will be only the color information (aka diffuse or albedo in 3d). We'll call this the Diffuse Pass. Then we need to draw all the normals information of each object on another surface. We'll call this the Normals Pass. (We could also have a 3rd pass for the specular pass if we wanted to)
By default, the objects in GameMaker are drawn on the Draw Event. I wanted to keep it as simple as possible, especially because some objects didn't even have normals information. So I created a parent object which I call obj_Renderable. This overrides the Draw Event and the Draw Begin Event. In the Draw Begin Event we will be drawing for the Diffuse Pass, and the Draw Event it will be drawing for the Normals Pass.
Now we don't want to be swapping between the diffuse and normal surfaces for every object, since that would turn out very inefficient and you'll soon have a low-fps running game. Instead we need to have some object (obj_Lighting) that will prepare the appropriate surface for the Diffuse Pass and Normal Pass before Draw Begin Event and the Draw Event respectively. GameMaker, as usual, will automatically call all the objects’ Draw Begin Events but this time they will be drawn on the diffuse surface. Next the same object (obj_Lighting) will prepare the next surface for the Normals Pass where all objects' Draw Events will be drawing on. Finally the two surfaces need to be passed to the shader to calculate the shading according to the light sources. This final step is done by the obj_FinalizeRenderer

How to draw/generate the Normal Maps

There are several ways how to generate the normal maps.

1) Auto-generators

You can use tools like the free opensource tool Laigter. There's also NormalMap Online. But the problem with auto-generating is that they try to guess 3d data from 2d image. You can try to see what they'll come up with, but sometimes it's just not enough.

2) Manual drawing

You can use any drawing tool. Just have a reference of the normal map sphere. Aseprite has a normal map mode too where you pick a color from the sphere and just draw away. You can also draw manually with Laigter

When using the normal sphere, you need to imagine that it is a ball in front of you, and pick the color according to the face you're trying to draw.
E.G. for this crate, the left side's normal would be something like the above picked color (my shader expects the Red channel flipped!). 
This process can be tedious but gives the best results, as you are 100% in control.

3) Using a 3d modelling application.

You can also use Blender which is free to download and use. There are some good tutorials for the basics. I've used Blender for the columns in Pyramid Plunge. I generated the model in 3d, then after setting it up to export normal maps information, I could easily adopt them for the game.


Sample Project

You can download the sample project for GMS2 here. Feel free to adapt to your project. The objects you want to start from are obj_Lighting and obj_FinalizeRender. You will also notice the text is drawn on Draw End, so it's not affected by lighting.

Known Limitations/Disadvantages

  • The shader will have a limited number of light sources. This number is set in the shader and is currently set to 8 (can probably be increased).
  • There is some getting used to at first since you're drawing on surfaces. So if you have a camera, the drawing positions need to be offset by the camera position accordingly.
  • If you want smooth shaded light, just disable the graded attenuation in sh_normal.
  • Be careful of red channel in normal maps. Shader in sample expects it flipped when generated with tools. You can easily change it.
If you have any questions, feel free to ask me on @polycrunchgames.
Also it would be awesome if you wishlisted Pyramid Plunge on Steam!
Written by Ross Manthorp
Ross Manthorp handles all things community at the GameMaker team. When he’s not pulling the strings from behind the scenes he’s enjoying Nintendo games, indie games, and getting emotional over cartoons and comics.
Back to blogs