Best / Easiest way to change color of certain pixels in a single Sprite?

I am used to dealing in just 2D and with much lower level programming, so forgive my newbie Unity question.

I have one 40x40 pixel sprite which is used for all characters, but the player gets to choose the color of their skin, the color of their shirt, and color of their hair. This 40x40 sprite is very simple and pixelated.

So the texture looks funny, with pure red/green/blue/yellow/etc. for the dyeable areas.
In C#, I simply took the image from memory, made a copy of it, dyed the character to the correct colors, and then displayed the image as a sprite.

In this way, I could have multiple characters who all look different. The reason I copied the images before changing them was so that ALL characters wouldn’t change when ONE was colored a certain way. All characters used the same sprite, but different coloring of that one sprite.

I would appreciate any advice as to what is the best or easiest way to do this would be. As I said before, I simply did everything through simple getPixel(), setPixel() functions and a few loops when dealing with the image in memory.

All of it is done before gameplay begins, or before a character is instantiated. So the characters never change color except when they’re being created.

2 Likes

You can do that basically the same way in Unity. Just remember to make the texture readable first (change texture type from Sprite to Advanced to access that bit).

–Eric

1 Like

Oh, awesome! :slight_smile:
Thank you, I will be digging in the API then whenever I get to that point.

1 Like

Mark the texture isReadable and then you can access it and change it.

Another way to do this would be to make a shader which takes various color inputs and recolors it on the fly… but note you’ll get another draw call every time you have a unique shader or texture.

If I mark it as “Read/Write Enabled”, then won’t it change the actual texture file in the asset folder? Permanently change it?

All characters use the same sprite sheet (Sprite, Sprite Mode: Multiple) but each individual character in the game needs to have different colors for certain pixels.

Something like, the original image has pure green (Color 0,255,0) for the hair pixels.
When instantiated, the character’s hair color is given as a parameter during instantiation, with a result like…

Character 1 = brown hair
Character 2 = blonde hair
Character 3 = red hair

All sharing the exact same texture2D sprite sheet, “Assets\Graphics\Character_SpriteSheet.png”

Would it be better to just use a shader? Otherwise, wouldn’t I have to create a unique texture anyway- so either way I have to do an extra draw call? Or can I have character prefabs which each have the Character_SpriteSheet.png unity sprite, and with read/write enabled I can edit one character without also changing the colors on another instantiated prefab, which uses the exact same unity sprite?

No, you need read/write enabled in order to read the texture in RAM. Otherwise it only exists in VRAM and you have no access to it.

A shader is more complicated and has to recompute the texture every frame, which granted may be very fast on the GPU, but nevertheless it’s extra work.

You would of course edit instances of the texture for each character.

–Eric

1 Like

Ah yes, this I discovered as I messed around.

Thank you.

I have been attempting this all night, but so far have little to show for it.

To anyone who is interested and finds this for reference, here is the solution:

//CopiedTexture is the original Texture  which you want to copy.
public Texture2D CopyTexture2D(Texture2D copiedTexture)
    {
               //Create a new Texture2D, which will be the copy.
        Texture2D texture = new Texture2D(copiedTexture.width, copiedTexture.height);
               //Choose your filtermode and wrapmode here.
        texture.filterMode = FilterMode.Point;
        texture.wrapMode = TextureWrapMode.Clamp;

        int y = 0;
        while (y < texture.height)
        {
            int x = 0;
            while (x < texture.width)
            {
                               //INSERT YOUR LOGIC HERE
                if(copiedTexture.GetPixel(x,y) == Color.green)
                {
                                       //This line of code and if statement, turn Green pixels into Red pixels.
                    texture.SetPixel(x, y, Color.red);
                }
                else
                {
                               //This line of code is REQUIRED. Do NOT delete it. This is what copies the image as it was, without any change.
                texture.SetPixel(x, y, copiedTexture.GetPixel(x,y));
                }
                ++x;
            }
            ++y;
        }
                //Name the texture, if you want.
        texture.name = (Species+Gender+"_SpriteSheet");

               //This finalizes it. If you want to edit it still, do it before you finish with .Apply(). Do NOT expect to edit the image after you have applied. It did NOT work for me to edit it after this function.
        texture.Apply();
 
//Return the variable, so you have it to assign to a permanent variable and so you can use it.
        return texture;
    }
 
    public void UpdateCharacterTexture()
    {
//This calls the copy texture function, and copies it. The variable characterTextures2D is a Texture2D which is now the returned newly copied Texture2D.
        characterTexture2D = CopyTexture2D(gameObject.GetComponent<SpriteRenderer> ().sprite.texture);
 
//Get your SpriteRenderer, get the name of the old sprite,  create a new sprite, name the sprite the old name, and then update the material. If you have multiple sprites, you will want to do this in a loop- which I will post later in another post.
        SpriteRenderer sr = GetComponent<SpriteRenderer>();
        string tempName = sr.sprite.name;
        sr.sprite = Sprite.Create (characterTexture2D, sr.sprite.rect, new Vector2(0,1));
        sr.sprite.name = tempName;

        sr.material.mainTexture = characterTexture2D;
        sr.material.shader = Shader.Find ("Sprites/Transparent Unlit");

    }

