Yesterday I released the latest update to Sprite Lamp, and with it, a properly implemented and documented palette system. Still pending are shaders for various engines to make it usable in your games, but in the mean time, I’m going to give a quick rundown of what it is, how it works, and how the (really pretty simple) shader works.
Why palettes?
This is something that came up due to repeated requests from artists. As a graphics programmer, my first thought was to calculate the light based on the angles of the light and the surface normal using standard Lambertian and basically call it a day. However, as several artists pointed out to me, this means that when a part of an image gets darker it approaches black, and artists very rarely use real black in their paintings. Shadows are more likely to have some blue to them, and highlights are likely to be slightly yellow rather than white. There are various ways to get around this – tweaking the ambient and direct light colour, for instance – and while those options remain, I thought it would be good to give artists more direct control. Hence the palette system.
This works by taking the diffuse map (that the artist creates), taking all the unique colours from it, and creating a sort of template palette image. This palette is then saved out and modified by the artist to get the effects they want. A palette image has a vertical column of pixels for every unique colour in the depth map. The vertical position in this column represents the colours that those pixels will be at different lighting levels. The midpoint of each column is the colour when that pixel is lit with full diffuse lighting but no specular lighting. Below that it fades as the diffuse lighting drops away to nothing, and above that it represents the colour when an increasingly strong specular highlight is added. Sprite Lamp can be used to generate either an ’empty’ palette that will create flat lighting (the columns are the same colour all the way up), or a palette that will produce simple calculated lighting, fading to black at the bottom and white at the top.
As an example, here is the diffuse channel of the Sprite Lamp zombie:
And here are the autogenerated palettes for said zombie (with and without default lighting built in):
From here, you save one of these images out (whichever would work best for you as a guide) and then change the colours as you see fit. The only really important thing is that you generate the index map (more on that later) and then make sure you keep the horizontal positions of the pixel columns in the same place/order in the palette.
Here are some examples of effects that are possible using this system, with the zombie’s palette image visible on the inset.
The Shader
So, as promised, I’m going to give a quick outline of how the shader works. It’s actually not very complicated, but it hinges around something that Sprite Lamp will generate for you, called an index map. You’ll need a normal map, as usual, and then a diffuse map, and from the diffuse map Sprite Lamp will generate the palette map template (which you then modify) and the index map (which you don’t). The index map for the zombie looks like this:
The grey values in this image are actually a value from 0-1, representing the horizontal position in the palette map where that column of colours resides. Black represents the leftmost column of the palette map, then the second darkest grey (the hair) will be the second column across, and so forth. This is why it’s important to keep the columns of the palette map in the same place.
As the shader programmer, all you need to do is calculate some lighting value between zero and one (the way Sprite Lamp’s palette shader does this is by averaging the diffuse and specular components and then clamping to the correct range). Then, a look up into the index map will get you a luminosity value between zero and one. From there, you do a look up into the palette map, using the value read from the index map as your U coordinate, and the calculated level of illumination from your lighting algorithm of choice for the V coordinate, and you have your final colour. Because the whole point of the palette system is to give the artist precise control over the colours that end up on the screen, nothing more is done – the colour is outputted onto the screen. The pseudocode for the shader might look something like this:
float colourLevel = (diffuseLevel + specularLevel) * 0.5; float indexPosition = tex2D(indexMap, textureCoords.uv).r; //Note that we use 1.0 - colourLevel because usually positive V //goes down the texture map. vec3 finalColour = tex2D(paletteMap, vec2(indexPosition, 1.0 - colourLevel);
The generation of the index map
For the most part this isn’t something you’ll be worried about, but there are certain circumstances where it’s important to know the details. Sprite Lamp generates a palette map from the diffuse map to use as a template, but then it generates the index map from the diffuse and the palette maps. It does this by going through every pixel of the diffuse map, and then searching for that pixel’s colour in the middle row of the palette map (that is, it looks through the row of pixels half way down the palette map). When it finds the colour it’s looking for, it will use the horizontal position of that to determine the greyness of that pixel in the index map.
This probably isn’t terribly relevant to most use cases, but it’s possible that you will want to use a palette map for more than one piece of art in your game (to render a whole scene coherently, for instance). In that case, you’ll want to make your own palette map from scratch, making sure the important colours are present all the way along the centre. Having done that, paint all your diffuse maps using only colours from that centre row of pixels. Then, load up a given diffuse map, and rather than generating a palette from it, open up the palette you made into the palette map, then click the ‘generate index’ button. If the diffuse colours are all present in the palette’s central row, Sprite Lamp should generate a fully functional index map without issue – it doesn’t matter that there are colours in the palette not present in the diffuse.
Conclusion
I would imagine that this problem has been tackled by other developers at various points through gaming history – this approach is just what came to mind to answer artists’ complains about black shadows. Given that, this explanation is straight from my brain to the page, as it were. Please, let me know if there’s anything I’ve been unclear on, so I can fix my explanation.
More importantly, if you do anything cool and unexpected with this system, I’d be keen to see your work. I’ve already been surprised/impressed with Halley’s implementation of the Drawbringer palette from above, and I’m confident that various cel shading and other techniques are possible using this system too.