Advertisement
  1. Game Development
  2. Shaders
Gamedevelopment

How to Use a Shader to Dynamically Swap a Sprite's Colors

by
Difficulty:IntermediateLength:LongLanguages:

In this tutorial, we'll create a simple color swapping shader that can recolor sprites on the fly. The shader makes it much easier to add variety to a game, allows the player to customise their character, and can be used to add special effects to the sprites, such as making them flash when the character takes damage.

Although we're using Unity for the demo and source code here, the basic principle will work in many game engines and programming languages.

Demo

You can check out the Unity demo, or the WebGL version (25MB+), to see the final result in action. Use the color pickers to recolor the top character. (The other characters all use the same sprite, but have been similarly recolored.) Click Hit Effect to make the characters all flash white briefly.

Understanding the Theory

Here's the example texture we're going to use to demonstrate the shader:

Example texture
I downloaded this texture from http://opengameart.org/content/classic-hero, and edited it slightly.

There are quite a few colors on this texture. Here's what the palette looks like:

A palette

Now, let's think about how we could swap these colors inside a shader.

Each color has a unique RGB value associated with it, so it's tempting to write shader code that says, "if the texture color is equal to this RGB value, replace it with that RGB value". However, this doesn't scale well for many colors, and is quite an expensive operation. We would definitely like to avoid any conditional statements entirely, in fact.

Instead, we will use an additional texture, which will contain the replacement colors. Let's call this texture a swap texture.

The big question is, how do we link the color from the sprite texture to the color from the swap texture? The answer is, we'll use the red (R) component from the RGB color to index the swap texture. This means that the swap texture will need to be 256 pixels wide, because that's how many different values the red component can take.

Let's go over all this in an example. Here are the red color values of the sprite palette's colors:

R color values for palette

Let's say we want to replace the outline/eye color (black) on the sprite with the color blue. The outline color is the last one on the palette—the one with a red value of 25. If we want to swap this color, then in the swap texture we need to set the pixel at index 25 to the color we want the outline to be: blue.

Swap texture with blue highlighted
The swap texture, with the color at index 25 set to blue.

Now, when the shader encounters a color with a red value of 25, it will replace it with the blue color from the swap texture:

The swap texture in action

Note that this may not work as expected if two or more colors on the sprite texture share the same red value! When using this method, it's important to keep the red values of the colors in the sprite texture different.

Also note that, as you can see in the demo, putting a transparent pixel at any index in the swap texture will result in no color swapping for the colors corresponding to that index.

Implementing the Shader

We'll implement this idea by modifying an existing sprite shader. Since the demo project is made in Unity, I'll use the default Unity sprite shader.

All the default shader does (that is relevant to this tutorial) is sample the color from the main texture atlas and multiply that color by a vertex color to change the tint. The resulting color is then multiplied by the alpha, to make the sprite darker at lower opacities.

The first thing we need to do is to add an additional texture to the shader:

As you can see, we've got two textures here now. The first one, _MainTex, is the sprite texture; the second one, _SwapTex, is the swap texture.

We also need to define a sampler for the second texture, so we can actually access it. We'll use a 2D texture sampler, since Unity doesn't support 1D samplers:

Now we can finally edit the fragment shader:

Here's the relevant code for the default fragment shader. As you can see, c is the color sampled from the main texture; it is multiplied by the vertex color to give it a tint. Also, the shader darkens the sprites with lower opacities.

After sampling the main color, let's sample the swap color as well—but before we do so, let's remove the part that multiplies it by the tint color, so that we're sampling using the texture's real red value, not its tinted one.

As you can see, the sampled color index is equal to the red value of the main color.

Now let's calculate our final color:

To do this, we need to interpolate between the main color and the swapped color using the alpha of the swapped color as the step. This way, if the swapped color is transparent, the final color will be equal to the main color; but if the swapped color is fully opaque, then the final color will be equal to the swapped color.

