Shader lerping colors and color values above 1

I have source images where each pixel is one of exactly 4 colors:

  • red = (1,0,0)
  • green = (0,1,0)
  • blue = (0,0,1)
  • black = (0,0,0)

I’m trying to modify Unity’s default sprite shader to use those colors (besides black) as a key to change to other colors. For example, any pixel that’s red should become _RedColor. My first attempt was:

fixed4 _RedColor;
fixed4 _BlueColor;
fixed4 _GreenColor;
  
fixed4 frag(v2f IN) : SV_Target {
    fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;
  
    fixed4 modColor;
    modColor.rgb = _RedColor.rgb*step(1, c.r);
    modColor.rgb = _BlueColor.rgb*step(1, c.b);
    modColor.rgb = _GreenColor.rgb*step(1, c.g);

    modColor.rgb *= c.a;
    return modColor;
}

This worked. The problem was it was very jagged. In exporting the image and being compressed in Unity I think some pixels had multiple colors but the shader simply would set the color to the last matching color.

So I tried to modify it to not reset the color if the next color matches, but instead blend between any colors present in the pixel weighted by their contribution. Here was my second attempt:

fixed4 frag(v2f IN) : SV_Target {
    fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;
       
    float total = c.r + c.b + c.g;

    // we set the total to 1 if it's 0 so that we don't divide by zero when the color is black
    total += step(total, 0); 

    c.rgb = _RedColor.rgb*c.r + _BlueColor.rgb*c.b + _GreenColor.rgb*c.g;
    c.rgb /= total;

    c.rgb *= a;
    return c;
}

This also worked and the jaggedness went away everywhere except the border of black and nonblack colors. So then I tried getting rid of the total and it was perfect:

fixed4 frag(v2f IN) : SV_Target {
    fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;

    c.rgb = _RedColor.rgb*c.r + _BlueColor.rgb*c.b + _GreenColor.rgb*c.g;

    c.rgb *= c.a;
    return c;
}

It’s perfectly blended between any of the pixels. But I don’t get why. I was dividing by the total so that the color values wouldn’t go above 1 and it would blend between them. For example if a pixel had both red and blue present it would be adding the 50% of _RedColor’s rgb value and 50% of the _BlueColor’s rgb value. But when I didn’t divide by the total it appears nothing changed besides blending between black and other colors.

This implies to me that I never had any pixels that actually had more than one nonzero color component to them (or else I would be getting white or weird colors at those pixels). But if that’s the case, why did switching from the first step function approach to a blend approach even do anything?

This is my first foray into shaders so it’s entirely possible I’m doing something terribly, but I just want to wrap my head around what’s going on and why. So I definitely appreciate any help/ideas.

It works because all your pixel values are close to 1 or 0 except where compression muddies the edges. So you would be adding 99% of your red color, and 1% of your blue color (divided by 1 for no change). You should set compression to 16-bit, use your last version of the shader, and you won’t get any compression artifacts at all.

That’s also why your first shader failed. A red value of 99% due to compression would step to 0, not 1. You could probably keep compression on if you used a slightly more lenient cutoff for your values. Using 0.99 as a cutoff would probably work better.

1 Like

Ahhhh. That makes perfect sense.

I was assuming compression would cause some pixels to be (0,1,1) for example but instead you’re saying it blends to something like (0, .99, .1) so it still adds to 1 so I don’t need to divide by the total.

My last question would be which of the versions of the shaders is more performant or are they both pretty much the same?

The step function is fairly heavy and you don’t need it to do what you want.

is the step() function really heavy? do you have any data about this?
we are using step() in pixel shader for a mobile game, so I am a bit worry about this.

I’m pretty sure it’s not as heavy as using if or alpha cutout shader… Quite fast if I may say…

From: http://http.developer.nvidia.com/Cg/step.html

float3 step(float3 a, float3 x)
{
     return x >= a;
}

That’s a branch. It’s simple so it probably won’t kill you (and maybe has some hardware optimizations? dunno), you’ll have to profile on device to know for sure how expensive it is.

It sounds like you don’t need step in this case at all but here’s an alternative:

modColor.rgb= _RedColor.rgb*step(1, c.r);

…could be…

//subtract one so we only get values above 1
float mask = c.r -1;

//multiply by some big number so we get white above 0
mask *= 900.0;

//saturate it for sanity
mask = saturate(mask);

modColor.rgb = _RedColor.rgb * mask;

Extra whitespace for clarity.

1 Like