Any idea why this shader doesn't work on some android devices?

I made a relatively simple shader, it works fine on my pc and on my android phone,
but on some phones it looks incorrect (and for some reason it doesn’t use it’s fallback).

And what’s even stranger is the fact that I made an other shader that is exactly the same, but slightly more complex, and that one works on all devices.

Does anyone have any idea why?

here’s the shader:

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unlit/Snake"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Solid ("Solid (R,L,U,D)", Vector) = (0,0,0,0)
        _Offset ("Shadow Offset", Vector) = (0,0,0,0)
        //_Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="TransparentCutout" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0
          
            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
          
            float4 _Solid;
            float3 _Offset;
          
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
          
            fixed4 frag (v2f i) : SV_Target
            {
          
                float3 offset = mul( (float3x3)unity_WorldToObject, _Offset) ;

                fixed4 col = tex2D(_MainTex, i.uv);
                clip(col.a-0.5);
              
                fixed4 shadowcolor = col;
                shadowcolor.rgb *= 0.85;

                i.uv += offset;
                if (i.uv.x>1 && _Solid.x<0.5)
                {
                    return shadowcolor;
                }
                if (i.uv.x<0 && _Solid.y<0.5)
                {
                    return shadowcolor;
                }
                if (i.uv.y>1 && _Solid.z<0.5)
                {
                    return shadowcolor;
                }
                if (i.uv.y<0 && _Solid.w<0.5)
                {
                    return shadowcolor;
                }

                if ((i.uv.x>1 || i.uv.x<0) && (i.uv.y>1 || i.uv.y<0))
                {
                    return shadowcolor;
                }
              
                float2 uv2 = i.uv - offset*0.5;
                if (tex2D(_MainTex, i.uv).a<0.5 || tex2D(_MainTex, uv2).a<0.5)
                {
                    return shadowcolor;
                }

                return col;
            }
            ENDCG
        }
    }
    Fallback "Unlit/Transparent Cutout"
}

and here’s the more complex shader that does work:

// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Unlit/SnakeHead"
{
    Properties
    {
        _MainTex ("Texture1", 2D) = "white" {}
        _MainTex2 ("Texture2", 2D) = "white" {}
        _Solid ("Solid (R,L,U,D)", Vector) = (0,0,0,0)
        _Offset ("Shadow Offset", Vector) = (0,0,0,0)
        _State1Time ("State 1 Time", float) = 2
        _State2Time ("State 2 Time", float) = 0.1
    }
    SubShader
    {
        Tags { "RenderType"="TransparentCutout" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0
          
            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _MainTex2;
          
            float4 _Solid;
            float3 _Offset;
          
            float _State1Time;
            float _State2Time;
          
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
          
            fixed4 frag (v2f i) : SV_Target
            {
          
                float3 offset = mul( (float3x3)unity_WorldToObject, _Offset) ;

                fixed4 col = tex2D(_MainTex, i.uv);
                if ((_Time.y)%(_State1Time+_State2Time)>_State1Time)
                {
                    col = tex2D(_MainTex2, i.uv);
                }
                clip(col.a-0.5);
              
                fixed4 shadowcolor = col;
                shadowcolor.rgb *= 0.85;

                i.uv += offset;
                if (i.uv.x>1 && _Solid.x<0.5)
                {
                    return shadowcolor;
                }
                if (i.uv.x<0 && _Solid.y<0.5)
                {
                    return shadowcolor;
                }
                if (i.uv.y>1 && _Solid.z<0.5)
                {
                    return shadowcolor;
                }
                if (i.uv.y<0 && _Solid.w<0.5)
                {
                    return shadowcolor;
                }

                if ((i.uv.x>1 || i.uv.x<0) && (i.uv.y>1 || i.uv.y<0))
                {
                    return shadowcolor;
                }
              
                float2 uv2 = i.uv - offset*0.5;

                if (tex2D(_MainTex, i.uv).a<0.5 || tex2D(_MainTex, uv2).a<0.5)
                {
                    return shadowcolor;
                }

                return col;
            }
            ENDCG
        }
    }
    Fallback "Unlit/Snake"
}