Let's not forget that the final color needs to be multiplied by the tint:

Now we need to consider what should happen if we want to swap a color on the main texture that isn't fully opaque. For example, if we have a blue, semi-transparent ghost sprite, and want to swap its color to purple, we don't want the ghost with the swapped colors to be opaque, we want to  preserve the original transparency. So let's do that:

The final color transparency should be equal to the transparency of the main texture color. 

Finally, since the original shader was multiplying the color's RGB value by the color's alpha, we should do that too, in order to keep the shader the same:

The shader is complete now; we can create a swap color texture, fill it with different color pixels, and see if the sprite changes colors correctly. 

Of course, this method wouldn't be very useful if we had to create swap textures by hand all the time! We'll want to generate and modify them procedurally...

Setting Up an Example Demo

We know that we need a swap texture to be able to make use of our shader. Furthermore, if we want to let multiple characters use different palettes for the same sprite at the same time, each of these characters will need its own swap texture. 

It will be best, then, if we simply create these swap textures dynamically, as we create the objects.

First off, let's define a swap texture and an array in which we'll keep track of all the swapped colors:

Next, let's create a function in which we'll initialize the texture. We'll use RGBA32 format and set the filter mode to Point:

Now let's make sure that all the texture's pixels are transparent, by clearing all the pixels and applying the changes:

We also need to set the material's swap texture to the newly created one:

Finally, we save the reference to the texture and create the array for the colors:

The complete function is as follows:

Note that it is not necessary for each object to use a separate 256x1px texture; we could make a bigger texture that covers all the objects. If we need 32 characters, we could make a texture of size 256x32px, and make sure that each character uses only a specific row in that texture. However, each time we needed to make a change to this bigger texture, we'd have to pass more data to the GPU, which would likely make this less efficient.

It is also not necessary to use a separate swap texture for each sprite. For example, if the character has a weapon equipped, and that weapon is a separate sprite, then it can easily share the swap texture with the character (as long as the weapon's sprite texture doesn't use colors that have red values identical to those of the character sprite).

It's very useful to know what the red values of particular sprite parts are, so let's create an enum that will hold this data:

These are all the colors used by the example character.

Now we have all the things we need to create a function to actually swap the color:

As you can see, there's nothing fancy here; we just set the color in our object's color array and also set the texture's pixel at an appropriate index. 

Note that we don't really want to apply the changes to the texture each time we actually call this function; we'd rather apply them once we swap all the pixels we want to.

Let's look at an example use of the function:

As you can see, it's pretty easy to understand what these function calls are doing just from reading them: in this case, they're changing both skin colors, both shirt colors, and the pants color.

Adding a Hit Effect to the Demo

Let's next see how we can use the shader to create a hit effect for our sprite. This effect will swap all the sprite's colors to white, keep it that way for a brief period of time, and then go back to the original color. The overall effect will be that the sprite flashes white.

First of all, let's create a function which swaps all the colors, but doesn't actually overwrite the colors from the object's array. We will need these colors when we will want to turn off the hit effect, after all.

We could iterate only through the enums, but iterating through the whole texture will ensure the color is swapped even if a particular color is not defined in the SwapIndex.

Now that the colors are swapped, we need to wait some time and return back to the previous colors. 

First, let's create a function which will reset the colors:

Now let's define the timer and a constant:

Let's create a function which will start the hit effect:

And in the update function, let's check how much time is left on the timer, decrease it every tick, and call for a reset when the time is up:

That's it—now, when StartHitEffect is called, the sprite will flash white for a moment and then go back to its previous colors.

Summary

This marks the end of the tutorial! I hope you find the method acceptable and the shader useful. It is a really simple one, but it works just fine for pixel art sprites that don't use many colors. 

The method would need to be changed a bit if we wanted to swap whole groups of colors at once, which definitely would require a more complicated and expensive shader. In my own game, though, I'm using very few colors, so this technique fits perfectly.

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.