Hi everyone! I was prompted to write one of these more thorough explainy posts by
a question on the Unity3D subreddit...
Sprites and Color ReplacementI'll talk about solving
two issues:
- Recoloring parts of a pixel art sprite dynamically
- Replacing entire sprite sheets in an animation
Sprites with varied colors.In Kingdom, both the citizens and the player can have different outfit and skin color variations. I'd like to avoid drawing sprites manually for each variation. And since each sprite has multiple dynamic colors (e.g. skin and clothes color), drawing sprites for the
combinations of these is just not feasible at all. One option would be to use white/uncolored sprites, split those out into layers, one for skin, one for clothes, and a regular sprite. Then, using the SpriteRenderer's "Color" property we could recolor each of these layers. The downside of that method is that we have to animate each of those layers separately, which could turn out to be a hassle.
RecoloringSo instead, I decided to recolor the sprites using a
shader. The primary idea here is that we can use the
alpha channel to store information about which part of the sprites are recolorable. This means we lose some of the information that the alpha channel usually confers, but most of my sprites are 100% opaque or 100% transparent anyway.
So, we
use a special alpha value to indicate that a pixels should be recolored. To avoid having to check for precise floating point equality, I decided to use intervals of the alpha value.
Now in the Fragment shader, we output the "new color" value given by a shader property if the alpha falls in either of these ranges.
void surf (Input IN, inout SurfaceOutput o)
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * IN.color;
#if defined(RECOLOR_ON)
o.Albedo = lerp(_RecolorB.rgb, _RecolorA.rgb, step(0.75f, c.a)) * c.r;
o.Albedo = lerp(o.Albedo, c.rgb, step(0.875f, c.a));
o.Alpha = step(0.5f, c.a);
o.Albedo *= o.Alpha;
#else
o.Albedo = c.rgb ; // * c.a;
o.Alpha = c.a;
#endif
// ...
The two lines with the "
lerp" are the lines doing the work.
step outputs 1 or 0 depending on if the alpha value is above or below 0.75. This determines which color is used. Then another similar line inserts the
original sprite color if the alpha is above 7/8ths (0.875). Additionally, the inserted color is
multiplied by the value of the red channel. This allows us to store brightness information in the red channel, so that a recolored pixel can still have its brightness modulated. You can see the effect of this in the screenshot above, where the outfits have darker parts in a matching tint.
Finally, the alpha value is thresholded at 0.5, effectively discarding any semi-transparency information, and just returning either fully opaque or fully transparent pixels. This is the cost of storing other information in the alpha channel. Of course, I could have been more efficient with the alpha information, but at the moment I don't need pixels that are
both semi-transparent and recolored.
Asset workflowNow you might think that these values (6/8, 7/8) seem a bit arbitrary. They are, but they were chosen to make the workflow of generating sprites with these special values a little easier. I chose to use fractions of 8 because hex colors are fractions of 16*16. Long story short, by using alpha values of 0xE0, 0xC0 and 0xA0 I am sure that they fall right in the middle of those 1/8th intervals. I just found out I'm probably doing the math wrong, but it doesn't matter as long as the alpha values are somewhere in the interval.
In PyxelEdit, I just draw with specific skin and clothes colors, and then replace those using a little script before importing into unity:
Convert to special alpha values. Note the transparency.#!/usr/bin/env bash
# Cloth colors
mogrify -channel RGA -fill "#FFFFFFD0" -opaque "#567271FF" "$@"
mogrify -channel RGA -fill "#AAAAAAD0" -opaque "#394b4aFF" "$@"
# Skin colors
mogrify -channel RA -fill "#FFFFFFB0" -opaque "#edbebfFF" "$@"
mogrify -channel RA -fill "#CCCCCCB0" -opaque "#bd9898FF" "$@"
Part 2: Replacing an entire sprite sheetAnother way to vary sprites is to replace a sprite sheet entirely. This means generating new animations for the alternative sprite sheet. I had already decided to just
duplicate the Animator Controller for each sprite variation, in this case, the Queen and the King.
Duplicating the Animator and replacing the animations in the "motion" field for each state is not too hard. The problem is generating all these duplicate animations, they are exact matches of the regular animations. My sprites are named systematically, I just need a way to copy the animations and replace the "sprite" field of each keyframe. So I wrote
a 'little' editor script for that. For each sprite keyframe, it looks up a sprite with a word replaced, and inserts that.
And that's it. That was a lot shorter than I thought it would be, so if you have any questions, please ask.