No one?

On which devices it doesn’t work? And what does it mean “it doesn’t work”? Pink color?

Usually Unity does some good job on printing actual error messages on shader compilation, try getting an adb log from the device on app startup or when the shader gets compiled, it will help you to understand which part of the shader may not work on certain devices

Oh right, I didn’t really give much info.

here’s how the shader is supposed to look:

this is how it looks on some devices:

As you can see, the head is still correct, this uses the more complex shader,
the tail-body uses the simpler shader and is somehow incorrect.

I don’t know their devices, I’ll ask it.

You can also drop an .apk file here so I’ll be able to take a loot at the shaders in native profilers. Right now I don’t really like lots of per-fragment ifs in shaders, maybe some devices don’t like them too

Your shader is crazy for what you’re trying to produce … just do two samples to fake that shadow.

Some Android devices are really broken when it comes to using if statements in shaders. It’s best practices in shader writing to avoid them at almost all costs.

1 Like

That’s what it does, the if statements are just to determine whether the 4 borders of the image should produce a shadow or not

yeah, I know, I already rewrote it as an ?: statement (I’ve heard that that’s better, but I dunno if it really is?)
I also combined it into a single ?: statement, so that should be a bit better at least I think.
I’ve yet to try these changes on the devices it didn’t work though.

You don’t need any branches. At all.

For desktop if vs ? has a difference. On mobile it may not. If it’s an OpenGLES 2.0 device there is no difference. If it’s an OpenGLES 3.0 device it should be different, but still might not be because the device in question may have buggy or poorly written shader support. The state of Android is such that two devices running the same version of Android, with the same processor and GPU, may run a shader completely differently. That’s just the way things are on Android.

However things like mixing tex2D() and if statements muddies things a bit and will usually make if and ? act the same even on desktop. For reasons I won’t go into here, those tex2D() calls always have to be run for the shader to work. The code on the device should know this and change something like:

if( tex2D(_Tex1, uv).a > 0.5 && tex2D(_Tex1, uv2).a) {
col = tex2D(_Tex2, uv);
}

into:

fixed temp1 = tex2D(_Tex1, uv).a;
fixed temp2 = tex2D(_Tex1, uv2).a;
fixed4 temp3 = tex2D(_Tex2, uv);
col = (temp1 > 0.5 && temp2 > 0.5) ? temp3 : col;

However that might not happen properly on all devices, and when it doesn’t it’ll cause catastrophes. So one thing that may help, though no guarantee, is don’t put the tex2D() calls inside or after the if() statements. Do all of the tex2D() calls before your first condition and store the data in temporary variables like the second example above.

This may feel dirty from an optimization stand point if you’re used to programming with C# or pretty much any other CPU side programming language, but shaders are not like normal programming languages and the requirements are very different for what is an optimization or not. And, as stated above, that “de-optimization” is what should be happening to your shader automatically for it to work, but it’s possible it isn’t causing the issue you’re seeing.

Or it could be a different bug.

2 Likes

Also this.

if ( value > 0.5) {
col = valueA;
}
else
{
col = valueB;
}

could (and should) be replaced with something like this:

col = lerp(valueB, valueA, step(value, 0.5));

or potentially:

col = lerp(valueB, valueA, saturate((value - 0.5) * 1000));

3 Likes

Thanks for the extensive help, I really appreciate all the info.

I made a new build sunday, with the changes I mentioned in my previous post,
and tested it on one of the devices the shader acted weird, and it works now!

I guess it was the many if-statements causing the issue, or the fact that I used tex2D() in an if-statement. (like you said)

I’ll take your advice and change the ?-statement into something like you wrote to furter improve the shader, because who knows, it might still not work on some other devices in its current state.

Thanks everyone for the help, I really appreciate it all.