Single Color Replace

Hey all, I’m trying to replace white with a material specific color, and do so in such a way that lighting still applies. To that end, I downloaded the Sprite/Diffuse source and went from there. Here’s what I have:

Shader "Sprites/DiffuseReplace"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("ReplaceWith", 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
        Blend One OneMinusSrcAlpha

        CGPROGRAM
        #pragma surface surf Lambert vertex:vert nofog keepalpha
        #pragma multi_compile _ PIXELSNAP_ON

        sampler2D _MainTex;
        fixed4 _Color;

        struct Input
        {
            float2 uv_MainTex;
            fixed4 color;
        };

        void vert (inout appdata_full v, out Input o)
        {
            #if defined(PIXELSNAP_ON)
            v.vertex = UnityPixelSnap (v.vertex);
            #endif

            UNITY_INITIALIZE_OUTPUT(Input, o);
            o.color = v.color;
        }

        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
            if (c[0] == 255 && c[1] == 255 && c[2] == 255) {
                o.Albedo = _Color * c.a;
            } else {
                o.Albedo = c.rgb * c.a;
            }

            o.Alpha = c.a;
        }
        ENDCG
    }

Fallback "Transparent/VertexLit"
}

Far as I can tell, there is no change to the sprite. I have also tried c.r and so on, but that didn’t work either.

Never do an if on an exact color in a shader. That is too specific. Always add a little blending. That is not your main mistake though.

float3 to_replace = float3(1.0, 1.0, 1.0);
float4 result = tex2D(_MainTex, IN.uv_MainTex);
float dist = distance(result.rgb, to_replace);
float factor = saturate(1.0 - dist / 0.1);
result.rgb = lerp(result.rgb, _Color, factor);
o.Albedo = result.rgb * result.a;

(The main mistake is to treat white as having a value of 255. It’s 1 in a shader.)

Thanks, @jvo3dc ! I’m not sure it’s fair to say that’s too specific, though. We want a one-for-one replace. It fits the art style.
Now that the color change is being applied however, it would seem as though lighting is still not factoring in at all. Any ideas as to why that might be?

Using a specific color will work as long as you are using point filtering on your texture, and no anti-aliasing of an kind. The second you start blending colors at all you will get ugly white fringes around your color blocks. It should work for you as long as you are sticking with a very crisp, pixelated look.

If you do run into blending problems, I would use a separate 8-bit mask texture to apple a color tint. Just multiple the color by the mask and the texture. It’s a bit of extra memory, but it’s more robust than trying to do a color replacement based solely on the RGB values.

Your lighting should still work. That is done by the surface shader after your surf function. Just double check that you aren’t accidentally putting anything crazy into the output structure (like a huge value into the emissive channel or something).

And it is if you use the Sprites/Diffuse shader?

@CaptainScience_1 I am indeed using point filtering without anti-aliasing. :slight_smile: Pixel perfect Unity 5 ftw…
To reply to both of you, lighting is in fact being applied to the texture, but not at all the the sections with the color replaced. They (being the color-replaced pixels) for some reason seem to not receive the lighting values at all, though I believe I coded it in such a way that they should. Here’s the code I’m using at this point:

Shader "Sprites/DiffuseReplace"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("ReplaceWith", 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
        Blend One OneMinusSrcAlpha

        CGPROGRAM
        #pragma surface surf Lambert vertex:vert nofog keepalpha
        #pragma multi_compile _ PIXELSNAP_ON

        sampler2D _MainTex;
        fixed4 _Color;

        struct Input
        {
            float2 uv_MainTex;
            fixed4 color;
        };

        void vert (inout appdata_full v, out Input o)
        {
            #if defined(PIXELSNAP_ON)
            v.vertex = UnityPixelSnap (v.vertex);
            #endif

            UNITY_INITIALIZE_OUTPUT(Input, o);
            o.color = v.color;
        }

        void surf (Input IN, inout SurfaceOutput o)
        {
            /**fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
            if (c[0] == 1 && c[1] == 1 && c[2] == 1) {
                o.Albedo = c.rgb * _Color * c.a;
            } else {
                o.Albedo = c.rgb * c.a;
            }**/

            float3 to_replace = float3(1.0, 1.0, 1.0);
            float4 result = tex2D(_MainTex, IN.uv_MainTex) * IN.color;
            float dist = distance(result.rgb, to_replace);
            float factor = saturate(1.0 - dist / 0.01);
            result.rgb = lerp(result.rgb, _Color, factor);
            o.Albedo = result.rgb * result.a;

            o.Alpha = result.a;
        }
        ENDCG
    }

