Unity FTLドライブシェーダー

Unity uGUI用、FTLドライブシェーダー。SFのワープ演出。
(FTL Drive Shader for Unity uGUI)

関連ページ 参考URL
集中線シェーダー を作っていく過程で、SFでよくあるワープ演出も作れるなと思い始めた。
前に作った 逆魚眼レンズシェーダー と組み合わせて、演出的には満足いくものが出来上がった。

ただ過去に自分が作ったシェーダーの中では、もっとも複雑でもっとも汚いコードになっている。処理も重い可能性がある。
もはや修正する気力もないので、それでも良ければどうぞ。

FTLドライブシェーダーの概要

FTLはFaster Than Lightの略で光速を超える移動のこと。昨今のゲーム業界では好んで使われる言葉。
今回作ったシェーダーはこのFTLを表現するもので、SFとかでよくあるワープ演出に使える。

下はFTLドライブシェーダーを実装したサンプル動画。

このシェーダーは GrabPass を実装の一部に使っている。
なので基本的には、UI Canvasの一番下に画面全体を覆うようにImageを用意し、そこにマテリアルをアタッチして使う。
FTLドライブシェーダーのHierarchyの位置

FTLドライブシェーダー、コード全文

今回のシェーダーは5つのPassを使った重厚長大なもので、プロパティの数は15に及ぶ。
以下コード全文。uGUI Imageへの反映方法は こちらの記事 参照。
Shader "UI/FTLDrive" 
{
    Properties 
    {
        [HideInInspector]_MainTex("-",2D)="white"{} 
        [HideInInspector] _EasePow ("EasePow", Range(0, 10)) = 10

        _Transition("Transition", Range(0, 1)) = 0

        [Space(10)]
        _Line1Color("Line1Color", Color) = (1,1,1,1)
        _Line2Color("Line2Color", Color) = (0,0,0,1)
        _FadeOutColor("FadeOutColor", Color) = (1,1,1,1) 
        _RippleColor("RippleColor", Color) = (0,0,0,1)

        [Space(10)]
        _CompDistortTiming("CompDistortTiming", Range(0, 1)) = 0.8
        _StartScaleInTiming("StartScaleInTiming", Range(0, 1)) = 0.5
        _CompLine1Timing("CompLine1Timing", Range(0, 1)) = 0.7
        _StartFadeOutTiming("StartFadeOutTiming", Range(0, 1)) = 0.8
        _CompFadeOutTiming("CompFadeOutTiming", Range(0, 1)) = 0.9
        _RippleTiming("RippleTiming", Range(0, 1)) = 0.7
        _StartLine2Timing("StartLine2Timing", Range(0, 1)) = 0.7

        [Space(10)]
        _Line1NoiseScale("Line1NoiseScale", Range(1, 500)) = 100
        _Line2NoiseScale("Line2NoiseScale", Range(1, 500)) = 100
        _RippleLength("RippleLength", Range(0, 5)) = 2
    }

    SubShader
    {
        Tags 
        {
            "Queue" = "Transparent"
            "RenderType"="Transparent"
        }
        Cull Off
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        CGINCLUDE
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"

        sampler2D _MainTex;
        sampler2D _GrabTexture;
        fixed _EasePow;

        fixed _Transition;
        
        fixed4 _Line1Color;
        fixed4 _Line2Color;
        fixed4 _FadeOutColor;
        fixed4 _RippleColor;

        fixed _CompDistortTiming;
        fixed _StartScaleInTiming;
        fixed _CompLine1Timing;
        fixed _StartFadeOutTiming;
        fixed _CompFadeOutTiming;
        fixed _RippleTiming;
        fixed _StartLine2Timing;

        fixed _Line1NoiseScale;
        fixed _Line2NoiseScale;
        fixed _RippleLength;     

        
//radianの最大値、degreeで言うと360度のこと

        static const float PI2 = 3.14159 * 2;

        struct appdata
        {
            fixed4 vertex : POSITION;
            fixed4 uv : TEXCOORD0;
        };

        struct v2f
        {
            fixed4 vertex : SV_POSITION;
            fixed2 uv : TEXCOORD0;
        };

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

        
//引数に渡された対象座標の角度を0~1の範囲に圧縮して返す。

        
//反時計周りに徐々に値が大きくなる。第二Passと第五Passで使用する。

        fixed2 getUvAngle(fixed2 uv)
        {
            
//uv座標を、画面中央を原点として(0,0)>(1,1)から(-1,-1)>(1,1)の範囲に修正する

            fixed2 fixUv = uv * 2 - 1;
            
//修正したuv座標の角度をradian値で取得

            fixed angle = atan2(fixUv.y, fixUv.x);
            
//0~PI*2の値を0~1の範囲に圧縮

            return angle / PI2;
        }

        
//Unity公式が用意してる4つのノイズ関数。

        //https://docs.unity3d.com/ja/Packages/com.unity.shadergraph@10.0/manual/Simple-Noise-Node.html
        inline float unity_noise_randomValue (float2 uv)
        {
            return frac(sin(dot(uv, float2(12.9898, 78.233)))*43758.5453);
        }
        inline float unity_noise_interpolate (float a, float b, float t)
        {
            return (1.0-t)*a + (t*b);
        }
        inline float unity_valueNoise (float2 uv)
        {
            float2 i = floor(uv);
            float2 f = frac(uv);
            f = f * f * (3.0 - 2.0 * f);
        
            uv = abs(frac(uv) - 0.5);
            float2 c0 = i + float2(0.0, 0.0);
            float2 c1 = i + float2(1.0, 0.0);
            float2 c2 = i + float2(0.0, 1.0);
            float2 c3 = i + float2(1.0, 1.0);
            float r0 = unity_noise_randomValue(c0);
            float r1 = unity_noise_randomValue(c1);
            float r2 = unity_noise_randomValue(c2);
            float r3 = unity_noise_randomValue(c3);
        
            float bottomOfGrid = unity_noise_interpolate(r0, r1, f.x);
            float topOfGrid = unity_noise_interpolate(r2, r3, f.x);
            float t = unity_noise_interpolate(bottomOfGrid, topOfGrid, f.y);
            return t;
        }
        void Unity_SimpleNoise_float(float2 UV, float Scale, out float Out)
        {
            float t = 0.0;
        
            float freq = pow(2.0, float(0));
            float amp = pow(0.5, float(3-0));
            t += unity_valueNoise(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;
        
            freq = pow(2.0, float(1));
            amp = pow(0.5, float(3-1));
            t += unity_valueNoise(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;
        
            freq = pow(2.0, float(2));
            amp = pow(0.5, float(3-2));
            t += unity_valueNoise(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;
        
            Out = t;
        }

        ENDCG

        
//第一Passの前に画面全体をキャプチャーする。

        GrabPass{}

        
//第一Pass、画面全体を糸巻歪曲収差で歪ませて、Trasnsitionの後半ではズームアップする

        Pass
        {
            CGPROGRAM

            fixed2 getIsUnderHalf(v2f i)
            {
                
//x軸が0.5より小さければ1を代入

                fixed isUnderHalfX = step(i.uv.x, 0.5);
                
//y軸が0.5より小さければ1を代入

                fixed isUnderHalfY = step(i.uv.y, 0.5);

                return fixed2(isUnderHalfX, isUnderHalfY);
            }
            
            fixed2 getFarFromSafe(v2f i)
            {
                fixed fixRange = lerp(0, 1, saturate(_Transition / _CompDistortTiming + 0.0001));
                fixed safeRange = 1 - fixRange;

                fixed farFromSafeX = abs(0.5 - i.uv.x) - (safeRange / 2);
                fixed farFromSafeY = abs(0.5 - i.uv.y) - (safeRange / 2);
                return fixed2(farFromSafeX, farFromSafeY);
            }

            fixed2 getFarFromCenter(v2f i)
            {
                fixed farFromCenterX = abs(0.5 - i.uv.x);
                fixed farFromCenterY = abs(0.5 - i.uv.y);
                return fixed2(farFromCenterX, farFromCenterY);
            }

            fixed2 getMainDist(fixed2 farFromSafe)
            {
                
//InQuadのEaseでfarFromSafeが大きい程指数関数的に座標がずれるようにする

                fixed quadX = farFromSafe.x * (farFromSafe.x - 2);
                fixed quadY = farFromSafe.y * (farFromSafe.y - 2);

                
//指数関数の伸びが強すぎるのでfixEaseで抑える

                
//このシェーダーではHideInspectorで隠され_EasePowの値は10で固定なので、実質fixEaseは1.9になる。

                fixed fixEase = 11.9 - _EasePow;
                fixed mainDistX = quadX * quadX / fixEase;
                fixed mainDistY = quadY * quadY / fixEase;

                return fixed2(mainDistX, mainDistY);
            }

            fixed2 getSubDist(fixed2 farFromCenter, fixed2 mainDist)
            {
                fixed subDistX = farFromCenter.x * mainDist.y;
                fixed subDistY = farFromCenter.y * mainDist.x;
                return fixed2(subDistX, subDistY);
            }

            fixed2 getMainReduce(fixed2 isUnderHalf, fixed2 mainDist)
            {
                fixed mainReduceX = (isUnderHalf.x * mainDist.x) + ((1 - isUnderHalf.x) * -1 * (mainDist.x));
                fixed mainReduceY = (isUnderHalf.y * mainDist.y) + ((1 - isUnderHalf.y) * -1 * (mainDist.y));
                return fixed2(mainReduceX, mainReduceY);
            }

            fixed2 getSubReduce(fixed2 isUnderHalf, fixed2 subDist)
            {
                fixed subReduceX = (isUnderHalf.x * subDist.x) + ((1 - isUnderHalf.x) * -1 * (subDist.x));
                fixed subReduceY = (isUnderHalf.y * subDist.y) + ((1 - isUnderHalf.y) * -1 * (subDist.y));
                return fixed2(subReduceX, subReduceY);
            }

            fixed2 getUseDist(fixed2 farFromSafe)
            {
                
//x軸が歪み対象エリア内かどうか

                fixed useDistX = step(0, farFromSafe.x);
                
//y軸が歪み対象エリア内かどうか

                fixed useDistY = step(0, farFromSafe.y);
                return fixed2(useDistX, useDistY);
            }

            fixed4 frag(v2f i) : SV_Target
            {
                
//対象のy軸とx軸座標が中心点を超えてるかどうかを習得

                fixed2 isUnderHalf = getIsUnderHalf(i);

                
//非歪みエリアから外側にx軸、y軸がどれだけ離れてるかを取得

                fixed2 farFromSafe = getFarFromSafe(i);
                
//中心からx軸、y軸がどれだけ離れてるかを取得

                fixed2 farFromCenter = getFarFromCenter(i);

                
//メインの歪みの基本値を取得

                fixed2 mainDist = getMainDist(farFromSafe);
                
//サブの歪みの基本値を取得

                fixed2 subDist = getSubDist(farFromCenter, mainDist);
                
//メインの座標の減退値を取得

                fixed2 mainReduce = getMainReduce(isUnderHalf, mainDist);
                
//サブの座標の減退値を取得

                fixed2 subReduce = getSubReduce(isUnderHalf, subDist);

                
//対象のx軸、y軸が歪み対象エリア内かどうか

                fixed2 useDist = getUseDist(farFromSafe);

                
//元のuv座標を歪ませて糸巻歪曲収差っぽくする

                i.uv.x = i.uv.x + useDist.x * mainReduce.x + useDist.y * subReduce.x;
                i.uv.y = i.uv.y + useDist.y * mainReduce.y + useDist.x * subReduce.y;

                fixed scalingBase = 4.7;
                fixed scaleFix = 5;

                
//拡大演出が開始されるタイミングを算出

                fixed multi = 1 / _StartScaleInTiming;
                fixed scaleTransition = saturate(_Transition - _StartScaleInTiming) * multi;
                
//画面の拡大値を算出。

                scaleTransition *= scaleTransition;
                fixed scaleInNum = 1 - (scaleTransition * scalingBase / scaleFix);
                
//画面拡大に伴って中央の座標がずれるので、座標の中央値を算出

                fixed scalingSub = scalingBase / 2;
                fixed2 scaleShiftPos = fixed2(scaleTransition * scalingSub / scaleFix, scaleTransition * scalingSub / scaleFix);

                fixed4 col = tex2D(_GrabTexture, i.uv * scaleInNum + scaleShiftPos);
                return col;
            }
            ENDCG
        }

        
//第二Pass、1つ目の集中線を作る

        Pass
        {
            CGPROGRAM

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _Line1Color;

                
//半時計周りに値を大きくなるグラデーションを取得

                fixed deg = getUvAngle(i.uv);
                float resultLine = 0;
                
//エッジ部分にグラデーションがかかった綺麗な集中線を取得

                Unity_SimpleNoise_float(float2(deg, deg), _Line1NoiseScale, resultLine);

                
//sin関数で集中線を細かくする。_Transitionの値が大きいほど、どんどん細かくなる

                fixed lineTransition = lerp(0, 1, saturate(_Transition / _CompLine1Timing + 0.0001));
                resultLine= sin(resultLine * lineTransition * 100);
                col.a *= resultLine;

                return col;
            }

            ENDCG
        } 

        
//第三Pass、画面全体を_FadeOutColorで塗りつぶす

        Pass
        {
            CGPROGRAM

            fixed4 frag (v2f i) : SV_Target
            {
                
//後半の画面全体を埋め尽くす色をalpha0で取得

                fixed4 col = fixed4(_FadeOutColor.r, _FadeOutColor.g, _FadeOutColor.b, 0);

                fixed fadeOutDiff = _CompFadeOutTiming - _StartFadeOutTiming;
                fixed multi = 1 / fadeOutDiff;

                
//_Transitionが_StartFadeOutTimingまで進んだ瞬間から_CompFadeOutTimingの間までにFadeOutを完了させる

                fixed alpha = saturate(saturate(_Transition - _StartFadeOutTiming) * multi);
                col.a = alpha * _FadeOutColor.a;
                return col;
            }

            ENDCG
        }         

        
//第四Pass、リップル演出を走らせる

        Pass
        {
            CGPROGRAM

            
//リップルが発生するTransitionの値を渡して、リップルのカラー値を取得

            fixed4 getRippleColor(fixed length, fixed startTransition)
            {
                fixed multi = 1 / (1 - startTransition);
                fixed rippleTransiton = saturate(_Transition - startTransition) * multi;
                fixed rippleStartPos = rippleTransiton;
                fixed rippleEndPos = (rippleStartPos + (_RippleLength * rippleTransiton));

                fixed isRipple = step(length, rippleEndPos) * step(rippleStartPos, length);

                
//ドーナツ状の波紋を作る

                fixed4 col = isRipple * _RippleColor;
                
//中心に近づくほどアルファが薄くなるグラデーションをかける

                col.a *= isRipple * (length - rippleStartPos) / (rippleEndPos - rippleStartPos);
                return col;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _RippleColor;

                
//uv座標を、画面中央を原点として(0,0)>(1,1)から(-1,-1)>(1,1)の範囲に修正する

                fixed2 fixUv = i.uv * 2 - 1;
                fixed len = length(fixUv) / 1.5;
                
//ドーナッツ状のリップルを取得

                col = getRippleColor(len, _RippleTiming);

                return col;
            }
            ENDCG
        }

        
//第五Pass、2つ目の集中線を作る

        Pass
        {
            CGPROGRAM

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _Line2Color;

                
//半時計周りに値を大きくなるグラデーションを取得

                fixed deg = getUvAngle(i.uv);
                float resultLine = 0;
                
//エッジ部分にグラデーションがかかった綺麗な集中線を取得

                Unity_SimpleNoise_float(float2(deg, deg), _Line2NoiseScale, resultLine);

                
//集中線が発生するタイミングを指定

                fixed fixTransition = saturate(_Transition - _StartLine2Timing);
                
//sin関数で集中線を細かくする。_Transitionの値が大きいほど、どんどん細かくなる

                resultLine= sin(resultLine * fixTransition * 100);
                col.a = resultLine;

                fixed multi = 1 / (1 - _StartLine2Timing + 0.0001);
                fixed rate = fixTransition * multi;
                
//Easeをかけて後半にかけて急速に1に近づくようにする

                rate = rate * rate;
                
//Transition後半にかけて段々と集中線を消す

                col.a -= lerp(0, 1, rate);

                return col;
            }

            ENDCG
        } 
    }
}

プロパティの解説

各プロパティの解説は下の通り。
ゲームに実装の際には、 C#からシェーダー変数にアクセス し、Transitionでアニメーションさせることになると思う。
FTLドライブシェーダーのプロパティ
 Transition 
: アニメーション開始から終了までの遷移の数値。この値を弄ってアニメーションさせる。

 Line1Color 
: 前半に現れる集中線の色。
 Line2Color 
: 後半に現れる集中線の色。
 FadeOutColor 
: 最終的に画面全体を塗りつぶす色。
 RippleColor 
: リップル(ドーナツ状の波紋)の色。

 CompDistortTiming 
: Transitionのどのタイミングで、画面全体が歪ませる処理を完了させるかの値。
 StartScaleInTiming 
: Transitionのどのタイミングで、拡大処理を開始させるかの値。
 CompLine1Timing 
: Transitionのどのタイミングで、前半の集中線の処理を完了させるかの値。
 StartFadeOutTiming 
: Transitionのどのタイミングで、画面全体の塗りつぶし処理を開始させるかの値。
 CompFadeOutTiming 
: Transitionのどのタイミングで、画面全体の塗りつぶし処理を完了させるかの値。
 RippleTiming 
: Transitionのどのタイミングで、リップル演出を開始させるかの値。
 StartLine2Timing 
: Transitionのどのタイミングで、後半の集中線の処理を開始させるかの値。

 Line1NoiseScale 
: 前半の集中線の線の細かさに影響する。
 Line2NoiseScale 
: 後半の集中線の線の細かさに影響する。
 RippleLength 
: リップルの太さに影響する。
気が向いたら各プロパティの解説動画を添付するかもしれない。
なにせ数が多くて時間が掛かるんで、とりあえず今はここまで。
0
0