はじめに
Unityのオーバードロー確認というと、
この明るさを見て判断することが多いかなと思います。
なんとなく重たいだろうなということは分かるが、
レギュレーションで取り扱うのには少し不便だなと感じていました。
そこで、オーバードローの数値化できないか試してみました。
明確に数値化されていれば、〇〇〇%は超えたらNGという形で
レギュレーションも決めやすくなると思います。
制作物はGithubで公開してます
以降で仕組みについて書いていきます。
画面を赤くする仕組みについて
下準備として、描画箇所の色が明るくなる仕組みを再現します。
まずは簡単なシェーダーを2つ用意しました。
Depthを描くためのOpaqueシェーダー
描画率を足していくための加算シェーダー
どちらもノード無しで色を (0.1, 0, 0)
の薄い赤に変えただけですね。
(数値化する時に扱いやすいのでこの色)
OpaqueのPassでZWriteをしておき、TransparentのPassでひたすら赤を加算していけば
描画の重なる場所だけ赤くなるだろうという流れです。
実際に表示するためにRendereDataを作る
先に用意したシェーダーだけの描画にしたいので、FilteringはどちらもNothingにしておきます。
Depth用とOverdraw用にRenderObjectsを登録します。
このRendererをカメラに適用して背景を黒にすれば完成!
描画の多い箇所が真っ赤になりました。
数値化について
本題の数値化に入っていきます。
現時点で画面のRチャンネルに{ 描画回数 * 0.1 }
の数値が入っているので、
この映像をRenderTextureとして取得し
{ R値の全ピクセル合計 * 100 / ピクセル数 }
を計算すれば平均描画数が分かります。
今回は%表記してるのでさらに100倍ですね。
CPUで計算した場合
ReadPixelsを使うとこんな感じになります
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)回ループさせてるので当然ですね…
ComputeShaderを利用する
これを実用レベルの負荷に抑えるために
覚えたてのComputeShaderを使ってみることにしました。
こんな風に画面をいくつかのグループに分割し、
各グループが持つピクセルの平均値をComputeShaderで計算してCPUに渡します。
CPUには全グループの結果を計算させて、ループ回数を抑える方針にします。
グループ数が多いほど、GPUの並列処理が効果を発揮してくれますが、
代わりにCPUで平均を出す際のループが重くなる形ですね。
今回は色々試してみて { 32 * 32 } グループに落ち着きました。
この方針でコードの方も見ていきます。
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); // 全グループの平均値
#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の時同様の数値になり、負荷もだいぶ収まりました。
あとがき
以上、オーバードローを数値化することで、レギュレーションで扱いやすくする試みでした。
今回は全ての描画を同じ負荷と仮定した計測になりますが、
実際には使われてるシェーダーの負荷が大きく影響するので、
シェーダーに応じて加算する値を変えるなどの拡張をすると
より実態に近い計測ができそうですね。