Fallback "Transparent/VertexLit"
}

The only obvious problem I see is that you are multiplying in the vertex color (IN.color) before you are doing your replace. That means it will be messing up the selection of pixels to replace and will be overwriting the IN.color completely when the replacement happens.

Also, if you are using exact pixel values, then you can get rid of the distance function which is expensive especially on older hardware where it will be compiled to multiple shader instructions.

Something like this should work, and uses a couple fewer instructions:

        void surf (Input IN, inout SurfaceOutput o)
        {
            float threshold = 2.985; // Threshold of 0.995 per channel (just under 255/256); added three times, once for each color channel
            float4 result = tex2D(_MainTex, IN.uv_MainTex);
            float luminance = dot(result.rgb, float3(1,1,1)); // Add up the color channels in the texture; dot products are fast
            float factor = step(threshold, luminance); // Step instead of distance is a bit faster; selects all white pixels
            result.rgb = lerp(result.rgb, _Color, factor);
            o.Albedo = result.rgb * IN.color * result.a;  // Multiply the vertex color *after* the replacement is done
            o.Alpha = result.a;
        }

You can try that, but I’m still not sure why the lighting wouldn’t be working. The vertex color you were losing should just be the color coming from the mesh unless I am mistaken. The lighting should all be happening after the surf() function is called so you shouldn’t be able to mess it up from there even if you wanted to.

Here’s an image comparison using your new chunk of code, to prove I’m not crazy:


In the source image, the center trim is obviously perfect white. The red shows no change from the lighting, while the rest of the image clearly does. @_@

I can’t think of why that is happening. I dropped that shader into a basic scene and it seems to be working fine with my test image:

This is a 3D scene, but I’m not sure what would be different about the lighting calculations in a 2D project. If you want to upload a small test project somewhere that reproduces the issue, I can maybe take a quick look and see if I (or someone else) can isolate the problem. There could be some obscure setting somewhere that is affecting things in a strange way.

Can I safely assume you’re familiar with git? And thanks very much for all your help!

I couldn’t replicate the issue in that project either. I just created a new scene with a sprite and a point light. It seems to work with the distance shader that was there, and the step version I posted.

It might be some kind of Unity cache issue where it is still using an older version of the shader, or you have the wrong material assigned accidentally. You can try creating a new shader and material from scratch and copy/paste the code into it to be sure it is using the right code.

Other than that just experiment and poke around a bit. I can’t replicate the issue on my end so I don’t have any good guesses as to what’s causing it.

Did you remove the directional light from the scene? It kind of looks like it.

Oh, actually, I know what it is. It’s because you’re using a completely red color. The values of the blue and green channel are 0 so no amount of light will ever brighten them up at all.

Interesting. I’ll try that, but it does look like disabling the directional scene lighting also makes the point light affect it.

That’s because the directional light is already bright enough to already completely saturate the red to (255,0,0). Adding more light can’t brighten it at all. With even small values in the blue/green channels it will brighten up a bit, but it will take quite a bit of light to get it all the way to white.

Edit: Change your directional light intensity from 0 to 8 using full red (255,0,0) versus mostly red (255, 25, 25) to see the difference.

1 Like

That makes sense, with the lighting being multiplicative… Thanks for all of your help. :slight_smile:
As an aside, why does the color picker in the unity interface work off of HSV with no way to switch to RGB? Gah.