(noob) frag shader to combine textures on sprite

I decided its time for me to learn shaders, reading some tutorials and watching Unite talks. This is what I want to achieve:

So my base texture acts like a mask (for alpha shape) but also I can tint it a colour. This base texture is a character sprite with more detail (trying to get this working using a basic white shape). I want to overlay a 2nd texture, but be able to tint that one too. That 2nd texture overlay, imagine a simple photoshop layer with the tint and alpha changed.

So progress:

So trying this out in the frag shader I do
fixed4 returnTexture = (mainTexture * _ColorMain) + (overlayTexture * _ColorOverlay);

Output

fixed4 returnTexture = (mainTexture * _ColorMain) + (overlayTexture * _ColorOverlay);

fixed4 returnTexture = mainTexture * _ColorMain;
if(overlayTexture.a > 0 && mainTexture.a>0){
returnTexture += (overlayTexture*_ColorOverlay);
}
returnTexture.a = mainTexture.a;

getting closer…

fixed4 returnTexture = mainTexture * _ColorMain;
if(overlayTexture.a > 0 && mainTexture.a>0){
returnTexture = (overlayTexture_ColorOverlay);
}
returnTexture.a = mainTexture.a;

Anyone have any tips for me on the way to add a texture (on top of) another? Some sort of blend mode I’m not understanding yet, instead of + and * ?

Many thanks

Oops here is the shader code so far

Shader "MyTests/textureCombine" 
{

    Properties 
    {
        // main
        _ColorMain ("Base Tint", Color) = (1,1,1,1)
        _MainTex ("Base Texture", 2D) = "white" {}

        // overlay
        _ColorOverlay ("Overlay Tint", Color) = (1,1,1,1)
        _OverlayTex ("Overlay Texture", 2D) = "white" {}

    }

    SubShader
    {
        Tags {
            "IgnoreProjector"="True"
            "Queue"="Transparent"
            "RenderType"="Transparent"
        }
        ZWrite Off
        Lighting Off
        Cull Off
        Fog { Mode Off }
        Blend SrcAlpha OneMinusSrcAlpha
        
        Pass 
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #pragma fragmentoption ARB_precision_hint_fastest
            #include "UnityCG.cginc"

            // variables
            fixed4 _ColorMain;
            fixed4 _ColorOverlay;

            sampler2D _MainTex;
            sampler2D _OverlayTex;


            // input
            struct vertexIn
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float2 texcoord1 : TEXCOORD1;
            };

            // output
            struct vertexOut
            {
                float4 vertex : SV_POSITION;
                float4 texcoord01 : TEXCOORD0;
                float4 texcoord02 : TEXCOORD1;
            };

            vertexOut vert(vertexIn v)
            {
                vertexOut OUT;
                OUT.vertex = UnityObjectToClipPos(v.vertex);
                OUT.texcoord01.xy = v.texcoord;
                OUT.texcoord02.xy = v.texcoord1;
                return OUT;
            }

            // frag
            fixed4 frag(vertexOut IN) : SV_Target
            {
                fixed4 mainTexture = tex2D(_MainTex, IN.texcoord01.xy);
                fixed4 overlayTexture = tex2D(_OverlayTex, IN.texcoord02.xy);

                fixed4 returnTexture = mainTexture * _ColorMain;
                if(overlayTexture.a > 0 && mainTexture.a>0){
                    returnTexture *= (overlayTexture*_ColorOverlay);
                }
                returnTexture.a = mainTexture.a;
                return returnTexture;
            }

            ENDCG
        } 
    }
}

Disclaimer: I am very tired and writing this from the top of my head, sooooo, your mileage may vary.

fixed4 mainTexture = tex2D(_MainTex, IN.texcoord01.xy);
fixed4 overlayTexture = tex2D(_OverlayTex, IN.texcoord02.xy);
fixed4 returnTexture = mainTexture * _ColorMain;
overlayTexture.rgb *= _ColorOverlay.rgb;
returnTexture.rgb = overlayTexture.a * _ColorOverlay.a * (overlayTexture.rgb-returnTexture.rgb) + returnTexture.rgb;
returnTexture.a = mainTexture.a;
return returnTexture;

