Friday, September 7, 2012

Modern Alternatives to Palette Swapping

Classic NES and SNES games often would utilize the palette swapping feature of the hardware to make objects of different colors that share the same art. Unfortunately for modern game developers on a budget, palette swapping is no longer a standard feature. Fortunately for everyone, there are lots of new ways to recolor art on modern GPUs. Here is a quick roundup of some popular options. For reference I'll mention the most restrictive version of OpenGL that supports this. For the sake of this article, everything that a version of OpenGL ES can do the same version of regular OpenGL can as well.

Real Palette Swapping (GLES 1.0)
While some support for it is still available, modern palette swap isn't as convenient as it was back in the day. First you have to check to see if the extension is available. Then you have to attach the palette to the image data. If you want to display two sprites with different colors simultaneously, you'll have to have two copies of the same texture with different palettes. While it's not very memory efficient. The inherent efficiency of of paletted images means that 4 copies of a 16 color palleted image will take up the same space as one copy of an RGB565 format image.

Emulated Palette Swapping (GLES 2.0)
Emulating the palette swap is as simple as creating two textures. One, a 1xN color texture using a texture filter mode of GL_NEAREST to hold the colors of your palette and the other, a luminance texture where the brightness is really the color index. Then in your shader you do this:

uniform sampler2D image;
uniform sampler2D palette;//use a Sampler1D on regular OpenGL
varying vec2 texCoord;
vec4 index = texture2D(image, texCoord);
//the math is to make the indexes hit the pixel centers)
gl_FragColor = texture2D(palette, vec2(0,index.r);

Unlike traditional palette swapping, this method also allows you to use colors in between the palette entries. For example if your palette has 16 entries, but your index texture is 8bpp the colors 0 and 16 represent colors 0 and 1 respectively. Color 8 would be a blend half way between those colors. To do this, your texture filter mode needs to be GL_LINEAR. There's a catch to this though, the index values will need to be offset to hit pixel centers by doing this: texture2D(image, vec2(0,index.r*255/256 + .5/256);

Hue Shifting (GL 1.5, GLES 2.0)
A hue shift is a simple way of describing a rotation in rainbow space. In mathematical terms you multiply the color vector by a rotation matrix. This can be done in standard OpenGL by setting glMatrixMode to GL_COLOR and using glRotatef to rotate around the vector (1,1,1). A rotation angle of PI will give you negative color but not negative brightness: green becomes magenta, blue becomes yellow, but white and black stay white and black. Rotating by 2PI will do nothing because just like with regular rotations it will bring you back where you started. While OpenGL ES 1.0 doesn't offer a color matrix, the math can be done manually in a 2.0 pixel shader.

Color Blending (GLES 1.0)
With color blending, a full color texture is used with an accent color. There's no reason to stop at just a single accent color. The triangle or quad used to draw the image gets 3 or 4 gourad shaded colors and each texture unit gets its own blend color. Even further, OpenGL guarantees at least 2 texture units. However don't get too carried away. If you start adding multiple blend textures please stop to think if it would be easier to just use separate art textures for each image without any blending at all.
The easiest way to do all this is to a shader instead of setting all the texture environment modes. However, if you're stuck with OpenGL ES 1.0, Phillip Rideout lays out how all this works in his excellent book "iPhone 3D Programming" you can read the relevant chapter here.
The simplest blend method is to use a glTexEnv mode of GL_MODULATE, which simply multiplies the texture with the color. This is the blending you get in XNA's SpriteBatch. One step up from this is to use the alpha channel in the texture as a blend weight for the color instead of translucency as it's typically used. Unlike some of the more complicated methods, your artist can emulate this effect using layers in photoshop by creating a solid color layer at the bottom of the stack. If you really want to get fancy, you can use a separate texture to provide blend weights per channel, but your artist will probably hate you for doing this.

Color Mapping (GL 2.0)
I recently read about a new method of color adjusting here. This method can accommodate just about every color adjustment Photoshop can throw at an image. First a 16x16x16 3D texture of the RGB color space is placed in 16x16x1 slices on the top of the texture image file resulting in a 256x16 stripe. Then when you apply your color adjustments the image, the color map is updated the same way. Afterward, the stripe is placed back into a 3D texture that represents your color adjustments. In a similar fashion to the shader for the emulated palette swap, use the image texture color as texture coordinates for a look-up in the 3D color mapping texture.

uniform sampler2D image;
uniform sampler3D colorMap;
varying vec2 texCoord;
vec4 imageColor = texture2D(image, texCoord);
//the math is to make the indexes hit the pixel centers)
gl_FragColor = texture3D(colorMap,  imageColor.rgb );