-100p

-10p

+10p

+100p

Unity糸巻歪曲収差シェーダー

UnityでuGUI用糸巻歪曲収差シェーダー。逆魚眼レンズのシェーダー。
(Pincushion Distortion Shader for Unity uGUI)

関連ページ 参考URL
最近レトロフリークで遊ぶために、少年時代買えなかったSFCのソフトを大量に買い集めてる。
その中で天地創造というゲームのマップ移動がなかなか美しくて、これを自分の今の技術で真似てみようと考えた。
天地創造のマップ表現は複数の技術を使っていて完コピとまでいかなかったが、ある程度コピーできたので共有。

糸巻歪曲収差シェーダー概要

糸巻歪曲収差というとやたら仰々しい名前に聞こえるが、魚眼レンズの逆バージョンと考えると分かりやすい。
画面の中心から離れて端に近づくほど、ぐいっと手前に画像が近寄ってくる感じ。

下はこのシェーダーを作る動機となった、天地創造の動画になる。

天地創造の場合、画面の下4/3は単純なy軸のみの糸巻歪曲収差シェーダーを使っているように見える。
対して残りの上1/4は、3~4つぐらいの技術を併用して描画してるように思う。
たぶん糸巻歪曲収差+y軸反転+色相を青色に寄せる+境目部分の黒いボヤけた表現など。
そもそも何十年も前の作品なので、今でいうシェーダーという技術は使ってなかったかもしれない。

とりあえず自分がやりたかったのは画面の周囲が歪む糸巻歪曲収差の部分だったので、それを作った。
下の動画はそのサンプル。


天地創造ではy軸のみの歪みを使っていたが、このシェーダーではx軸+y軸、y軸のみ、x軸のみの3パターンを選べるようにした。

糸巻歪曲収差シェーダー、コード全文

糸巻歪曲収差処理はネットで検索してすぐ見つかったものの、そのコードが何をやってるかよく分からなかった。
なのでEaseを使って自前で実装した。
見る人が見るとやたら長くて汚いコードに見えるかもしれない。

以下がそのコード全文。uGUI Imageへの反映方法は こちらの記事 参照。
今回の場合GrabPassを使っているので、 こちら の知識も必要になる。
Shader "UI/Pincushion" 
{
    Properties 
    {
        [HideInInspector]_MainTex("-",2D)="white"{} 
        [KeywordEnum(All, Vertical, Horizontal)] _Direction("Direction", Int) = 0
        _DistortionRange ("DistortionRange", Range(0.0, 10.0)) = 5
        _EasePow ("EasePow", Range(0, 10)) = 10
    }

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

        GrabPass{}

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

            sampler2D _GrabTexture;
            float4 _GrabTexture_TexelSize;

            #pragma multi_compile _DIRECTION_ALL _DIRECTION_VERTICAL _DIRECTION_HORIZONTAL

            fixed _DistortionRange;
            fixed _EasePow;

            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;
            }

            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 = _DistortionRange * 0.1;
                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で抑える

                
//_EasePowのrangeが0~10なのでfixEaseの振れ幅は12.7~2.7

                fixed fixEase = 12.7 - _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);

#ifdef _DIRECTION_ALL
                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;
#elif _DIRECTION_VERTICAL
                i.uv.x = i.uv.x + useDist.y * subReduce.x * 2;
                i.uv.y = i.uv.y + useDist.y * mainReduce.y;
#else
                i.uv.x = i.uv.x + useDist.x * mainReduce.x;
                i.uv.y = i.uv.y + useDist.x * subReduce.y * 2;
#endif
                fixed4 col = tex2D(_GrabTexture, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

プロパティを解説

用意してあるプロパティは3つ。

 Direction 
: 歪みを掛ける方向。Allで全方位に歪みをかける。Verticalで縦方向、Horizontalで横方向のみ。
 DistortionRange 
: 外側から中心に向かう、歪みの効果範囲。0で歪みを掛けない、10で全画面に歪みを掛ける。
 EasePow 
: 歪みの力。大きくすると、画面端に近づくほど極端に画面が歪む。初期値ですでに最大値にしてある。

EasePowは初期値で最大にしてあるが、歪みが強すぎると思う人は小さくするとその分歪みが柔らかくなる。

1
1

-100p

-10p

+10p

+100p