LoginSignup
4
4

Overdrawを数値化してみた

Last updated at Posted at 2024-01-28

はじめに

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

なんとなく重たいだろうなということは分かるが、
レギュレーションで取り扱うのには少し不便だなと感じていました。
image.png


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

OverdrawChecker_1_2.gif

制作物はGithubで公開してます

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

画面を赤くする仕組みについて

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

Depthを描くためのOpaqueシェーダー

image.png

描画率を足していくための加算シェーダー

image.png

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

実際に表示するためにRendereDataを作る

先に用意したシェーダーだけの描画にしたいので、FilteringはどちらもNothingにしておきます。
image.png

Depth用とOverdraw用にRenderObjectsを登録します。
image.png

image.png

このRendererをカメラに適用して背景を黒にすれば完成!
描画の多い箇所が真っ赤になりました。
image.png

数値化について

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

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

CPUで計算した場合

ReadPixelsを使うとこんな感じになります

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

ComputeShaderを利用する

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

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

image.png

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

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

この方針でコードの方も見ていきます。

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); // 全グループの平均値
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

あとがき

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

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

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4