return fixed4(lerp(mainTexture * _ColorMain, overlayTexture * _ColorOverlay, overlayTexture.a * _ColorOverlay.a).rgb, mainTexture.a);

Thanks! This seems to work, I’m just trying to unpack what you did so that I can understand the code and add a third texture on top.

thanks for replying. the colours are right and the base mask, but the overlay colour alpha doesn’t work in this example.

If you add * _ColorOverlay.a right after overlayTexture.a, rsmeenk’s code will work just fine.

(because basically it’s the same thing with mine, only I kinda wrote what the lerp does without using a lerp, because I’m tired :stuck_out_tongue: )

Edited my previous post with mentioned comment. Little bit confusing to communicate on Twitter and the forum at the same time.:stuck_out_tongue:

Your basemap alpha acts as the final alpha used for blending with the rest of the scene. (defines the shape)
Your overlay alpha is used as the blendfactor for the overlay color. (color override)
You can stack up multiple lerps (linear interpolations) to build up the final color.

Hope that help.

Thanks both for your help.

So I wanted to add a third texture and tint, I’ve tried this (making the code longer so I can understand better)

            fixed4 frag(vertexOut IN) : SV_Target
            {
                // texture references
                fixed4 mainTexture = tex2D(_MainTex, IN.texcoord01.xy);
                fixed4 overlayTexture = tex2D(_OverlayTex, IN.texcoord02.xy);
                fixed4 overlayTextureTwo = tex2D(_OverlayTexTwo, IN.texcoord02.xy);

                fixed4 mainTextureTinted = mainTexture * _ColorMain; // main texture tinted
                fixed4 overlayTextureTinted = overlayTexture * _ColorOverlay; // overlay texture tinted
                fixed4 overlayTextureTintedTwo = overlayTextureTwo * _ColorOverlayTwo; // 2nd overlay

                fixed4 firstOverlay = mainTexture; // just for a return texture to store (also keeps the alpha from main)

                // one lerp to add first overlay to main texture
                firstOverlay.rgb = lerp(mainTextureTinted, overlayTextureTinted, overlayTexture.a * _ColorOverlay.a).rgb;

                // 2nd lerp to add 2nd overlay on top of above
                fixed4 returnTexture = firstOverlay;

                returnTexture.rgb = lerp(firstOverlay, overlayTextureTintedTwo, overlayTextureTwo.a * _ColorOverlayTwo.a).rgb;

                // set to main texture
                returnTexture.a = mainTexture.a;

                return returnTexture;

            }

Output…

Seems to work as expected.

Could the code be sped up / cleaner, or ok to leave it as above?

Should work ok on mobile?

Is there a limit to the number of textures you can combine like this?

Thanks!

There’s always a limit. :slight_smile: And it depends on the number of texture samplers that the hardware supports.
Sampling textures is a relatively costly operation so you want to minimize those. In your example you are using full four channel textures (rgb + alpha), but this could be reduced to a grayscale texture (since you really only are using the alpha for blend factor). If you combine four grayscale textures (for 4 different overlays) into a combined four channel texture you reduce the number of texture samples. This way you can reduce your samples by a factor of four, but note that this can only be used if you are using the same texture coordinate (which is the case with your two overlay textures). So you save a texture sample and still have room for two more for free! There is no difference in cost between sampling a four channel vs single channel texture.

Thanks, I’ll look into using grayscale for the two overlays.

The issue I’m trying to solve right now is uv mapping.

So the base texture is actually a sprite atlas, in my code I’m using the same uv on the overlays, but they are both 512x512 repeating images.

Whats the best way to keep my base texture using its own uv co-ords and then the overlays using their own?

Here is what my code looks like at the moment, trying to combine a base texture (on an atlas (tk2d not unity sprite, so basically just mesh with verts)) and a single repeating overlay

