【Unity】ちゃんとした円形フェードShaderを作る

# はじめに
円形フェードをShaderで作る場合にいろいろ考える点が多くあったのでここに残します。

これ以降出てくるコードについて説明します。
まずUnityのUnlitShaderから作っています。
一部のよく使うメソッドはここで紹介して以後説明を省きます。
```HLSL
// 円の距離関数
float circle(float2 p, float r)
{
    return length(p)-r;
}
```

完成形の簡単なデモが[こちら(ShaderToy)](https://www.shadertoy.com/view/Dsl3D7)にあります。
クリックしたところをフェード目標地点として円が大小します。

# 中途半端な円形フェード
一番簡単で中途半端な円形フェードは
```HLSL
_Fade ("Fade", float) = 0.5
_FadePos("FadePos", Vector) = (0.0,0.0,0.0,0.0)

float4 frag (v2f i) : SV_Target
{
    float2 uv = i.uv*2-1;
    float col = step(.001, circle(uv + _FadePos,_Fade));
    return float4(0,0,0,col);
}
```
![image_003_0000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/dcf443e6-c36f-59fa-b3f4-d260e82f59ca.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/fd01a3bf-5be2-272a-f192-e7f894831e92.png)
これの問題点は
* `_Fade`の値が1.0になっても四隅がフェードされない
* 画面に合わせて円が引き延ばされて楕円形になってる

### `_Fade`の値が1.0になっても四隅がフェードされない
0.0~1.0の間で制御できたほうが便利です。一応`_Fade`の値を1.0以上にすれば四隅をフェードさせることはできますが使いにくいでしょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/5c034363-4233-b85b-136a-5d9d32626a25.png)
また、`_FadePos`でフェードする中心点をずらした場合にも1.0ですっぽりとフェードさせることができません。画像では全部フェードさせるのに大体`_Fade=1.9`ぐらいを必要としています。
`_FadePos`が変わる度に決め打ちで`_Fade`の値を打つのはカッコ悪いです。

### 画面に合わせて円が引き延ばされて楕円形になってる
楕円形にしたいのならばこの記事の大部分を読まなくてもいいことになります。
今回は円にすることを目標にします。

# いい感じに見えるようにしていく
### 1.円を引き延ばされないようにする
``` HLSL
v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);

    // フェードの画像サイズを使って縦横比を1:1に修正する
    float2 size = float2(_ImageSizeX,_ImageSizeY);
    v.uv*=size;
    v.uv=(v.uv*2.-size.xy)/min(size.x,size.y);
    o.uv = v.uv;
    return o;
}
```
コメント以降部分でuvの縦横比を1:1にして円が楕円になるのを防いでいます。
ゲームの時の画面サイズは大体**1920*1080**なので`_ImageSizeX=1920` `_ImageSizeY=1080`になります。
この正規化する方法は[ShaderToy](https://www.shadertoy.com/)などで使われている方法をUnityに変換しただけです。
```HLSL
vec2 p = (fragCoord.xy * 2. - iResolution.xy)/min(iResolution.x, iResolution.y);
```
ShaderToyでこんなコードを見たことがあるはず。
円をが楕円になってしまうのを防ぐのはこれで解決です。

### 2.フェードの目標地点を設定する
``` HLSL
v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);

    // フェードさせたい座標分uvをずらす
    v.uv+= _FadeTarget*-.5;
    
    // 縦横比を1:1に修正するコード
}
```
頂点シェーダー内でフェードさせたい座標分uvをずらします。
入れる座標は画面中央を(0,0)として右上(1,1)、左下(-1,-1)です。
数値を変換するコードをC#で用意しておくと便利です。
```C#
// ワールド座標からフェード位置を作り出す
public static void SetFadeTargetPos(Material material, Vector2 posWS)
{
    Vector2 posVS = Camera.main.WorldToViewportPoint(posWS);
    posVS *= 2f;
    posVS = new Vector2(posVS.x - 1f, posVS.y - 1f);
    material.SetVector("_FadeTarget", new Vector4(posVS.x, posVS.y, 0f, 0f));
}
```
### 3. `_Fade`の値が1.0で四隅がフェードするようにする
```HLSL
// 距離を求める
float dist(float2 A, float2 B)
{
    float2 C=A-B;
    return dot(C,C);
}

float4 frag (v2f i) : SV_Target
{
    float aspect = _ImageSizeX/_ImageSizeY;

    // フェード地点から一番遠い四隅を探す
    const float2 fadeTargetPos = float2(_FadeTarget.x*aspect, _FadeTarget.y);
    float maxDist;
    maxDist=dist(fadeTargetPos, float2(-1.*aspect,-1.));
    maxDist=max(maxDist, dist(fadeTargetPos, float2(1.*aspect,-1.)));
    maxDist=max(maxDist, dist(fadeTargetPos, float2(1.*aspect,1.)));
    maxDist=max(maxDist, dist(fadeTargetPos, float2(-1.*aspect,1.)));
    maxDist=sqrt(maxDist);

    // 一番遠い四隅の距離を最大半径とするフェードをする
    float fade = step(.001 ,Circle(i.uv, (1.-_Fadeout)*maxDist));
    float4 col = float4(_Color,fade);
    return col;
}
```
ピッタリ覆いつくせるフェード半径を計算します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/1feaf7a4-f0c4-0275-089d-5cc68b89d783.png)
今までのフェード半径は画面の横の長さをフェード半径としていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/1ed29c66-cd49-5630-17e5-791a4a3b3b72.png)
これを**フェード中心から一番遠い四隅までの距離**をフェード半径とすればピッタリの円を作ることができます。
フェードする場所は真ん中だけを想定していないので一番遠い四隅を探します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/43b38589-b9e3-84b2-fa3f-f1644be96969.png)
4回探索して赤矢印が一番長い距離なのでこれを採用して半径とします。
```HLSL
maxDist=dist(fadeTargetPos, float2(-1.*aspect,-1.));
maxDist=max(maxDist, dist(fadeTargetPos, float2(1.*aspect,-1.)));
maxDist=max(maxDist, dist(fadeTargetPos, float2(1.*aspect,1.)));
maxDist=max(maxDist, dist(fadeTargetPos, float2(-1.*aspect,1.)));
```
`max()`を使って更新してます。

距離を求める方法は`distance()`を使ってもいいのですが
```math
a^2=b^2+c^2\\
a=\sqrt{b^2+c^2}\\
```
ルートを外しても距離の大小は変化しないので距離の二乗を利用して処理速度を速くします。
```HLSL
float dist(float2 A, float2 B)
{
    float2 C=A-B;
    return dot(C,C);
}
```
これを使えばできます。

# 完成！
* 1.0で四隅がしっかりとフェードして
* 形が楕円にならない
フェードの完成です。

問題点として
* フェードする目標地点が動きながらフェードするとフェードが大小してしまう
がありますが私が作りたかったフェードは完成しました。