CopyTexture2D will take a any Texture2D (including a Sprite’s SpriteSheet / Texture2D) and then create a copy of it. If you change the new copy, it will NOT effect the old original Texture2D. This was required for me to both change the pixel’s colors AND change the Texture2D without effecting ALL characters who use that Texture2D.

The Sprite with SpriteMode: Multiple, needs to be turned from a “Texture Type: Sprite” into a "Texture Type: Advanced and have Read/Write ENABLED.

If you want to change the FilterMode or WrapMode, do so in CopyTexture2D, as I have mine set as “Point” and “Clamp”.

To change a pixel’s color , go to the if statement inside of the while loop. The ELSE is required, as that is what copies the image as it was originally without any change to that pixel.

//INSERT YOUR LOGIC BELOW HERE
if(copiedTexture.GetPixel(x,y) == Color.green) //ex. IF the original pixel is Green
{
       texture.SetPixel(x, y, Color.red);        //Then turn the Green pixel into a Red pixel.
}
//INSERT YOUR LOGIC ABOVE HERE

//This line of code is REQUIRED. Do NOT delete it.
//This is what copies the image as it was, without any change to the original pixel.
else
{       texture.SetPixel(x, y, copiedTexture.GetPixel(x,y));
}
8 Likes

Hey Thanks so much Carter. This is very relevant to me right now. I’m more the artist than the Coder and about to upload a set of Animated 2D Fish Sprites to the asset store (here’s the animated demo: https://db.tt/nqNfxYu5). I imagined folks would want to randomly color the fish when instanced for play, so I broke out the Teeth and Eyes as separate sprites on the sprite sheet. This isn’t an awful idea as one may want to animate an eye, but assuming not, Just being about to alter the body color would be great. I’m gonna try to implement what you kindly shared. Hopefully I can figure it out.

Thanks for the great info!
B.

Sure thing! And rock on! Those fish look great.

Here is an addition which I thought was an easy way to handle changing colors during runtime.

What this does, is setup a bool to UpdateColors.
AFTER you change the character’s assigned colors in the game, you then either flag it as TRUE in code, or in the editor you can simply click the “Update Colors” box and it will update the sprite. It doesn’t update unless this bool is activated, so it never updates the sprite’s colors every frame.

	public bool UpdateColors = false;

public void Update()
	{

		if (UpdateColors)
		{
			UpdateCharacterTexture();
			UpdateColors = false;
		}
	}

Here is my entire class, if anyone is interested.
Like I promised, this includes the loop which will update ALL sprites inside of a spritesheet set to ‘Sprite Mode: Multiple’.

using UnityEngine;
using System.Collections;
using System; //added to access enum

public class Character : MonoBehaviour
{
	////Character Data
	//Labels
	public string Name;
	public string Class;
	public string Species;
	public string Gender;
	public bool bearded = false;
	public bool masked = false;

	public bool UpdateColors = false;

	///Render Data
	public bool UpdateAnimation = true;
	public string AnimationName;
	public string DirectionName;
	public Texture2D characterTexture2D;
	public Sprite[] characterSprites;
	private string[] names;
	private string spritePath;

	public Color Hair = new Color32(96, 60, 40, 255);
	public Color Skin = new Color32(247, 207, 134, 255);
	public Color Pants = new Color32(55, 55, 55, 255);
	public Color ShirtL = new Color32(240, 240, 240, 255);
	public Color ShirtM = new Color32(240, 240, 240, 255);
	public Color ShirtR = new Color32(240, 240, 240, 255);
	public Color Object1 = new Color32(127, 127, 127, 255);
	public Color BeardMask = new Color32(247, 207, 134, 255);
	public Color Badge = new Color32(255, 255, 0, 255);
	public Color Outline = Color.black;
	
	// Use this for initialization
	void Start ()
	{
		TempStart ();
	}
	
	void TempStart()
	{
		Species = "Human";
		Gender = "Male";
		AnimationName = "Idle";
		DirectionName = "S";
		spritePath = ("Characters/" + Species + Gender);
		characterSprites = Resources.LoadAll<Sprite>(spritePath);
		names = new string[characterSprites.Length];
		UpdateCharacterTexture ();
		UpdateAnimationImage();
	}

	public Texture2D CopyTexture2D(Texture2D copiedTexture)
	{
		Texture2D texture = new Texture2D(copiedTexture.width, copiedTexture.height);
		texture.filterMode = FilterMode.Point;
		texture.wrapMode = TextureWrapMode.Clamp;

		int y = 0;
		while (y < texture.height)
		{
			int x = 0;
			while (x < texture.width)
			{
				if(copiedTexture.GetPixel(x,y) == new Color32(0,255,0, 255))
				{
					if(masked)
					{
						texture.SetPixel(x, y, BeardMask);
					}
					else if(bearded)
					{
						texture.SetPixel(x, y, Hair);
					}
					else
					{
						texture.SetPixel(x, y, Skin);
					}
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(255,0,0, 255))
				{
					texture.SetPixel(x, y, ShirtR);
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(0,0,255, 255))
				{
					texture.SetPixel(x, y, ShirtM);
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(255,255,0 , 255))
				{
					texture.SetPixel(x, y, ShirtL);
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(255,0,255, 255 ))
				{
					texture.SetPixel(x, y, Badge);
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(55,55,55, 255 ))
				{
					texture.SetPixel(x, y, Pants);
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(247,207,134, 255 ))
				{
					texture.SetPixel(x, y, Skin);
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(127,127,127, 255 ))
				{
					texture.SetPixel(x, y, Object1);
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(96,60,40, 255 ))
				{
					texture.SetPixel(x, y, Hair);
				}

				else if(copiedTexture.GetPixel(x,y) == new Color32(0,0,0, 255 ))
				{
					texture.SetPixel(x, y, Outline);
				}

				//DARK COLORS
				else if(copiedTexture.GetPixel(x,y) == new Color32(191,0,0, 255))
				{
					texture.SetPixel(x, y, DarkenColor(ShirtR, 0.8f));
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(0,0,200, 255))
				{
					texture.SetPixel(x, y, DarkenColor(ShirtM, 0.8f));
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(200,200,0 , 255))
				{
					texture.SetPixel(x, y, DarkenColor(ShirtL, 0.8f));
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(42,42,42, 255 ))
				{
					texture.SetPixel(x, y, DarkenColor(Pants, 0.8f));
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(242,184,77, 255 ) || copiedTexture.GetPixel(x,y) == new Color32(243,188,84, 255) )
				{
					texture.SetPixel(x, y, DarkenSkin(Skin, 0.95f));
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(64,64,64, 255 ))
				{
					texture.SetPixel(x, y, DarkenColor(Object1, 0.5f));
				}
				else if(copiedTexture.GetPixel(x,y) == new Color32(74,48,32, 255) || copiedTexture.GetPixel(x,y) == new Color32(72,45,30, 255) )
				{
					texture.SetPixel(x, y, DarkenColor(Hair, 0.8f));
				}
				
				else
				{
				texture.SetPixel(x, y, copiedTexture.GetPixel(x,y));
				}
				++x;
			}
			++y;
		}
		texture.name = (Species+Gender);
		texture.Apply();
	

		return texture;
	}

	public Color32 DarkenColor(Color32 color, float factor)
	{
		Color hexColor = color;
		hexColor.r *= factor;
		hexColor.g *= factor;
		hexColor.b *= factor;
		color = hexColor;

		return color;
	}

	public Color32 DarkenSkin(Color32 color, float factor)
	{
		//skin      0.969, 0.812, 0.525
		//dark skin 0.949, 0.722, 0.302
		Color hexColor = color;
		hexColor.r -= 0.02f;
		hexColor.g -= 0.09f;
		hexColor.b -= 0.223f;
		hexColor.r *= factor;
		hexColor.g *= factor;
		hexColor.b *= factor;
		color = hexColor;
		
		return color;
	}

	public void UpdateCharacterTexture()
	{
		Sprite[] loadSprite = Resources.LoadAll<Sprite> (spritePath);
		characterTexture2D = CopyTexture2D(loadSprite[0].texture);

		int i = 0;
		while(i != characterSprites.Length)
		{
			//SpriteRenderer sr = GetComponent<SpriteRenderer>();
			//string tempName = sr.sprite.name;
			//sr.sprite = Sprite.Create (characterTexture2D, sr.sprite.rect, new Vector2(0,1));
			//sr.sprite.name = tempName;

			//sr.material.mainTexture = characterTexture2D;
			//sr.material.shader = Shader.Find ("Sprites/Transparent Unlit");
			string tempName = characterSprites[i].name;
			characterSprites[i] = Sprite.Create (characterTexture2D, characterSprites[i].rect, new Vector2(0,1));
			characterSprites[i].name = tempName;
			names[i] = tempName;
			++i;
		}

		SpriteRenderer sr = GetComponent<SpriteRenderer>();
		sr.material.mainTexture = characterTexture2D;
		sr.material.shader = Shader.Find ("Sprites/Transparent Unlit");

	}

	public void UpdateAnimationImage()
	{
		SpriteRenderer sr = GetComponent<SpriteRenderer> ();
		string animname = DirectionName + "_" + AnimationName;
		sr.sprite = characterSprites [Array.IndexOf (names, animname)];
		UpdateAnimation = false;
	}
	
	public void Update()
	{
		if (UpdateAnimation) 
		{
			UpdateAnimationImage();
		}

		if (UpdateColors)
		{
			UpdateCharacterTexture();
			UpdateColors = false;
		}
	}
	
}

It also includes the logic I used to dye these sprites, into these characters:

If you notice, I used all colors that were originally in the sprite, and made them all dyeable.

This also includes logic to change animations based on direction and animation, such a “S_Idle” or “E_Walk1”. As long as the sprite names are correct, such as each sprite being called S_Idle, or South_Idle, etc.

5 Likes

Wow…this is wicked cool!
I think this discussion will broaden and more folk will benefit from your example as more devs who are trying to get a handle on 2D workflow search for a solution to color management of sprites. Making instanced sprite more interesting and extend the artwork’s useful range is a great option.

Again thanks for sharing your discoveries and solutions here. It is much appreciated.

Very cool. I actually went the custom shader route to accomplish this same task, but if I can get this working, I’m sure this will be faster. For posterity, and to add to a great thread, here’s my shader code:

Shader "Custom/PixelColors" {
	Properties
	{
		[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
		_ColorTint ("Tint", Color) = (1,1,1,1)
		_Color1in ("Color 1 In", Color) = (1,1,1,1)
		_Color1out ("Color 1 Out", Color) = (1,1,1,1)
		_Color2in ("Color 2 In", Color) = (1,1,1,1)
		_Color2out ("Color 2 Out", Color) = (1,1,1,1)
		_Color3in ("Color 3 In", Color) = (1,1,1,1)
		_Color3out ("Color 3 Out", Color) = (1,1,1,1)
		_Color4in ("Color 4 In", Color) = (1,1,1,1)
		_Color4out ("Color 4 Out", Color) = (1,1,1,1)
		[MaterialToggle] PixelSnap ("Pixel snap", Float) = 0
	}

	SubShader
	{
		Tags
		{ 
			"Queue"="Transparent" 
			"IgnoreProjector"="True" 
			"RenderType"="Transparent" 
			"PreviewType"="Plane"
			"CanUseSpriteAtlas"="True"
		}

		Cull Off
		Lighting Off
		ZWrite Off
		Fog { Mode Off }
		Blend SrcAlpha OneMinusSrcAlpha

		Pass
		{
		CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag			
			#pragma multi_compile DUMMY PIXELSNAP_ON
			#include "UnityCG.cginc"
			
			struct appdata_t
			{
				float4 vertex   : POSITION;
				float4 color    : COLOR;
				float2 texcoord : TEXCOORD0;
			};

			struct v2f
			{
				float4 vertex   : SV_POSITION;
				fixed4 color    : COLOR;
				half2 texcoord  : TEXCOORD0;
			};
			
			fixed4 _ColorTint;
			fixed4 _Color1in;
			fixed4 _Color1out;
			fixed4 _Color2in;
			fixed4 _Color2out;
			fixed4 _Color3in;
			fixed4 _Color3out;
			fixed4 _Color4in;
			fixed4 _Color4out;

			v2f vert(appdata_t IN)
			{
				v2f OUT;
				OUT.vertex = mul(UNITY_MATRIX_MVP, IN.vertex);
				OUT.texcoord = IN.texcoord;				
				OUT.color = IN.color * _ColorTint;
				#ifdef PIXELSNAP_ON
				OUT.vertex = UnityPixelSnap (OUT.vertex);
				#endif

				return OUT;
			}

			sampler2D _MainTex;			
			
			fixed4 frag(v2f IN) : COLOR
			{
				float4 texColor = tex2D( _MainTex, IN.texcoord );
				texColor = all(texColor == _Color1in) ? _Color1out : texColor;
				texColor = all(texColor == _Color2in) ? _Color2out : texColor;
				texColor = all(texColor == _Color3in) ? _Color3out : texColor;
				texColor = all(texColor == _Color4in) ? _Color4out : texColor;
				 
				return texColor * IN.color;
			}
		ENDCG
		}
	}
}

I started with the basic Default-Sprite shader.

  • Set the sprite sheet’s Filter Mode to Point.
  • You may need to override your Format on your target platform to be Truecolor. If the Format is “compressed”, then Unity may (and did for me) choose a slightly different pixel color, so your swap out may not work.
  • I don’t have any real data to support the speed of this approach to the approach above, but it seems like a custom shader would be slower. I’d love to hear some thoughts here.

To use, do Assets > Create > Shader, copy/paste the code above, then place the shader file in your Assets folder somewhere. Then, Assets > Create > Material and put the Material in the Resources folder. Assign the new material to use the Shader Custom > PixelColors. In the Material inspector, set “in” colors to be the color to find, and “out” to be the new color. Tint will tint the whole image. Then, after you instantiate your object, use:

TheSpriteGameObject.renderer.material = Resources.Load("YourMaterialName", Material);

You can also set the colors (for example, to random colors) in script using:

TheSpriteGameObject.renderer.material.SetColor("_Color1out", Color(Random.Range(0.0,1.0),Random.Range(0.0,1.0), Random.Range(0.0,1.0)));

Hope this helps someone!

7 Likes

Very cool… So curious how this works as a shader. Look forward to trying it, and appreciate your sharing as I imagine many here will too.

Shader language looks like gibberish to me, and I didn’t want to mess with shaders since I already knew how to do it this way.
Thank you for this.

I imagine that if you’re changing colors many times at Runtime, a shader would be better on performance by a significant amount.

But if you’re just changing the colors before load, I imagine a shader would be extra draw calls…or the same draw calls?

I know adding a new texture, means a new draw call. Adding a shader, also means that.

So in the end, a shader might be better in every way? That is my guess, although I am only relying on a few things I’ve read from people who know Unity better than I.

Hopefully I did not miss this, as it’s an important step, but I think I did:

Texture needs to be in Truecolor.

Even my tiny little pixel guy with only a handful of colors, had Unity’s compression totally mess up the RGB(0,255,0) color - which is only 4 pixels, and turned it into 4 different colors. No idea why, as that means MORE colors, not less.

I also would like to to note that another way to handle it, would be to have text files (similar to ASCII), where each letter is a different color- for pixels.
I tried this out on my old project, and it worked. What I’d do is make an image in MS Paint, just like I showed you above (my little guy). Then I wrote a program to take that image, and turn it into a text file with a unique letter for each color. In the actual game, I’d read those text files and then create an image from the unique letters- but color the pixel as I saw fit. So the actual game assets were text files, significantly smaller than any .png image file would ever be.

Compression isn’t about fewer colors, it’s about representing the colors in a more data-efficient way.

–Eric

1 Like

Yea, I guess, but other compression utilities do not destroy my basic sprites like Unity’s does. So something is obviously goofy about Unity’s compression.

It’s not Unity’s compression, it’s the graphics card. That’s how the hardware works…generally either DXT or PVRTC depending on the GPU, and it’s not goofy, it’s lossy. Kinda vaguely like JPEG but with a fixed size; since it removes data, what you get out is not what you put in.

–Eric

1 Like

Well, I thought I knew what I was talking about, seeing as how all other DXT compression utilities are just as lossy.

1491583--83279--$improveit.png

Since Unity is involved in creating the Texture2D, packing sprites, and all sorts of other stuff- it is certainly Unity, not the graphics card, which results in this problem. Hence Unity’s Compression, as opposed to Any-Other-DXT5-Tool’s compression.