Overdrawを数値化してみた

# はじめに
Unityのオーバードロー確認というと、
この明るさを見て判断することが多いかなと思います。

なんとなく重たいだろうなということは分かるが、
レギュレーションで取り扱うのには少し不便だなと感じていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555386/63b212cf-6c89-d3d1-96c1-843e1ef1bd7c.png)

\
そこで、オーバードローの数値化できないか試してみました。
明確に数値化されていれば、〇〇〇%は超えたらNGという形で
レギュレーションも決めやすくなると思います。

![OverdrawChecker_1_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555386/6aa262cd-8de5-d667-c936-895071d2a4f4.gif)


#### 制作物はGithubで公開してます
https://github.com/haw2fregel/OverdrawChecker

以降で仕組みについて書いていきます。


# 画面を赤くする仕組みについて
下準備として、描画箇所の色が明るくなる仕組みを再現します。
まずは簡単なシェーダーを２つ用意しました。



##### Depthを描くためのOpaqueシェーダー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555386/5528af39-e00a-bc6b-5515-009e3acaa343.png)
##### 描画率を足していくための加算シェーダー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555386/d5a1ec55-6b7b-9381-497e-5a20436b27a2.png)

どちらもノード無しで色を `(0.1, 0, 0)` の薄い赤に変えただけですね。
（数値化する時に扱いやすいのでこの色）
OpaqueのPassでZWriteをしておき、TransparentのPassでひたすら赤を加算していけば
描画の重なる場所だけ赤くなるだろうという流れです。

#### 実際に表示するためにRendereDataを作る
先に用意したシェーダーだけの描画にしたいので、FilteringはどちらもNothingにしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555386/27a28fd9-0aeb-81c6-39d5-43a4672dcc37.png)

Depth用とOverdraw用にRenderObjectsを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555386/26199676-a304-087c-8234-6fa4ec108c50.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555386/35bc0569-c1f4-3326-5a73-7ad4aba94efb.png)


このRendererをカメラに適用して背景を黒にすれば完成！
描画の多い箇所が真っ赤になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555386/803259ff-71ea-96e1-8f49-e3177c7f7ebf.png)

# 数値化について
本題の数値化に入っていきます。

現時点で画面のRチャンネルに`{ 描画回数 * 0.1 }`の数値が入っているので、
この映像をRenderTextureとして取得し
`{ R値の全ピクセル合計 * 100 / ピクセル数 }`を計算すれば平均描画数が分かります。
今回は％表記してるのでさらに100倍ですね。

#### CPUで計算した場合
ReadPixelsを使うとこんな感じになります
```C#:OverdrawChecker.cs
int overdrawValue = 0;

//RenderTextureをTexture2Dで読み取る
var currentRT = RenderTexture.active;
RenderTexture.active = _rt;
var texture = new Texture2D(_rt.width, _rt.height);
texture.ReadPixels(new Rect(0, 0, _rt.width, _rt.height), 0, 0);
texture.Apply();
RenderTexture.active = currentRT;

//Texture2Dのピクセル情報を配列で取得
var colors = texture.GetPixels();

//全ピクセルループでR値の合計を取得
foreach (var color in colors)
{
    overdrawValue += (int)(color.r * 10000);
}

//ピクセル数で割り平均値を取得
overdrawValue = overdrawValue / colors.Length;
```

数値の取得ができました！
ただし、FPSを見ると分かりますがめちゃくちゃ重いです。
(1980*1080)回ループさせてるので当然ですね…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555386/8d180170-2b4f-d690-6e91-e8c81cf6f75c.png)

#### ComputeShaderを利用する
これを実用レベルの負荷に抑えるために
覚えたてのComputeShaderを使ってみることにしました。

こんな風に画面をいくつかのグループに分割し、
各グループが持つピクセルの平均値をComputeShaderで計算してCPUに渡します。
CPUには全グループの結果を計算させて、ループ回数を抑える方針にします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555386/16c6c45b-07e1-1815-12e5-f47563ec23f8.png)

グループ数が多いほど、GPUの並列処理が効果を発揮してくれますが、
代わりにCPUで平均を出す際のループが重くなる形ですね。

今回は色々試してみて { 32 * 32 } グループに落ち着きました。

この方針でコードの方も見ていきます。
```C#:OverdrawChecker.cs
int overdrawValue = 0;

int num = DivCount * DivCount; // グループ数の指定　32*32
ComputeBuffer buffer = new ComputeBuffer(num, sizeof(int)); // 結果受け取り用バッファ

int kernel = _cs.FindKernel("CSMain");

_cs.SetBuffer(kernel, "_Result", buffer); 
_cs.SetTexture(kernel, "_OverdrawTex", _rt);
_cs.SetInt("_DivCount", DivCount); //グループ数
_cs.SetVector("_Resolution", new Vector4(_rt.width / DivCount, _rt.height / DivCount, 0, 0)); //各グループが持つピクセル数xy

_cs.Dispatch(kernel, DivCount, DivCount, 1); // ComputeShader実行

int[] datas = new int[num];
buffer.GetData(datas);
buffer.Release();

foreach (var data in datas) // グループ数分のループ 1024
{
    overdrawValue += data;
}
overdrawValue = (int)Mathf.Ceil((float)overdrawValue / (float)num); // 全グループの平均値
```
```hlsl:OverdrawToValue.compute
#pragma kernel CSMain

RWStructuredBuffer<int> _Result;
Texture2D<float4> _OverdrawTex;

int _DivCount;
uint2 _Resolution;

[numthreads(1, 1, 1)]
void CSMain (uint2 id : SV_GroupID) // グループIDを取得
{
    uint result = 0;
    uint2 offset = id * _Resolution + _Resolution * 0.5; // 現在見てるグループの中心

    //グループが担当する全ピクセルをループで合計値を出す
    for (int i = (int)(_Resolution.y * -0.5); i <= (int)(_Resolution.y * 0.5); i++)
    {
        for (int j = (int)(_Resolution.x * -0.5); j <= (int)(_Resolution.x * 0.5); j++)
        {
            uint2 uv = offset + uint2(j, i); // ピクセル座標
            result += (int)(_OverdrawTex[uv].r * 100); // 1描画当たり1加算していく
        }
    }

    int index = id.y * _DivCount + id.x; // 計算結果の格納先index
    _Result[index] = ceil((float)result / (_Resolution.x * _Resolution.y) * 100); //担当ピクセル数で割って平均値を送る
}
```

結果はこちら。
ReadPixelの時同様の数値になり、負荷もだいぶ収まりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555386/dc28b049-b932-ec98-66ae-443ee0e5608f.png)


# あとがき
以上、オーバードローを数値化することで、レギュレーションで扱いやすくする試みでした。

今回は全ての描画を同じ負荷と仮定した計測になりますが、
実際には使われてるシェーダーの負荷が大きく影響するので、
シェーダーに応じて加算する値を変えるなどの拡張をすると
より実態に近い計測ができそうですね。