Search Unity

Procedural change of sprite colors via shader

Discussion in 'Shaders' started by VLukianenko, Jul 5, 2017.

  1. VLukianenko

    VLukianenko

    Joined:
    Mar 27, 2017
    Posts:
    30
    Hello! I'm trying to achieve the most efficient way to change colors in a shader for a sprite.

    Input: a sprite
    Output: a sprite with different colors, passed in via script (random colors) or at least via palette (non-procedural)

    I've read this article - but there is a problem where compression makes this method work in a weird way. I tried masking via getting pixel grayscale brightness, but then some similar colors can get mixed up and again - compression. I tried masking (indexing) in alpha channel, but then my shader ends up in 200 operations per fragment if I want to change 10 colors - which isn't the most efficient way.

    It sounds like quite simple task, because (I think) it's done in so many games, but here I am researching on the topic for a week, implementing different methods never getting a satisfactory result. Have anyone done anything similar and could share best practices or point out a direction to a good solution?

    Thanks.
     
    Last edited: Jul 5, 2017
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,339
    Changing lots of colors in a shader efficiently for the general case is actually kind of hard.

    The example you linked to is a pretty good idea, but it relies on point filtering and disabling compression on the textures. If I were going down a route similar to that I would probably just have all my sprites be grey scale using an Alpha8 format (uncompressed, but single channel, so about the same memory usage as a compressed texture) then map the grey scale values as indices into a palette texture.

    A more common technique for recoloring stuff is using a mask texture, but since you only have 3 or 4 colors per mask you only get 4 or 5 (R, G, B, A, and nothing) regions you can control unless you use a second mask texture. If you want 10 colors that's going to be difficult.

    You could also do what color grading post processes effects do and use a 3D texture to map one color to any other color ... but that's probably overkill for what you want.

    Honestly your alpha channel indexing is a fine idea. Even with a compressed texture you shouldn't have a problem (though you will still need to use point sampling, and compression might still mess it up a little).

    I suspect your shader looks something like this:

    if (alpha < 16)
    // transparent
    else if (alpha >= 16 && alpha < 32)
    // use color A
    else if (alpha >= 32 && alpha < 48)
    // use color B


    ... etc etc ...


    Don't do that. Just have a texture that's as many number of colors you want to be able to have control over wide, and 1 pixel high, set it to uncompressed, clamped wrap, no mip maps, and point filtering, then just do something like this:

    fixed4 tintColor = tex2D(_PaletteTex, float2(alpha, 0));

    Here's a version of that little guy done in grey scale with a palette of 16 possible colors:
    Old hero.png

    The palette you can download below. Set the hero texture to be uncompressed, alpha from greyscale, no mips, point filtering. You can set the texture's platform override to be Alpha8 as well. Set the palette to uncompressed, no mips, point filtering. Then use this shader:
    Code (CSharp):
    1. Shader "Unlit/AlphaToPaletteShader"
    2. {
    3.     Properties
    4.     {
    5.         [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {}
    6.         [NoScaleOffset] _PaletteTex ("Palette", 2D) = "white" {}
    7.     }
    8.     SubShader
    9.     {
    10.         Tags { "Queue"="AlphaTest" "RenderType"="TransparentCutout" "PreviewType"="Plane" }
    11.         LOD 100
    12.  
    13.         Pass
    14.         {
    15.             CGPROGRAM
    16.             #pragma vertex vert
    17.             #pragma fragment frag
    18.             // make fog work
    19.             #pragma multi_compile_fog
    20.          
    21.             #include "UnityCG.cginc"
    22.  
    23.             struct appdata
    24.             {
    25.                 float4 vertex : POSITION;
    26.                 float2 uv : TEXCOORD0;
    27.             };
    28.  
    29.             struct v2f
    30.             {
    31.                 float2 uv : TEXCOORD0;
    32.                 float4 vertex : SV_POSITION;
    33.             };
    34.  
    35.             sampler2D _MainTex;
    36.             sampler2D _PaletteTex;
    37.             float4 _PaletteTex_TexelSize;
    38.          
    39.             v2f vert (appdata v)
    40.             {
    41.                 v2f o;
    42.                 o.vertex = UnityObjectToClipPos(v.vertex);
    43.                 o.uv = v.uv;
    44.                 return o;
    45.             }
    46.          
    47.             fixed4 frag (v2f i) : SV_Target
    48.             {
    49.                 fixed alphaIndex = tex2D(_MainTex, i.uv).a;
    50.                 float paletteU = (alphaIndex * _PaletteTex_TexelSize.z + 0.5) * _PaletteTex_TexelSize.x;
    51.                 fixed4 col = tex2D(_PaletteTex, float2(paletteU, 0.0));
    52.                 clip(col.a - 0.5);
    53.                 return col;
    54.             }
    55.             ENDCG
    56.         }
    57.     }
    58. }
    59.  
     

    Attached Files:

  3. VLukianenko

    VLukianenko

    Joined:
    Mar 27, 2017
    Posts:
    30
    Thanks for your input! The idea with just one channel sounds quite cool! It'll save me 4x on texture size so there can be no compression which is mainly the problem! I'll try to stick to that.

    And yes, my shader does quite that, but in a "shader-optimized" way. It masks out the part of the texture that fits in the current index-range (index pointer is alpha value of color you want to replace with) and colors it. Here's my method for doing that, it works fine for few alpha-masked colors but when there are many - it's too many operations:

    Code (CSharp):
    1. fixed4 MaskColor(float currentAlpha, fixed4 replacementColor) {
    2.  
    3.                 fixed alphaMin = saturate(replacementColor.a - 0.047f);
    4.                 fixed alphaMax = saturate(replacementColor.a + 0.047f);
    5.  
    6.                 fixed isGreater = max(sign(currentAlpha - alphaMin), 0.0);
    7.                 fixed isLess = max(sign(alphaMax - currentAlpha), 0.0);
    8.  
    9.                 fixed mask = isGreater * isLess;
    10.  
    11.                 fixed maskInverted = 1.0 - mask;
    12.                 fixed3 newColor = max(replacementColor * mask, maskInverted);
    13.                 return fixed4 (newColor.rgb, 1);
    14.                 }