Unity3D - Shader for sprite clipping

Khaled Abu AlKheir picture Khaled Abu AlKheir · Apr 19, 2014 · Viewed 19.6k times · Source

I am trying to create a shader that can be used to clip 2D sprites in a game, I found this shader in another question

Shader "Sprites/ClipArea"
{
Properties
{
    _MainTex ("Base (RGB), Alpha (A)", 2D) = "white" {}
    _Length ("Length", Range(0.0, 1.0)) = 1.0
    _Width ("Width", Range(0.0, 1.0)) = 0.5
 }

SubShader
{
    LOD 200

    Tags
    {
        "Queue" = "Transparent"
        "IgnoreProjector" = "True"
        "RenderType" = "Transparent"
    }

    Pass
    {
        Cull Off
        Lighting Off
        ZWrite Off
        Offset -1, -1
        Fog { Mode Off }
        ColorMask RGB
        Blend SrcAlpha OneMinusSrcAlpha

        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag

        #include "UnityCG.cginc"

        sampler2D _MainTex;
        float4 _MainTex_ST;
        float _Length;
        float _Width;

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

        struct v2f
        {
            float4 vertex : POSITION;
            float2 texcoord : TEXCOORD0;
        };

        v2f vert (appdata_t v)
        {
            v2f o;
            o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
            o.texcoord = v.texcoord;
            return o;
        }

        half4 frag (v2f IN) : COLOR
        {
            if ((IN.texcoord.x<0) || (IN.texcoord.x>_Width) || (IN.texcoord.y<0) || (IN.texcoord.y>_Length))

            {
                half4 colorTransparent = half4(0,0,0,0) ;
                return  colorTransparent ;
            }
            return tex2D(_MainTex, IN.texcoord);
        }
        ENDCG
    }
}
}

which works perfectly on single sprites, but I am using sprite sheets, divided by Unity Sprite editor.

The _Width variable in the shader is covering the whole sprite sheet, not the sprite I am working on. I searched for a way to get the current sprite rect inside the shader but couldnt find anything.

Answer

Khaled Abu AlKheir picture Khaled Abu AlKheir · Apr 22, 2014

Well, after pulling my hair for three days, I managed to find a workaround. After searching more about how shaders work and there role in the pipeline, I realized that the sprite rect info will probably be not available in the shader for one simple reason, the functionality of almost all shaders (except mine) does not require this info, because the job of the shader is to take a vertex, modify its position (if needed) through the vertex function and then decide its pixel colour through the fragment function, it does not care about the whole sprite, it only needs to lookup the pixel colour for a certain vertex from the texture using its texture coordinates. I am sure this is trivial info for people working in shaders, but it took me time to realize it ( This was my first ever shader). So as a workaround I had to use the shader properties to pass the MinX and MaxX of the current sprite the shader is working on in the sprite sheet, so the shader now looks like this:

Shader "Sprites/ClipArea"
{
    Properties
    {
        _MainTex ("Base (RGB), Alpha (A)", 2D) = "white" {}
        _Fill ("Fill", Range(0.0, 1.0)) = 1.0
        _MinX ("MinX", Float) = 0
        _MaxX ("MaxX", Float) = 1
     }

    SubShader
    {
        LOD 200

        Tags
        {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
        }

        Pass
        {
            Cull Off 
            Lighting Off
            ZWrite Off
            Offset -1, -1
            Fog { Mode Off }
            ColorMask RGB
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _MinX;
            float _MaxX;
            float _Fill;

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

            struct v2f
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };

            v2f vert (appdata_t v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.texcoord = v.texcoord;
                return o;
            }

            half4 frag (v2f IN) : COLOR 
            {
            if ((IN.texcoord.x<_MinX)|| (IN.texcoord.x>(_MinX+_Fill*(_MaxX-_MinX))))
                {
                    half4 colorTransparent = half4(0,0,0,0) ;
                    return  colorTransparent ;
                }
                return tex2D(_MainTex, IN.texcoord);
            }
            ENDCG
        }
    }
} 

To use this shader, you need to create a material that uses it, then use that material in the SpriteRenderer, you can change the Fill amount, MinX , and MaxX from the inspector, or call the spriteRenderer.material.setFloat(property, value) from code.

I then faced another issue with animated sprites, I had to keep updating the MinX and MaxX on every frame, but when I did it in the Update function the animation started flickering, and thats because the update was being called after the sprite is rendered, so I had to use the Main Camera OnPreRender event to update the material properties.

Maybe there is a better way to achieve all this but this is the best I could come up with, and I hope this will benefit someone trying to achieve the same effect.