知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】円でトランジション【2】 #31

前回の成果

円をアスペクト比に関わらず、変化させないように表示した

soramamenatan.hatenablog.com


今回やること

円のトランジションを、前回の続きからやっていきます。

karanokan.info


前回までのソースコード

Shader "Unlit/transitionCircle" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType" = "Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

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

            // 内積を出す
            float circle(float2 p) {
                return dot(p, p);
            }

            fixed4 frag (v2f i) : SV_Target {
                // 円の大きさと位置の調整
                float2 f_st = frac(i.uv) * 2.0 - 1.0;
                // 画面解像度に影響されないようにする
                f_st.y *= _ScreenParams.y / _ScreenParams.x;
                float ci = circle(f_st);
                fixed4 col = 0.0;
                // ciが0.1未満なら0を、そうでないなら1を返す
                col.a = step(0.1, ci);
                return col;
            }
            ENDCG
        }
    }
}


円を増やす

まず、表題の通り円を増やしていきます。
上記のソースコードに下記のソースコードを追加してください。


ソースコード

Properties {
    _MainTex ("Texture", 2D) = "white" {}
    // 追加
    _CircleSideNum ("Circle Side Num", int) = 16
}

// 省略

sampler2D _MainTex;
float4 _MainTex_ST;
// 追加
int _CircleSideNum;

// 省略

fixed4 frag (v2f i) : SV_Target {
    // 追加
    float2 div = float2(_CircleSideNum, _CircleSideNum * _ScreenParams.y / _ScreenParams.x);
    float2 st = i.uv * div;
    float2 f_st = frac(st) * 2.0 - 1.0;
    float ci = circle(f_st);
    fixed4 col = 0.0;
    col.a = step(0.1, ci);
    return col;
}

uv値を_CircleSideNum倍しているので、その分円が増えます。


結果

円が増えれば成功です。
下記の画像では_CircleSideNumを5に設定しています。

f:id:soramamenatan:20191207195742p:plain

次は円を徐々に大きくしていきます。


円を大きくし、大きくするタイミングをズラす

次に、今のままだと円が小さいので大きくします。
そして、一斉に大きくなるとトランジションっぽくないので円を大きくするタイミングをズラしていきます。


ソースコード

こちらも同じように追加してください。

Properties {
    _MainTex ("Texture", 2D) = "white" {}
    _CircleSideNum ("Circle Side Num", int) = 16
    // 追加
    _CircleValue ("Circle Value", Range(0, 1)) = 0
    _Threshold ("Threshold", Range(0, 1)) = 0
}

// 省略

sampler2D _MainTex;
float4 _MainTex_ST;
int _CircleSideNum;
// 追加
float _CircleValue;
float _Threshold;

// 省略

fixed4 frag (v2f i) : SV_Target {
    float2 div = float2(_CircleSideNum, _CircleSideNum * _ScreenParams.y / _ScreenParams.x);
    float2 st = i.uv * div;
    // 追加
    float i_st = floor(st);
    float value = _CircleValue - i_st.x * _Threshold;
    float2 f_st = frac(st) * 2.0 - 1.0;
    float ci = circle(f_st);
    fixed4 col = 0.0;
    col.a = step(value, ci);
    return col;
}

stepを0.1の固定値からvalueに変更しました。


floor

これはfracの逆で、小数値の整数部分を返す関数となります。

// 3が返ってくる
frac(3.5);

// 0が返ってくる
frac(0.8);


円の大きくなり方がズレる理由

ズラす処理は主にこの3行で行なっています。

float2 st = i.uv * div;
float i_st = floor(st);
float value = _CircleValue - i_st.x * _Threshold;

上記で説明したfloorを使用することで、i_stに入る値が下記の画像のようになります。

f:id:soramamenatan:20191213143742p:plain

これにより、valueに入る値が各列で異なるので差が生まれます。

結果

このように各列で円の大きさに差があれば成功です。
下記の画像では、

  • _CircleSideNumを6
  • _CircleValueを1
  • _Thresholdを0.19

