In this post I want to share my approach on creating games with indexed pixel art palettes. It’s an experimental approach that is requiring a lot of preparation and there is a lot to improve but maybe it could still be a useful reference to some.
This is a pretty specific topic and will be hard to understand without basic knowledge about shader programming.
I will use the above gif as an example and explain my setup step by step.
Here are few things to consider before we start:
- This is only useful when not using Biliniear Filtering; it’s basicly just useful for pixel games
- I will only cocentrate on lights in this post but gradients can be used for a lot more (e.g. particle systems; color customization)
- The system works similar to a deferred pipeline; A forward approach is possible but I won’t get into that here
- The palettes size is important. Changing things later is a lot of work since it requires encoding the sprites again
- This is using pixel perfect rendertargets as described in the previous post
Let’s take a look at how the whole thing is build. I use different drawing layers (Excluding the HUD) for sorting purposes. These layers are foreground, sprites, background.
The upper row (gray) shows us the fully colored layers. For the next steps we need to define a palette (See the palette part at this post for more information).
The resulting palette has to hold all possible gradients from our scene / game and we will use it to encode our sprites. It’s important to know how many gradients we want to have because adding new ones later is a lot of work. We can also include a row with only our black color as the first. (And one with only white as the last if we want to).
After we defined our palette it’s time to encode our sprites. (Continued at [Channel packing])
[Making the palette]
- The upper row shows all used colors.
- The lower left part is a definition of the light to shadow gradients.
-  Lookup texture of the gradients (+ one row that simply has the black color); this one will be imported into our game later.
-  Horizontal gradient with the size of our lookup texture to get the shading.
-  Vertical gradient to get the gradient row.
- The lowest part is a combination of the 3 to make it easier to manualy look up things later.
Now that we have defined our gradient texture we need to make two grayscale maps. The ones I marked with a red background are pointing to the shade gradient and easy to interpret by the human eye. The green one however points to the earlier defined value of the row of the gradient (The thing we defined at ) so it can be confusing at times.
For the next step we need a graphics programm capable of channel packing to put those two greyscale maps into the r and g channel of a single texture. Here is an example on how it would look like for the sprites.
The dark grey is our original image and the lower two are the encoded two greyscale maps. After packing the maps into the channels the result should look like the light grey area.
If we use the alpha channel for transparency we still have the blue channel to use for other things. This channel could be used for stuff like telling the shader what parts are always lit or (what I do for pixel collision of a tileset) to draw a collision mask but that depends on what the sprite will be used for.
[Bringing everything together]
Now that we finally got our textures ready it’s time to put everything together. Inside the engine we need to generate our light texture (greyscale texture). There are different ways to do this but for this example I’m going with simple light circles. These are script driven and change their size and falloff to simulate “breathing” of the flame. We can simply draw them on quads and capture them into the blue channel of a rendertarget. The result should be something like figure 4.
Now into the r and g channel of the rendertarget we will render our encoded layers (I’m not using a depthbuffer so the layer draw order is important) so we need to render our prepared encoded sprites / tileset with whatever methods we want to use.
For this scene I also let certain sprites and the foreground layer mask themselves into the blue layer so that I can later exclude them from the light. The resulting rendertarget will look like figure 5.
The rest is fairly easy. All we have to do is combine our information in a shader and output it to a fullscreen quad.
For the light I overwrite the inputed texture with something like this:
tex.x = clamp(tex.x - Ambient + tex.b * Ambient, 0, 1);
I use an ambient value to always have a base light level in case not everything is supposed to be dark.
Now we just need to get the color. For this we just have to reconstruct it by using our provided gradient palette.
return tex2D(s_colorTable, float2(tex.x, tex.y));
And that’s it. In case we want some kind of post processing (like a fade out) we can either do that before combining (to make use of the gradients) or write the result into another rendertarget.
It’s best to stay on the low resolution rendertargets for calculations and when we finally want to output our image we just have to render it on a fullscreen quad to the backbuffer.
[Automating the encoding]
There are several ways to do this. The probably easiest way is to work with the lookup gradient table while making the graphics. This might result in a few colors beeing two or more times in the same palette (The black color in this case repeats in every row) but it would be possible to write a script that gets the color ID of the palette and calculates the encoding by itself either at texture import or during the game in loading sequences.
[Proof of concept]
Here is a small sequence of a scene I did in Monogame where I used the gradients on the lights. I also used them for particles and to change the colors of the character (skin tone / hair color / ect).