Shader "MyTest/twoTextureTest" 
{

    Properties 
    {
        _MainTex ("Base Texture", 2D) = "white" {}
        _OverlayTex ("Overlay Texture", 2D) = "white" {}

    }

    SubShader
    {
        Tags {
            "IgnoreProjector"="True"
            "Queue"="Transparent"
            "RenderType"="Transparent"
        }
        ZWrite Off
        Lighting Off
        Cull Off
        Fog { Mode Off }
        Blend SrcAlpha OneMinusSrcAlpha
        
        Pass 
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #pragma fragmentoption ARB_precision_hint_fastest
            #include "UnityCG.cginc"

            // variables
            sampler2D _MainTex;
            sampler2D _OverlayTex;
            float4 _OverlayTex_ST;

            // input
            struct vertexIn
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float2 uv1 : TEXCOORD1;
            };

            // output
            struct vertexOut
            {
                float4 vertex : SV_POSITION;
                float4 uv : TEXCOORD0;
                float4 uv1 : TEXCOORD1;
            };

            vertexOut vert(vertexIn v)
            {
                vertexOut OUT;
                OUT.vertex = UnityObjectToClipPos(v.vertex);
                OUT.uv.xy = v.uv;
                OUT.uv1.xy = TRANSFORM_TEX(v.uv1, _OverlayTex);
                return OUT;
            }

            // frag
            fixed4 frag(vertexOut IN) : SV_Target
            {
                
                fixed4 mainTexture = tex2D(_MainTex, IN.uv.xy);
                fixed4 overlayTexture = tex2D(_OverlayTex, IN.uv1.xy);

                fixed4 returnTexture = mainTexture;

                returnTexture.rgb = lerp(mainTexture, overlayTexture, overlayTexture.a).rgb;

                return returnTexture;

            }

            ENDCG
        } 
    }
}

I made this texture to use as the overlay to debug what is happening (black is transparent)

This is how it looks using the above shader on 3 sprites of different shape and size…

I find it really unpredictable to see how the overlay will work, the more sprites in the atlas makes it worse.

Any thoughts on using the same uv positions from the overlay on each mesh, or how to do it properly?

Many thanks indeed.

Not sure what tk2d does with second UV channel, but you should be able to use the 1st uv channel again with different tiling parameters.

What happens when you change

OUT.uv1.xy = TRANSFORM_TEX(v.uv1, _OverlayTex);

to

OUT.uv1.xy = TRANSFORM_TEX(v.uv, _OverlayTex);

(although it’s probably not what you want again, but at least this should be predictable)

For debugging I often output to color in a fragment shader:
return fixed4(IN.uv.xy, 0.0, 1.0);
This will create a red/green output that will tell you where 0 is (black) or 1 (full green or red).

But, for learning purposes it may be easier to start with
return fixed4(IN.uv.xxx, 1.0);

See what happens
and then

return fixed4(IN.uv.yyy, 1.0);

This will output the incoming uv coordinate values as grayscale color.

1 Like

output using these sprites:

this is the output from your suggestions @rsmeenk

return fixed4(IN.uv.xy, 0.0, 1.0);

return fixed4(IN.uv.xxx, 1.0);

return fixed4(IN.uv.yyy, 1.0);

this is how the atlas looks

basically i want to keep the UV of the base texture as you see from the atlas, but from the overlay it would repeat on its own co-ordinates, not taking texture position from the base and using it on the overlay, using the overlay own co-ords.

I made up a simple project with what I think you want, see attached.

I combined 4 overlay masks in one texture like this :
3427157--270651--overlays.png

And the shader code is this one :

