After much trial and error, I was able to achieve what I wanted by modifing Unity's default "GUI/Text Shader" shader, which I downloaded from here under "Downloads (Win)" > "Built in shaders".
I have very little idea how shaders work, but here's my crude explanation of what I think is happening in the shader code below:
- It stores the original pixel art in a variable named
fixed4 sprite_texture
- It stores a solid colored copy of the pixel art, painted with the SpriteRenderer's current color, in a variable named
fixed4 solid_color
- It merges the two variables together using
lerp
, with the SpriteRenderer's current color's alpha value being used to determine how much the original pixel art should be colorized.
Once that shader has been applied to a GameObject's SpriteRenderer's material, I can easily change the GameObject's color through its MonoBehavior code.
Usage example (C#):
"0% red":
this.sprite_renderer.color = new Color(1f, 0f, 0f, 0f);
"50% red":
this.sprite_renderer.color = new Color(1f, 0f, 0f, .5f);
"100% red":
this.sprite_renderer.color = new Color(1f, 0f, 0f, 1f);
I'm not sure if there any drawbacks to this approach, but from what I can tell, it's easy to use, supports all colors (including pure white and black), and supports pixel art with a transparent background, which is exactly what I wanted.
Usage example (GUI):

Full shader code:
Shader "DynamicColorShader"
{
Properties
{
_MainTex ("Font Texture", 2D) = "white" {}
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
}
Lighting Off Cull Off ZTest Always ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ UNITY_SINGLE_PASS_STEREO STEREO_INSTANCING_ON STEREO_MULTIVIEW_ON
#include "UnityCG.cginc"
struct appdata_t
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_OUTPUT_STEREO
};
sampler2D _MainTex;
uniform float4 _MainTex_ST;
uniform fixed4 _Color;
v2f vert (appdata_t v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
o.vertex = UnityObjectToClipPos(v.vertex);
o.color = v.color;
o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}
fixed4 frag (v2f IN) : SV_Target
{
fixed4 sprite_texture = tex2D (_MainTex, IN.texcoord);
fixed4 solid_color = IN.color;
solid_color.a *= tex2D(_MainTex, IN.texcoord).a;
fixed4 final = lerp(sprite_texture, solid_color, solid_color.a);
return final;
}
ENDCG
}
}
}