に設定しています。

f:id:soramamenatan:20191214115345p:plain


円を消し、方向を加味してトランジション

今のままですと、トランジションしきった後にも下記の画像のように円が残ってしまいってしまいます。

f:id:soramamenatan:20191215143531p:plain


ですので、しっかりと円が消えて、なおかつ芳香も考慮するようにします。


ソースコード

こちらも追加してください

Properties {
    _MainTex ("Texture", 2D) = "white" {}
    _CircleSideNum ("Circle Side Num", int) = 16
    _CircleValue ("Circle Value", Range(0, 1)) = 0
    _Threshold ("Threshold", Range(0, 1)) = 0
    // 追加
    _Direction ("Direction(X, Y)", Vector) = (1, 1, 0, 0)
}

// 省略

float _CircleValue;
float _Threshold;
// 追加
float2 _Direction;

// 省略

// 変更
fixed4 frag (v2f i) : SV_Target {
    float2 div = float2(_CircleSideNum, _CircleSideNum * _ScreenParams.y / _ScreenParams.x);
    float2 st = i.uv * div;
    float2 i_st = floor(st);
    float2 dir = normalize(_Direction);
    float value = _CircleValue * (dot(div - 1.0, abs(dir)) * _Threshold + 2.0);
    float2 sg = sign(dir);
    float2 f = (div - 1.0) * (0.5 - sg * 0.5) + i_st * sg;
    float v = value - dot(f, abs(dir)) * _Threshold;
    float2 f_st = frac(st) * 2.0 - 1.0;
    float ci = circle(f_st);
    fixed4 col = 0.0;
    float a = 1;
    a = min(a, step(v, ci));
    col.a = a;
    return col;
}

方向であるdirectionを追加し、fragment shaderで色々しています。


_CircleValue * (dot(div - 1.0, abs(dir)) * _Threshold + 2.0);

これで、最後の円が大きくなるタイミングで、円が分割した領域よりも大きくなる値(2.0)を加算します。
そして、_CircleValueに乗算することにより、valueが0~1の範囲となるようにしています。
2.0を加算しないと、_CircleValueを1にしても下記の画像のようにトランジションしきらなくなってしまいます。

f:id:soramamenatan:20191215152250p:plain


normalize

これは正規化する関数です。 正規化とは、ベクトルの方向を維持しながら大きさを1にすることです。

yaju3d.hatenablog.jp


sign

これはベクトルが正か負かを返す関数です

// 1.0が返ってくる
sign(1.0, 1.0)

// -1.0が返ってくる
sign(-1.0, -1.0)

// 0.0が返ってくる
sign(0.0, 0.0)


(div - 1.0) * (0.5 - sg * 0.5) + i_st * sg;

この数式の前半の部分は、円をどのように大きくするかを決めています。
後半のi_st * sgこの部分で、i_stを逆数にしています。

そして、下記の数式で、今回出したfと方向ベクトルの内積によって円ごとに大きくなるタイミングを調整しています。

float v = value - dot(f, abs(dir)) * _Threshold;


円と円との繋ぎ目を修正する

これまでの段階でほぼトランジションは完成しています。

f:id:soramamenatan:20191215160559g:plain

しかし、拡大してよくみると円と円との繋ぎ目が汚くなってしまっています。

f:id:soramamenatan:20191215154857p:plain


ソースコード

flagment shaderを変更してください。

// 変更
fixed4 frag (v2f i) : SV_Target {
    float2 div = float2(_CircleSideNum, _CircleSideNum * _ScreenParams.y / _ScreenParams.x);
    float2 st = i.uv * div;
    float2 i_st = floor(st);
    float2 dir = normalize(_Direction);
    float value = _CircleValue * (dot(div - 1.0, abs(dir)) * _Threshold + 2.0);
    float2 sg = sign(dir);
    float a = 1;
    for (int i = -1; i <= 1; i++) {
        for (int j = -1; j <= 1; j++) {
            float2 f = (div - 1.0) * (0.5 - sg * 0.5) + (i_st + float2(i, j)) * sg;
            float v = value - dot(f, abs(dir)) * _Threshold;
            float2 f_st = frac(st) * 2.0 - 1.0;
            float ci = circle(f_st  - float2(2.0 * i, 2.0 * j));
            a = min(a, step(v, ci));
        }
    }
    fixed4 col = 0.0;
    col.a = a;
    return col;
}