Shader "Unlit/SpriteMat"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}

        [NoScaleOffset] _OverlayTex ("Overlays Texture", 2D) = "White"{} // Don't display the tiling/offset here, we'll add them in the next lines

        _OverlayST_0 ("Layer 1 Tiling Offset", Vector) = (1,1,0,0) // Express each tiling/offset as vector4 (tiling X & Y ; offset Z & W)
        _OverlayColor_0 ("Layer 1 Color", Color) = (1,0,0,1) // Overlay Layer color

        _OverlayST_1 ("Layer 2 Tiling Offset", Vector) = (1,1,0,0)
        _OverlayColor_1 ("Layer 2 Color", Color) = (0,1,0,1)

        _OverlayST_2 ("Layer 3 Tiling Offset", Vector) = (1,1,0,0)
        _OverlayColor_2 ("Layer 3 Color", Color) = (0,0,1,1)

        _OverlayST_3 ("Layer 3 Tiling Offset", Vector) = (1,1,0,0)
        _OverlayColor_3 ("Layer 3 Color", Color) = (0,1,1,1)

    }
    SubShader
    {
        Tags { "RenderType"="Transparent" }
        LOD 100

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
           
            #include "UnityCG.cginc"

            struct appdata // vertex shader input
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f // vertex to fragment data
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;

                float4 uvOverlay01 : TEXCOORD1; // uvs of layer 0 (xy) and 1 (zw) with tiling and offset applied
                float4 uvOverlay23 : TEXCOORD2; // uvs of layer 2 (xy) and 3 (zw) with tiling and offset applied
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            sampler2D _OverlayTex;
            float4 _OverlayST_0;
            float4 _OverlayColor_0;
            float4 _OverlayST_1;
            float4 _OverlayColor_1;
            float4 _OverlayST_2;
            float4 _OverlayColor_2;
            float4 _OverlayST_3;
            float4 _OverlayColor_3;
           
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                o.uvOverlay01 = float4(
                    v.uv * _OverlayST_0.xy + _OverlayST_0.zw,
                    v.uv * _OverlayST_1.xy + _OverlayST_1.zw
                );
                o.uvOverlay23 = float4(
                    v.uv * _OverlayST_2.xy + _OverlayST_2.zw,
                    v.uv * _OverlayST_3.xy + _OverlayST_3.zw
                );

                return o;
            }
           
            fixed4 frag (v2f i) : SV_Target
            {
                // sample the main texture, never modify the alpha later on
                fixed4 col = tex2D(_MainTex, i.uv);

                // Apply each layers:
                // ---- 0 ----
                fixed l0 = tex2D(_OverlayTex, i.uvOverlay01.xy).r; // Get the layer opacity from texture channel
                col.rgb = lerp( col.xyz, _OverlayColor_0.rgb, l0 * _OverlayColor_0.a ); // lerp with the previous color and take into account the layer color opacity
               
                // ---- 1 ----
                fixed l1 = tex2D(_OverlayTex, i.uvOverlay01.zw).g;
                col.rgb = lerp( col.xyz, _OverlayColor_1.rgb, l1 * _OverlayColor_1.a );
               
                // ---- 2 ----
                fixed l2 = tex2D(_OverlayTex, i.uvOverlay23.xy).b;
                col.rgb = lerp( col.xyz, _OverlayColor_2.rgb, l2 * _OverlayColor_2.a );
               
                // ---- 3 ----
                fixed l3 = tex2D(_OverlayTex, i.uvOverlay23.zw).a;
                col.rgb = lerp( col.xyz, _OverlayColor_3.rgb, l3 * _OverlayColor_3.a );

                return col;
            }
            ENDCG
        }
    }
}

If you have any questions on how I made it, feel free to ask.

3427157–270649–Sprite_Combiner.zip (71.6 KB)

HI @Remy_Unity thank you for taking the time to make this example!

The approach of making the overlay from one image and using the 4 channels to basically make 4 single overlays is a good one. However ultimately I wanted to use different textures with a tint on them, as one of the textures will have multiple colours in.

Also changed since I posted above, I found that changing the colours on a per sprite basis using MaterialPropertyBlock increases draw calls, so I just wanted to get the tiling working and then work on passing the colour tints hidden in the uv data, that way I could per sprite tint different colours and not use materialproperty block.

I can post up an example if you want, which version of Unity are you using? (I’m 2017)

Thanks!

I’m on so many Unity version that I can’t count them anymore XD.
If you want to have less batched with material property block, look at how implementing GPPU instances : Unity - Manual: GPU instancing
I’m quite curious on how you pass the tints in the uv data :slight_smile: