はじめに
ゲームにおける描画パフォーマンスのチューニングを進めていく際、(もちろんまずはプロファイリングから入るわけですけれどもそれはそれとして)大抵、真っ先に議題に上がるのはピクセルの塗り潰し/フィル負荷になりますよね。
シェーダを軽量化する、ポストプロセスを軽いものに変更する、画面解像度を小さくする、VFXの発生数を削る、その他諸々...最適化の選択肢は数多ありますが、負荷を確認する上で大きな指標になるのは重ね塗り、いわゆるオーバードローです。
オーバードロー
オーバードローの問題は、大きく2種類の基準に分けられます。
- 無駄/不要なフィルが発生していないか?
- 過度に塗り過ぎてはいないか?
前者「無駄/不要なフィルが発生していないか?」は話として分かりやすいですよね。他のオブジェクトに隠れた見えない場所にオブジェクトを配置していたり、同じオブジェクトを複数個重なった状態で配置してはいないか。このような「シンプルに無駄な塗り」が検出された場合、不要なオブジェクトを削除したり、不透明描画であれば手前から描かれるようにRenderQueueを調整したり、あるいはオクルージョンカリングの調整を行ったりといった改善策を模索することができます。
昨今のモバイルGPUはタイルベースアーキテクチャが採用されており、不透明描画は最も手前の1ピクセル分しかシェーディングされないといった内部的な最適化が走っていたりしますが、これに頼るだけで最適化を怠るのはプログラマとして褒められた行動ではないと私は考えます。最低限やるべきことはやっておきましょう。
そして後者「過度に塗り過ぎてはいないか?」。こちらが今回の主題になります。VFX(エフェクト)やUIといった半透明を重ね塗ることを前提とした描画が、どれくらいの面積を実際に塗り潰しているのか。その多寡によってゲームのパフォーマンスは大きく変化します。ゲーム中に大量のVFX/パーティクルが乱舞してフレームレートが激減、ガックガクの動作になる...なんてのは日常的に見る風景ですよね。
このようなオーバードローの問題に対して取りうる対応策を考えていきましょう。
Unity(URP)標準のオーバードロー確認ツール
Unity2021以降であれば、RenderingDebugger という標準で組み込まれたツールの中にオーバードローを確認する機能が用意されています。
とても見やすく優秀ではあるのですが、この機能は"DEBUG_DISPLAY"というShaderキーワードによって実現されており、個別のシェーダ全てにそれぞれ(自作のカスタムシェーダであっても)このDEBUG_DISPLAYキーワードに対応する専用描画処理を追加する必要があります。
非常に手間が掛かるし扱いにくいしデバッグも面倒くさそうだな...と訝しんでいたら案の定、URPのUnity公式サンプルの時点でもカスタムシェーダがDEBUG_DISPLAY非対応なものがいくつも見つかりました。当然、オーバードロー表示はバグりまくっており、「まあそうなるよね...」と遠い目になりました。
ShaderGraphで書かれたシェーダであれば自動的にDEBUG_DISPLAYへの対応が行われます(私がShaderGraphを積極的に使うべきだと認識しているのはこういう点からですね)が、それ以外、例えばUI-Defaultシェーダなどでも対応していません。ScreenSpace-Overlayで描かれたUIはそもそもURPを通っていないので仕方ないとしても、URP経由で描かれたUIに関してもオーバードロー確認表示に対応できていません。
以上のように、Unity(URP)標準のオーバードロー確認ツールは便利ではあるけど頼り切るのは厳しいかなあ、というのが私の印象です。
「オーバードロー率計測ツール」の自作
そんなわけで、実際のゲーム開発の現場でも使えるオーバードロー確認ツールを自作します。しました。
そしていきなりですが、ここで私から強く提案したいことがあります。
「オーバードローを数値化しませんか?」
です。いまこのフレームでは画面の何%分を塗っているのか?を常時見えるようにして、その面積、つまり"オーバードロー率"を基準に最適化作業など進めませんか?ということです。
もちろんこの塗り面積 / オーバードロー率という数値が、そのまま描画負荷とリニアなわけではないのは言うまでもありません。リアルなライティングを目指したPBRな3Dモデル向けシェーダと、UIアイコン配置用のシンプルなシェーダでは同じ1ピクセルでも重みは全く異なります。
その辺りはもちろん承知の上で、厳密なパフォーマンス計測が可能な据え置き機ならともかく、モバイル端末のようなパフォーマンス計測手段がチープな環境においては「おおよそ」の負荷が数値化されて目に見て分かることが有効な手段である、と考えています。
世間によくあるオーバードロー確認ツールのような、ただ濃淡の画をぱっとアーティストに見せて「この部分が重なりすぎてるからなんとかしてね」というぼんやりした指示を出すよりも、「いまこのシーンでは3Dのキャラと背景で画面の200%分を塗っています。VFXは最大で1600%塗っています。このゲームは最大で1200%以上をレギュレーション違反としているので、ちょっとVFXの発生数減らして軽くしましょうか」といった、オーバードロー率という具体的な数字で指示を出せた方がアーティストも作業がしやすいでしょうし、プログラマ/アーティストの共通言語としてコミュニケーションが円滑になる、と私は思うわけです。
実装概説
そんなわけで実装についての解説に移るのですが、正直なところ 「オーバードローは数値化して話を進めようぜ!」という話が終わった時点でこのエントリの目的自体は達成しています。
一応これから私が作成/運用しているツールの実装について簡単に解説していきますが、ここから先は蛇足みたいなものです。あと核となる部分はそこまでUnityに詳しくなかった(URPの改造等にも疎かった)数年前に作ったもので、2024年現在となってはより効率の良い手法がある気がしています。ですので、あくまで参考程度にどうぞ。
例によって新しめのUnity+URP、Editor環境でのみの動作を想定しています。実機ランタイムで動かすことも出来るとは思いますが、今のところその必要に迫られたことはありません。将来的に改修を加える可能性はあります。
また要件として、この機能はあくまでデバッグ/パフォーマンスチューニング用途であり、リリース版ランタイムのパフォーマンスをほんの僅かでも悪化させてはなりません。デバッグ用の機能はこれを常に意識しておくことが大事ですね。
処理の流れ
少々ややこしいのですが真面目に解説すると長文になってしまうので、ざっくり端的に説明しますと「オブジェクトの描画にRenderObjectsを用いてStencilアクセスをオーバーライドし、Stencilバッファを単純にインクリメント。最終的にStencilバッファ値の総和を計算し、それを総塗り面積とする」となります。これで伝わる人には伝わるでしょうし、伝わらない人にはこの後の文章を読んでも伝わらない気がします。
-
計測したいカメラをコピーしシーンに配置
この(計測専用)コピーカメラはオリジナルのカメラのパラメータを毎フレーム追従させます。外部コンポーネントは基本不要なので、duplicateするのではなくTransformなど必要なパラメータだけコピーする仕組みの方がベターです。 -
コピーカメラに計測用のDepthStencilなRenderTextureと、オーバードローカウント専用のRenderer(UniversalRendererData)を割り当て
オーバードロー計測にはStencilバッファを利用しますので、その為のRenderTextureを用意し割り当てます(本来であればフレームバッファのDepthStencilをそのまま使うことも出来るはずなのですが、Unity社に確認したところこのDepthStencilを読み出す手段は存在していないということなので断念し、専用のRenderTextureを用意しています)。必要なのはDepthとStencilだけなので、カラーバッファは不要です。 -
RendererFeatureのRenderObjectsを利用して、オブジェクト描画時のStencil操作を「加算合成で+1するだけ」のものにオーバーライド
RendererのFiltering(LayerMask)
はどちらもNothing
とし、RenderObjects
でのみ描画するようにします。また一つのRenderObjects
はOpaque/Transparentのどちらかしか描画することが出来ないので、それぞれ用意します。 -
RendererFeatureにてComputeShaderを起動、Sitencilバッファを読み出しそのバッファ全体の総和を求めGPUリードバック
全てのオブジェクト描画が終わった後、Stencilバッファに書き込まれた値を全て掻き集めます。
StencilバッファをComputeShaderで読み出す、というのが普段あまりやらない少しトリッキーな手段になるかと思いますが、Shader.SetGlobalTexture
にUnityEngine.Rendering.RenderTextureSubElement.Stencil
の引数でRenderTextureを渡せばいけます。ComputeShader内ではTexture2D<uint2>
の.g
要素にStencil値が格納されていますので、これを読み出します。
あとはParallelReductionとかを適当にぶん回して全ピクセルの総和を求めuint[1]
なGraphicsBufferにInterlockedAdd
で書き込み、最後にGrahicsBuffer.GetData
を使ってGPUリードバックを行うことで総塗り面積が手に入ります。まあシェーダ屋さんならこの辺りは呼吸するより簡単ですよね。
注意点
わざわざStencilバッファを利用して集計しているのは、一連の描画処理の中でStencil操作が一番オーバーライドして影響/ダメージが少ないであろうという判断からです。故に、Stencilを扱っているシェーダは破綻し計測した数値が本来のものと比べると多少狂います。同様にRenderObjectsを利用しているので、RendererFeatureを描画順管理などで使っている場合にも諸々破綻します。
ですが、マテリアルを総差し替えするような一般的な手法に比べれば副作用は少ない方だと思いますし、そこまで致命的な差にはならないというのが自分の認識です。
もう一つ、Stencilバッファは8bitなので、同じ座標に0xFF回以上のオーバードローを行った際にはオーバーフローしますが、そんなことを気にするより今日のお昼ごはんを何にするかの方が大事ですから無視しましょう。
あと、わざわざ計測専用コピーカメラを作っているのは、フレームバッファのStencil値を読むことがデフォルトのURPでは不可能だとUnity社から伺ったからなのですが、URPを改造すればその辺りは対処が可能な気がしています。試していませんがどうなんでしょうね。
実装例
こんな感じのウインドウを用意しています。
現在のオーバードロー率と、計測期間中に見つかった最大オーバードロー率を大きく表示しています。この数値が一定ラインを超えると文字が黄色くなり、レギュレーション違反となる数値に到達すると赤くなります。
好きなカメラでの計測が可能になっていたり、UI用に特殊なカメラを用意しているのでそれ専用のオプションを用意していたり、レギュレーション違反が確認されると自動的にポーズが掛かる仕組みを用意したり、色々やってますが細かいことなので解説は割愛します。
この辺りはタイトルの方向性によって必要なものが異なるかと思いますので、お好きに作り変えるのが良いかと思います。
さいごに
今エントリでは、描画パフォーマンスチューニングの重要ポイントであるオーバードローについて、数値化で定量的な判断をしましょうという提案と、その為のツールの実装方法について解説しました。
正直、こういった遠回しな努力をせずにそのまま欲しい負荷情報が手に入るプロファイリング環境が存在する据え置きゲーム機開発は本当に恵まれているよなあ、と羨ましく思ってしまうわけです。
特にモバイル環境は機種ごとのアーキテクチャ差が激しく難しい話ではあるのでしょうけれども、あともう少しくらいはプロファイリング環境が整ってくれてもいいんじゃないでしょうか...なんて...
以上です。何かの参考になりましたら幸いです。