汚い原因

今のソースコードですと、各円の区切られた範囲より外には描画することができないので、繋ぎ目の見栄えがよくありません

f:id:soramamenatan:20191215162524p:plain

シェーダでトランジション(図形) │ 空の缶詰:より引用


繋ぎ目を綺麗にする

これは、円自身と、円と周りの8つの円の計9つの円を計算すれば可能です。

for (int i = -1; i <= 1; i++) {
    for (int j = -1; j <= 1; j++) {
        float2 f = (div - 1.0) * (0.5 - sg * 0.5) + (i_st + float2(i, j)) * sg;
        float v = value - dot(f, abs(dir)) * _Threshold;
        float2 f_st = frac(st) * 2.0 - 1.0;
        float ci = circle(f_st  - float2(2.0 * i, 2.0 * j));
        a = min(a, step(v, ci));
    }
}

iを-1から始めることにより、(-1, -1)は左上の円、(0, 0)は自身、(1, 1)は右下の円となるので、9つの円を計算することができます。

結果

これにより、繋ぎ目を綺麗にすることができました。

f:id:soramamenatan:20191215164650p:plain

下記の画像では、

  • _CircleSideNumを16
  • _CircleValueは0から1に
  • _Thresholdを0.541
  • _Directionを(1, 0, 0, 0)

にしています
f:id:soramamenatan:20191215165247g:plain

また、_Directionを(1, 1, 0, 0)にすると左下からトランジションをしてくれるようになります。

f:id:soramamenatan:20191215165520g:plain

ソースコードにコメントを付与

Shader "Unlit/transitionCircle" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
        _CircleSideNum ("Circle Side Num", int) = 16
        _CircleValue ("Circle Value", Range(0, 1)) = 0
        _Threshold ("Threshold", Range(0, 1)) = 0
        _Direction ("Direction(X, Y)", Vector) = (1, 1, 0, 0)
    }
    SubShader {
        Tags { "RenderType" = "Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            int _CircleSideNum;
            float _CircleValue;
            float _Threshold;
            float2 _Direction;

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

            // 内積を出す
            float circle(float2 p) {
                return dot(p, p);
            }

            fixed4 frag (v2f i) : SV_Target {
                float2 div = float2(_CircleSideNum, _CircleSideNum * _ScreenParams.y / _ScreenParams.x);
                // 円の数
                float2 st = i.uv * div;
                float2 i_st = floor(st);
                // 正規化した方向
                float2 dir = normalize(_Direction);
                // 最後の円が大きくなるタイミングを加味したトランジション
                float value = _CircleValue * (dot(div - 1.0, abs(dir)) * _Threshold + 2.0);
                float2 sg = sign(dir);
                float a = 1;
                // 自身と周囲8つの円を描画
                for (int i = -1; i <= 1; i++) {
                    for (int j = -1; j <= 1; j++) {
                        // 円の消えるタイミング
                        float2 f = (div - 1.0) * (0.5 - sg * 0.5) + (i_st + float2(i, j)) * sg;
                        float v = value - dot(f, abs(dir)) * _Threshold;
                        float2 f_st = frac(st) * 2.0 - 1.0;
                        float ci = circle(f_st  - float2(2.0 * i, 2.0 * j));
                        a = min(a, step(v, ci));
                    }
                }
                fixed4 col = 0.0;
                col.a = a;
                return col;
            }
            ENDCG
        }
    }
}

今回はここで終了となります。
ご視聴ありがとうございました。