Custom Shader not working on Android

5.6k Views Asked by At

I ported a Plasma ball shader from Shadertoy to Unity as Image Effect which is attached to the camera. It works fine on the Editor and Windows standalone build. It does not work on Android devices. It is flashing blue and black images on Android.

Here is what it looks like in Unity Editor and Windows Build:

Here is what it looks like on Android:

The ported Shader code:

Shader "Hidden/Plasma Space Ball Image Effect"
{
    Properties
    {
        iChannel0("iChannel0", 2D) = "white" {}
    //[MaterialToggle] _isToggled("isToggle", Float) = 0
    }
        SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D iChannel0;

            //Ported from https://www.shadertoy.com/view/MstXzf

            float3 hb(float2 pos, float t, float time, float2 rot, float size, sampler2D tex0)
            {
                float2 newUv = 0.2*(pos / (1.2 - t) + 0.5*time*rot);
                //float texSample = texture(tex0, newUv).b;
                float texSample = tex2D(tex0, newUv).b;
                float uOff = 0.2*(texSample + 0.3*time);     //lsf3RH
                float2 starUV = newUv + float2(uOff, 0.0);
                //return float3(0.3, 0.3, 1.0) + 1.3*texture(tex0, starUV).b;
                return float3(0.3, 0.3, 1.0) + 1.3*tex2D(tex0, starUV).b;
            }

            float4 blob(float2 uv, float size, float time, sampler2D tex0)
            {
                float2 center = float2(0., 0.);

                float2 pos = center - uv;
                float t = length(pos);
                float st = size - t;

                float2 rot = 0.005*float2(sin(time / 16.), sin(time / 12.)); //MslGWN

                float alpha = smoothstep(0.0, 0.2*size, st);

                float3 col = hb(pos, t, time, rot, size, tex0);
                float a1 = smoothstep(-1.4, -1.0, -col.b);
                col = lerp(col, hb(pos, t, -time, -rot, size, tex0), a1);

                col += 0.8*exp(-12.*abs(t - 0.8*size) / size);
                float a2 = smoothstep(-1.4, -1.0, -col.b);

                alpha -= a2;

                //float crosshair = float((abs(pos.x) < 0.005 && abs(pos.y) < 0.15) || (abs(pos.y) < 0.005&&abs(pos.x) < 0.15));
                //return float4(col, alpha) + crosshair;

                return float4(col, alpha);
            }

            float4 main_(float2 uv, float size)
            {
                return blob(uv, size, _Time.y, iChannel0);
            }


            fixed4 frag(v2f i) : SV_Target
            {
                float4 fragColor = 0;
                float2 fragCoord = i.vertex.xy;

                ///---------------------------------------------------

                float2 uv = fragCoord.xy / _ScreenParams.xy;
                float2 cr = uv*2. - 1.;
                cr.x *= _ScreenParams.x / _ScreenParams.y;

                //late addition to elaborate background motion, could be reused later on
                float2 rot = 0.5*float2(sin(_Time.y / 16.), sin(_Time.y / 12.));

                float4 ball = clamp(main_(cr, sin(_Time.y)*0.05 + 0.5 + 0.5), 0., 1.);
                //float3 bg = float3(0.7, 0.7, 1.0)*texture(iChannel0, uv + rot + 0.1*ball.rb).b;
                float3 bg = float3(0.7, 0.7, 1.0)*tex2D(iChannel0, uv + rot + 0.1*ball.rb).b;

                //simulated gl blend
                fragColor = float4(lerp(bg, ball.rgb, ball.a), 1.0);
                //fragColor = lerp(fragColor,tex2D(iChannel0, i.uv).rgba,.5);
                return fragColor;
            }
            ENDCG
        }
    }
}

You can find the image that is used for the iChannel0 input slot here in the Shader above.

Things I've tried:

  • Adding the shader to the Graphics Settings so that Unity will include it in during build process.
  • Disabling Auto Graphics API and trying OpenGLES2 and OpenGLES3.
  • Checking the log with Android Studio. No error/warning at-all.

None of these solved the problem and I ran out of things to try.

Software and Device Info if that helps:

  • Unity 5.6.0f3
  • Android 4.4.2

This is used for learning and educational purposes as I am studying GLSL, HLSL, CG/shaderlab shader language. I just want to know why the ported shader is not working as expected on Android devices.

Why is it flashing blue and black images on Android?

1

There are 1 best solutions below

1
On BEST ANSWER

You need to use the VPOS semantic for positions in the fragment shader for OpenGLES2.
From Unity docs:

A fragment shader can receive position of the pixel being rendered as a special VPOS semantic. This feature only exists starting with shader model 3.0, so the shader needs to have the #pragma target 3.0 compilation directive.

So to get screen space positions:

// note: no SV_POSITION in this struct
struct v2f {
    float2 uv : TEXCOORD0;
};

v2f vert (
    float4 vertex : POSITION, // vertex position input
    float2 uv : TEXCOORD0, // texture coordinate input
    out float4 outpos : SV_POSITION // clip space position output
    )
{
    v2f o;
    o.uv = uv;
    outpos = UnityObjectToClipPos(vertex);
    return o;
}


fixed4 frag (v2f i, UNITY_VPOS_TYPE screenPos : VPOS) : SV_Target
{
    // screenPos.xy will contain pixel integer coordinates.
    float4 fragColor = 0;
    float2 fragCoord = screenPos;

But you already pass in uvs so maybe you can use those?

float2 uv = i.uv;

It turns out I was wrong. You dont get clip space positions in the fragment shader in OpenGLES2 you get .. 0. (Maybe someone can explain this?)

I made a small test shader:

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

float4 vert (float4 vertex : POSITION) : SV_Position
{                   
    return UnityObjectToClipPos(vertex);                
}

fixed4 frag (float4 screenPos : SV_Position) : SV_Target
{   
    float uvx = screenPos.x/_ScreenParams.x;
    return float4(uvx, 0., 0., 1.);
}
ENDCG

and the line float uvx = screenPos.x/_ScreenParams.x; gets compiled as
tmpvar_2.x = (0.0 / _ScreenParams.x); // OpenGLES2
u_xlat0 = gl_FragCoord.x / _ScreenParams.x; // OpenGLES3

But if you use the VPOS semantic
fixed4 frag (float4 screenPos : VPOS) : SV_Target the same line gets compiled as
tmpvar_2.x = (gl_FragCoord.x / _ScreenParams.x); // OpenGLES2
u_xlat0 = gl_FragCoord.x / _ScreenParams.x; // OpenGLES3

So for OpenGLES2 it looks like you need to use the VPOS semantic to get positions in screen space in the fragment shader.