Michał Lorenc
Michał Lorenc

Reputation: 83

Multi layer texture shader in Unity

I'm really new to shaders. I searched on different forums for a similar problem but none of them helped me with the thing im struggling with.

Basically, I have to make a shader that will merge 6 textures into one. I have 6 layers. Every layer is a texture, and all of these layers must display on top of each other. Every layer texture is same size.

It's my code for now:

Shader "TileShaders/Chunk"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _SecondLayer ("Second Layer", 2D) = "white" {}
        _ThirdLayer ("Third Layer", 2D) = "white" {}
        _FourthLayer ("Fourth Layer", 2D) = "white" {}
        _FifthLayer ("Fifth Layer", 2D) = "white" {}
        _SixthLayer ("Sixth Layer", 2D) = "white" {}
        _Color ("Tint", 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

        Pass
        {
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ PIXELSNAP_ON
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
            };

            fixed4 _Color;

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                OUT.vertex = UnityObjectToClipPos(IN.vertex);
                OUT.texcoord = IN.texcoord;
                OUT.color = IN.color * _Color;
                #ifdef PIXELSNAP_ON
                OUT.vertex = UnityPixelSnap (OUT.vertex);
                #endif

                return OUT;
            }

            sampler2D _MainTex;
            sampler2D _AlphaTex;
            float _AlphaSplitEnabled;

            fixed4 SampleSpriteTexture (float2 uv)
            {
                fixed4 color = tex2D (_MainTex, uv);

                #if UNITY_TEXTURE_ALPHASPLIT_ALLOWED
                    if (_AlphaSplitEnabled)
                        color.a = tex2D (_AlphaTex, uv).r;
                #endif //UNITY_TEXTURE_ALPHASPLIT_ALLOWED

                return color;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color;
                c.rgb *= c.a;
                return c;
            }
        ENDCG
        }
    }
}

It's Sprites/Default unity shader and i just added these few variables, because i know i will need this data to display it as one texture:

_SecondLayer ("Second Layer", 2D) = "white" {}
_ThirdLayer ("Third Layer", 2D) = "white" {}
_FourthLayer ("Fourth Layer", 2D) = "white" {}
_FifthLayer ("Fifth Layer", 2D) = "white" {}
_SixthLayer ("Sixth Layer", 2D) = "white" {}

That's all i did. I really don't know how to start. It's first time im doing something with shaders

EDIT

Thank you for response! lerp() helped me a lot, without it all layers were visible at the same time what looked really weird.

Now everything looks great. But there is a small problem. As long as all layers contain texture it looks great. But in case only one layer has texture assigned and the rest dont, the whole result texture is white.

This is how the code looks now:

        fixed4 c = tex2D(_MainTex, IN.texcoord);
        fixed4 c2 = tex2D(_SecondLayer, IN.texcoord);

        fixed4 returnTexture = c;

        returnTexture.rgba = lerp(c, c2, c2.a).rgba;

        return returnTexture;

To avoid this problem i was thinking about if statements that will check if the texture on each layer exist, but i heard that i should avoid it anytime if that's possible. Is there a better way to handle it?

EDIT 2 I don't know if answer that finally helped me is still visiblue due to my latest answer to post deletation, but here is the solution from: trojanfoe

Rather than creating an answer to extend your question, please edit your original question. Don't use if statements; rather you might want to use shader features and use #if HAVE_TEXTURE_2 etc. This way unity will generate multiple versions of your shader, each one tailored to textures that are turned "on". Also you don't need the .rgba swizzles as that is the default.

EDIT 3

So i tried to display all of these layers on top of each other and i have some problem with lerp().

        fixed4 c1 = tex2D(_MainTex, IN.texcoord);
        fixed4 c2 = tex2D(_SecondLayer, IN.texcoord);
        fixed4 c3 = tex2D(_ThirdLayer, IN.texcoord);
        fixed4 c4 = tex2D(_FourthLayer, IN.texcoord);
        fixed4 c5 = tex2D(_FifthLayer, IN.texcoord);
        fixed4 c6 = tex2D(_SixthLayer, IN.texcoord);

        fixed4 returnTexture1 = c1;
        fixed4 returnTexture2 = c2;
        fixed4 returnTexture3 = c3;
        fixed4 returnTexture4 = c4;
        fixed4 returnTexture5 = c5;
        fixed4 returnTexture6 = c6;

        returnTexture1 = lerp(c1, c2, c2.a);
        returnTexture2 = lerp(c3, c4, c4.a);
        returnTexture3 = lerp(c5, c6, c6.a);

        fixed4 returnAlmostFinalTexture = lerp(returnTexture1, returnTexture2, returnTexture2.a);
        fixed4 returnFinalTexture = lerp(returnTexture2, returnTexture3, returnTexture3.a);

        return returnFinalTexture;

'returnAlmostFinalTexture' works as it should, but 'returnFinalTexture' breaks. For now i have 4 working layers, but i can't manage to have all 6 layers working. But im pretty sure if i had 8 layers in total it would do the work.

Can i lerp more than 2 textures easier way?

EDIT 4

Well, i got this to work and this is how it looks now:

        fixed4 c1 = tex2D(_MainTex, IN.texcoord);
        fixed4 c2 = tex2D(_SecondLayer, IN.texcoord);
        fixed4 c3 = tex2D(_ThirdLayer, IN.texcoord);
        fixed4 c4 = tex2D(_FourthLayer, IN.texcoord);
        fixed4 c5 = tex2D(_FifthLayer, IN.texcoord);
        fixed4 c6 = tex2D(_SixthLayer, IN.texcoord);

        fixed4 returnTexture1 = lerp(c1, c2, c2.a);
        fixed4 returnTexture2 = lerp(c3, c4, c4.a);
        fixed4 returnTexture3 = lerp(c5, c6, c6.a);

        fixed4 returnAlmostFinalTexture = lerp(returnTexture1, returnTexture2, returnTexture2.a);
        fixed4 returnFinalTexture = lerp(returnAlmostFinalTexture, returnTexture3, returnTexture3.a);

        return returnFinalTexture;

Is it the best way i could do that? Or i can somehow lerp all these textures easier/more optimized way?

Upvotes: 1

Views: 5029

Answers (1)

trojanfoe
trojanfoe

Reputation: 122458

You would normally multiply the colour values in order to merge them, so in the fragment shader you would do something like:

fixed4 c1 = tex2D(_MainTex, IN.texcoord);
fixed4 c2 = tex2D(_SecondLayer, IN.texcoord);
fixed4 c3 = tex2D(_ThirdLayer, IN.texcoord);
fixed4 c4 = tex2D(_FourthLayer, IN.texcoord);
fixed4 c5 = tex2D(_FifthLayer, IN.texcoord);
fixed4 c6 = tex2D(_SixthLayer, IN.texcoord);

return c1 * c2 * c3 * c4 * c5 * c6 * _Color;

If you wanted to configure how much each layer influenced the final colour you could use lerp() with a normalized input value for each texture.

EDIT: You have extended your question, mentioning that this doesn't work when textures are not assigned to the shader inputs. My response to this would be to use shader features where you can add toggles for each optional texture input and then use #if HAVE_TEXTURE_2 ... #endif. This way Unity will generate different shaders depending on what textures are turned "on" and this will be more efficient that using if (have_texture_2) etc.

Upvotes: 2

Related Questions