One fairly common practice to increase visual variety in video games is to re-colour sprites or textures, but runtime colour variation can also be used as a means of allowing players to further customize their game characters or equipment. For our next project, we need to make use of colour variation for both of these purposes and set out to compare some of the different methods for achieving this goal.
We want to be able to take this image and apply new colour variations in-game to allow players to dye their character’s equipment to fit their desired colour scheme.
NOTE: The code shown below is HLSL.
float3 newColour = initialColour * tintColour;
This is the quickest and easiest method of changing a texture’s colour, but it doesn’t produce very good results. The old colours are partially showing through and the whole sword has to be tinted a single colour.
To remove the effect of the old colours showing through, we can desaturate the original image first, but this still leaves us with the issue of only being able to provide a single colour; and in addition, the sword now looks even more monochrome.
float3 Desaturate(float3 colour, float desaturationAmount)
float intensity = 0.3 * colour.r + 0.59 * colour.g + 0.11 * colour.b;
return lerp(colour, intensity.rrr, desaturationAmount);
float3 newColour = Desaturate(initialColour, 1.0) * tintColour;
Desaturated then Tinted
– Very easy to do
– Could produce decent results for items that don’t vary greatly in colour
– Separate parts of the texture can’t have different colours applied
– Monochrome results aren’t very visually appealing
This requires a little more set up than tinting. Instead of saving colour information in our sprites, we can save what is effectively a “colour ID”.
In this image, each channel represents a separate colour ID which we will swap out with the desired colour in our shader.
float3 newColour = initialColour.r * ColourR + initialColour.g * ColourG + initialColour.b * ColourB;
NOTE: ColourA, ColourB and ColourC are parameters passed to the shader that represent the colours we’d like to use in place of the IDs in the mask.
The results produced by masking are a marked improvement over tinting, but there are still some issues.
We can’t easily apply more than three different colours to our sprite; and if we wanted a sprite with a different number of colours, we’d need to make a new shader – not an ideal situation.
– Not much more complicated than tinting
– Produces good results with a low (and consistent) number of colours
– No way to elegantly handle a variable number of colours
– Only allows block colours
– Requires a special workflow for creating sprites
This method involves converting your standard RGB sprites into another colour space (in this case, HSV: Hue-Saturation-Value), adjusting the hue, and then converting back to RGB to be displayed on the screen. This is both the most complicated and most expensive method on this list.
float3 newColour = RGBtoHSV(initialColour);
newColour.r = (newColour.r + HueShiftAmount) % 1.0;
newColour = HSVtoRGB(newColour);
NOTE: Colour space conversion functions are fairly long and have been omitted. It’s easy enough to find them through Google.
The results look pretty good – we’ve managed to keep all five of our distinct colours, but shifted the hue to give ourselves a different result from the original sprite.
– Good looking results
– No need for special set up or change in workflow
– We can’t individually tweak the colours
– Converting between colour spaces is a relatively costly operation
Gradient mapping is often used in visual effects, but rarely finds use outside of that narrow topic. Basically, gradient mapping involves creating a grayscale image and mapping each value to a colour in a separate gradient texture.
Black values in the image to the left correspond the leftmost colours in the gradient, whereas white values map to the rightmost colours.
In our shader we use the values taken from our encoded sprite (left) and use them as the U coordinate to sample into our gradient.
float gradientValue = tex2D(GradientIDs, UV).r;
float3 newColour = tex2D(Gradient, float2(gradientValue, 0)).rgb;
This allows us to specify as many colours as we like, which can be swapped out by replacing the gradient texture (which itself is simple enough to generate at runtime).
One other benefit to this method is that we aren’t restricted to a set number of colours. Although I haven’t done this in the example, we could make use of the “inbetween values” to allow for a smooth falloff from one colour to the next.
– Allows for a much greater variety of colours. The number is limited only by the width of the gradient texture used
– An arbitrary number of colours can be used (varying by sprite) without needing to make any modifications to the shader
– Not restricted to block colours like many of the other options
– A single channel can be used to store all of your colour information (in addition to a small 256×1 texture – which can be shared between many sprites), freeing up two extra channels for other things and saving memory in the process.
– Requires a special workflow. All sprites must now be made as what is effectively a lookup table into a gradient
In the end, we chose to use gradient mapping for our project. The ability for us to have a different number of colours for each sprite is invaluable. We can let players dye their equipment simply by allowing them to change the colours of the “key frames” in the gradient (while optionally allowing more advanced users to play around with the gradient itself, adding/moving/deleting colours).
The one big downside to gradient mapping is that it requires a completely different workflow from what most artists are used to (which becomes even more of an issue when allowing players to create their own sprites – we can’t expect modders to be familiar with uncommon workflows like this). We intend to combat this issue by implementing a comprehensive set of tools inside the editor itself for easily creating these gradient mapped sprites, though that still makes it difficult for players to use image-editing software that they are familiar with.
Ultimately, the choice of how to handle colour variation differs from project-to-project and it’s definitely worth the effort to look into existing solutions to see which is the best fit for your game. Personally, I find gradient mapping to be a really useful but under-used technique and hope to see it used more often in the future.