Pages

Friday, April 24, 2015

Sprite palette swapping with shaders in Unity

As any game developer knows, asset reuse is your friend. Whenever the design calls for a new character, I often turn to our existing assets to see if I can create what we need more quickly and cheaply than contracting out another character sprite sheet.

One method for reusing assets is to swap out the colors. RPGs have used this trick for years, and it works just as well in Hellenica.


Every sprite in Hellenica uses a relatively limited color palette. To make a new variation, we just choose a few colors in the palette and assign new colors. Here's what our friendly pirate's color palette looks like:


What I ended up doing is creating a custom shader that takes a sprite and a small palette texture (like the one above) and swaps the colors on the fly.

-- Start of the technical bit! --

Creating the palette texture is pretty straightforward, but I thought it would be interesting to explain how the shader knows which color to use when it's drawing a pixel.

One way to do this is to embed the palette indices straight into the original sprite. But that presents a tricky problem! How can I change the data stored in the image without changing how the image looks? It turns out that it's doable if you don't change too much.

Before diving in to how this works, it's important that we're on the same page regarding digital color. For the purposes of this post, all you need to know is that every displayable color can be represented as a combination of three colors: red, green, and blue, or RGB for short. If you've used a paint program, you've probably seen this before:


If the color depth is sufficiently high, changing these RGB values by very small amounts doesn't change the final color much. In our case, we're using 32-bit color depth, which means each RGB value can range from 0 to 255.

This image contains 64 different colors, but to my eye it's all the same color. You can open it up in a paint program and poke around with the eyedropper to see what I mean.


Starting with a bright red color, RGB(252, 0, 0), I generated all possible color combinations that can be made by adding 0 - 3 to each of the color channels (red, green, and blue). That's 4 * 4 * 4 = 64 combinations.

This is the approach I'm using to embed the palette index into the original sprite. Since all of our character sprites in Hellenica use fewer than 64 colors, I can safely store the palette index inside the sprite pixels without modifying the end result visually.

Here's what it looks like when I modified our pirate friend:


The pirate on the left is the original source sprite, but the pirate on the right is the sprite that I modified. Can you tell the difference?

Here's a visual breakdown of the process for storing the indices:


We support at most 64 unique colors, so that means we only need 6 bits to represent the color index. First, I split this index into three separate 2-bit values. Then I hide these in the two least significant bits of the three color channels (red, green, and blue). As you saw before, since I'm only changing the two least significant bits, the visual impact is negligible.

I put together a little editor script to generate a color palette texture from a sprite and embed the indices using this process. Then I made a shader that can pick out the index and use it to look up the actual color in the palette texture.

Putting all of this together, creating a new variation now takes about as much time as it takes to swap out some colors in Paint.

-- End of the technical bit! --

Here are some variations on our pirate friend and the associated palette textures:


Aside from simpler color swapping, this technique is also amenable to some more imaginative uses!


I'm really happy with how things turned out, and I'm sure I'll come up with more uses as the project progresses. Let me know what you think in the comments!

3 comments:

  1. Does it work with bilinear filter?

    ReplyDelete
  2. No, any blending of the palette or index textures will result in noisy output from the shader. We only use Point filtering for our pixel art.

    ReplyDelete
  3. I know this post is crazy old but I'm hoping you're still out there, and willing to share some more info. I'm trying to replicate your approach here but I'm having trouble extracting the index from the color value within the shader. I was hoping that you could share some more info around how you were able to get that working within the shader code.

    ReplyDelete