Unity3D Shader系列之UI流光效果

Unity3D Shader系列之UI流光效果

2020年11月28日 1 作者 老王

1. 引言

周末用两种方式实现了UI流光的效果。
第一种是使用流光贴图+遮罩(Mask)贴图的方式。
mask贴图实现流光
第二种是不需要任何贴图,纯用代码实现的流光效果。
纯计算实现方式
第一种方式的优点在于可以用遮罩贴图控制流光显示的区域,以及用贴图控制流光任意的形状(比如上面的效果就是我自己随便画的一个弯曲的形状)。
第二种方式的优点是不需要多次对贴图采样(但是会加大计算量,和第一种方式比起来哪种效率更高还真不好说,我也不知道怎么去评测)。
流光的基本原理就是uv流动。下面看看两种方式的实现。

2. 流光纹理+遮罩纹理

流光纹理和遮罩纹理一般都是一张黑白图,比如Demo中用到的两个纹理分别如下。
流光纹理:
流光纹理
遮罩纹理:
遮罩纹理
流光纹理用于控制流光的形状(白色区域),遮罩区域用于控制流光显示的区域(白色区域)。
核心代码就下面几句,一看就明白,不多说了。

fixed2 uv = IN.texcoord.xy;
uv.x -= _FlowSpeed * _Time.y;
fixed4 flow = _FlowColor * tex2D(_FlowTex, uv) * tex2D(_FlowMask, IN.texcoord).r;
color += flow;

完整shader

Shader "Custom/UI/Flow"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _FlowTex("Flow Tex", 2D) = "white" {}
        _FlowMask("Flow Mask", 2D) = "white" {}
        _FlowColor("Flow Color", Color) = (1, 1, 1, 1)
        _FlowSpeed("Flow Speed", Range(0, 3)) = 1

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            Name "Default"
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            #pragma multi_compile_local _ UNITY_UI_CLIP_RECT
            #pragma multi_compile_local _ UNITY_UI_ALPHACLIP

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

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

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;

            sampler2D _FlowTex;
            float4 _FlowTex_ST;
            sampler2D _FlowMask;
            fixed4 _FlowColor;
            half _FlowSpeed;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.worldPosition = v.vertex;
                OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

                OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                OUT.color = v.color * _Color;
                return OUT;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

                #ifdef UNITY_UI_CLIP_RECT
                color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif

                fixed2 uv = IN.texcoord.xy;
                uv.x -= _FlowSpeed * _Time.y;
                fixed4 flow = _FlowColor * tex2D(_FlowTex, uv) * tex2D(_FlowMask, IN.texcoord).r;
                color += flow;

                return color;
            }
        ENDCG
        }
    }
}

3. 纯计算方式

这里参考了风宇冲的实现方式。
但是他的代码有点问题,当looptime调节过大时,会导致流光不能完整流动。

tmpBrightness = inFlash(75,i.uv,0.25f,5f,2f,0.15,3f);

风宇冲效果
所以我改了一下,核心代码如下,能够精确控制流光流动的时间。

// 角度, uv, 流光宽度(0~1), 两次流光开始的间隔时间, 流光流动流动一个完整图片的时间
fixed inFlow(float angle, float2 uv, fixed width, int interval, float duration)
{
    float rad = angle * 0.0174444;
    float tanRad = tan(rad);

    float maxYProj2X = 1.0 / tanRad;
    float totalMovX = 1 + width + maxYProj2X;

    float totalTime = interval + duration;
    int cnt = _Time.y / totalTime;
    float currentTime = _Time.y - cnt * totalTime;

    fixed flow = 0;
    if(currentTime < duration)
    {
        fixed x0 = currentTime / (duration / totalMovX);
        float yProj2X = uv.y / tanRad;
        float xLeft = x0 - width - yProj2X;
        float xRight = xLeft + width;
        float xMid = 0.5 * (xLeft + xRight);
        flow = step(xLeft, uv.x) * step(uv.x, xRight);
        // 插值,根据与中心的距离的比例来计算亮度
        flow *= (width - 2 * abs(uv.x - xMid)) / width;
    }
    return flow;
}

代码解释:
求解图示
如图,绿色部分为我们要增加流光的图片。最左侧的平行四边形为流光的起始位置,最右侧的平行四边形为流光的结束位置。
一次完整的流光过程为,从起始位置到结束位置(即从O点到E点)。
duration为从O点到E点的时间,单位为秒。
internal = duration + 下次流光开始的间隔时间。
其他地方就没什么可说的了,图示+代码应该就明白了。
完整代码

