【UnityShader】円でトランジション【2】 #31
前回の成果
円をアスペクト比に関わらず、変化させないように表示した
今回やること
円のトランジションを、前回の続きからやっていきます。
前回までのソースコード
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に設定しています。
次は円を徐々に大きくしていきます。
円を大きくし、大きくするタイミングをズラす
次に、今のままだと円が小さいので大きくします。
そして、一斉に大きくなるとトランジションっぽくないので円を大きくするタイミングをズラしていきます。
ソースコード
こちらも同じように追加してください。
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に入る値が下記の画像のようになります。
これにより、valueに入る値が各列で異なるので差が生まれます。
結果
このように各列で円の大きさに差があれば成功です。
下記の画像では、
- _CircleSideNumを6
- _CircleValueを1
- _Thresholdを0.19
に設定しています。
円を消し、方向を加味してトランジション
今のままですと、トランジションしきった後にも下記の画像のように円が残ってしまいってしまいます。
ですので、しっかりと円が消えて、なおかつ芳香も考慮するようにします。
ソースコード
こちらも追加してください
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にしても下記の画像のようにトランジションしきらなくなってしまいます。
normalize
これは正規化する関数です。
正規化とは、ベクトルの方向を維持しながら大きさを1にすることです。
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;
円と円との繋ぎ目を修正する
これまでの段階でほぼトランジションは完成しています。
しかし、拡大してよくみると円と円との繋ぎ目が汚くなってしまっています。
ソースコード
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; }
汚い原因
今のソースコードですと、各円の区切られた範囲より外には描画することができないので、繋ぎ目の見栄えがよくありません
繋ぎ目を綺麗にする
これは、円自身と、円と周りの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つの円を計算することができます。
結果
これにより、繋ぎ目を綺麗にすることができました。
下記の画像では、
- _CircleSideNumを16
- _CircleValueは0から1に
- _Thresholdを0.541
- _Directionを(1, 0, 0, 0)
にしています
また、_Directionを(1, 1, 0, 0)にすると左下からトランジションをしてくれるようになります。
ソースコードにコメントを付与
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 } } }
今回はここで終了となります。
ご視聴ありがとうございました。