Shader "Custom/UI/Flow2"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _Angle ("Angle", Range(1, 89)) = 75                 // 倾斜角度
        _Width ("Width", Range(0.1, 1)) = 0.25              // 流光宽度
        _Interval ("Interval", Int) = 3                     // 间隔
        _Duration ("duration", Float) = 1.5                 // 持续时间
        _FlowColor("Flow Color", Color) = (1, 1, 1, 1)      // 流光颜色

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            Name "Default"
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            #pragma multi_compile_local _ UNITY_UI_CLIP_RECT
            #pragma multi_compile_local _ UNITY_UI_ALPHACLIP

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

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

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;

            float _Angle;
            fixed _Width;
            int _Interval;
            float _Duration;
            fixed4 _FlowColor;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.worldPosition = v.vertex;
                OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

                OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                OUT.color = v.color * _Color;
                return OUT;
            }

            // 风宇冲的实现: http://blog.sina.com.cn/s/blog_471132920101d8zf.html
            //必须放在使用其的 frag函数之前,否则无法识别。
            //核心:计算函数,角度,uv,光带的x长度,间隔,开始时间,偏移,单次循环时间
            float inFlash(float angle,float2 uv,float xLength,int interval,int beginTime, float offX, float loopTime )
            {
                //亮度值
                float brightness =0;

                //倾斜角
                float angleInRad = 0.0174444 * angle;

                //当前时间
                float currentTime = _Time.y;

                //获取本次光照的起始时间
                int currentTimeInt = _Time.y / interval;
                currentTimeInt *= interval;

                //获取本次光照的流逝时间 = 当前时间 - 起始时间
                float currentTimePassed = currentTime -currentTimeInt;
                if(currentTimePassed > beginTime)
                {
                    //底部左边界和右边界
                    float xBottomLeftBound;
                    float xBottomRightBound;

                    //此点边界
                    float xPointLeftBound;
                    float xPointRightBound;

                    float x0 = currentTimePassed-beginTime;
                    x0 /= loopTime;

                    //设置右边界
                    xBottomRightBound = x0;

                    //设置左边界
                    xBottomLeftBound = x0 - xLength;

                    //投影至x的长度 = y/ tan(angle)
                    float xProjL;
                    xProjL= (uv.y)/tan(angleInRad);

                    //此点的左边界 = 底部左边界 - 投影至x的长度
                    xPointLeftBound = xBottomLeftBound - xProjL;
                    //此点的右边界 = 底部右边界 - 投影至x的长度
                    xPointRightBound = xBottomRightBound - xProjL;

                    //边界加上一个偏移
                    xPointLeftBound += offX;
                    xPointRightBound += offX;

                    //如果该点在区域内
                    if(uv.x > xPointLeftBound && uv.x < xPointRightBound)
                    {
                        //得到发光区域的中心点
                        float midness = (xPointLeftBound + xPointRightBound)/2;

                        //趋近中心点的程度,0表示位于边缘,1表示位于中心点
                        float rate= (xLength -2*abs(uv.x - midness))/ (xLength);
                        brightness = rate;
                    }
                }
                brightness= max(brightness,0);

                //返回颜色 = 纯白色 * 亮度
                float4 col = float4(1,1,1,1) *brightness;
                return brightness;
            }

            // 角度, uv, 流光宽度(0~1), 两次流光开始的间隔时间, 流光流动流动一个完整图片的时间
            fixed inFlow(float angle, float2 uv, fixed width, int interval, float duration)
            {
                float rad = angle * 0.0174444;
                float tanRad = tan(rad);

                float maxYProj2X = 1.0 / tanRad;
                float totalMovX = 1 + width + maxYProj2X;

                float totalTime = interval + duration;
                int cnt = _Time.y / totalTime;
                float currentTime = _Time.y - cnt * totalTime;

                fixed flow = 0;
                if(currentTime < duration)
                {
                    fixed x0 = currentTime / (duration / totalMovX);
                    float yProj2X = uv.y / tanRad;
                    float xLeft = x0 - width - yProj2X;
                    float xRight = xLeft + width;
                    float xMid = 0.5 * (xLeft + xRight);
                    flow = step(xLeft, uv.x) * step(uv.x, xRight);
                    // 插值,根据与中心的距离的比例来计算亮度
                    flow *= (width - 2 * abs(uv.x - xMid)) / width;
                }
                return flow;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

                #ifdef UNITY_UI_CLIP_RECT
                color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif

                 //传进i.uv等参数,得到亮度值
                //float flow = inFlash(30, IN.texcoord, 0.5, 5/*interval*/, 2/*beginTime*/, 0/*xOffset*/, 2/*loopTime*/);

                fixed flow = inFlow(_Angle, IN.texcoord, _Width, _Interval, _Duration);
                color += _FlowColor * flow * step(0.5, color.a);

                return color;
            }
        ENDCG
        }
    }
}

项目链接:
链接:https://pan.baidu.com/s/1uVnAnEyQoZH8QURbxSnTfw
提取码:9zin
(备注:项目部分素材来源与unity shader 流光(1))

4. 参考文章