RenderDoc Meta Forkを使用したアプリの最適化 - パート2
このガイドでは、アプリの最適化に利用できるRenderDoc Meta Forkの主なシナリオをいくつか詳しく紹介します。ガイドのこの部分では、容易に識別できるレンダリングパターンに焦点を当てます。これは、レンダリングスレッドとGPUの両方を最適化できるチャンスを見極めるのに役立ちます。このガイドの目的は、それらの問題点を特定し、それを緩和する方法を示すことです。
フォワードレンダリング
フォワードレンダリングでは、ドローコール(描画呼び出し)を通じてディスパッチするメッシュを単に描画し、フレームバッファに直接レンダリングします。ライティングはフラグメントや頂点シェーダーの中で計算され、その発行元ドローコールの中でモデルに適用されます。この方式のデメリットは、ダイナミックで重要なライティングにおいて、追加のパスが必要になる可能性があることです。GPUの負荷は、各モデルのジオメトリ量、必要な描画パスの数、ライティングモデルの複雑さによって決まります。一方、遅延レンダリングでは、ライティングは画面空間で行われます。
遅延レンダリング
基本的に遅延レンダリングでは、ライティング計算が基本パス完了後まで遅延されることになります。遅延レンダリングの基本パスは、Gbuffer (geometric buffer)という複数のレンダリングターゲットに書き込むという点で、フォワードレンダリングの基本パスとは異なっています。Gbufferには、albedo (色)、法線、粗度値など、グラフィックパイプラインを通じてモデルを再実行することなくライティングを計算するのに必要なものを定義する、複数のレンダリングターゲットが含まれています。これらの値すべてを書き込んだ後の次の段階は、それ以前に生成されたGbufferを使用して画面空間内でライティングパスを実行することです。このモデルのメリットは、ライティングの計算が画面スペース内で実行されるため、ジオメトリの複雑さがライティングから切り離されることです。遅延レンダリングに伴う問題点は、Gbufferの中にどれだけのレンダリングターゲットがあるとしても、ライティングパスの段階でそのすべてを解決する必要があり、そのためだけにフレーム想定量の半分近くが消費される可能性があるということです。これには、高解像度のMeta Quest画面上ではピクセルごとにライティングを計算する必要があるためにリソースを多量に必要とするライティングパスも含まれていません。
上記のRenderDocスクリーンショットは、遅延レンダリング方式を使用したゲームのキャプチャを示しています。矢印は、個々のドローコールごとに描画されるレンダリングターゲットそれぞれを示しています。次にそれらのレンダリングターゲットが、ライティング計算の入力テクスチャーとして使用されます。比較のため、下記に示すフォワードレンダリング方式を使用したゲームのRenderDocキャプチャをご覧ください(バインドされたアイバッファの1つのセット)。
ドローコールごとに生成されるシェーダーバイトコードは、RenderDocの中で表示可能なので、そのシェーダーのコンパイル結果が正確にどうなるかを確認できます。最適化を行うには、ジオメトリ数が少なく、ピクセルカバレッジも大きくないにもかかわらず、処理時間が比較的長くかかっているドローコールに注目するのが効果的です。RenderDocを使用すればするほど、どこを調べればよいか見当を付けやすくなります。開発者によれば、最適なパフォーマンスを維持したまま望ましい外観を達成するためには、一般にシェーダーの命令数は25~50が最適な範囲とされています。ただし、複雑な数学命令の量、テクスチャー検索、そしてピクセルカバレッジの可能性を考慮する必要があります。一般に、一括処理を最大限に増やし、setpass/パイプラインの変更を最小限に抑えるため、作成するuberシェーダーのセットに制限を設けることをおすすめします。付随的な利点として、一度にごく少数のシェーダーのセット用の生成コードを考慮するだけで良いということがあります。uberシェーダーの限定セットを使用する場合、その少数のシェーダーのパフォーマンス向上が、該当する論理グループ内のすべてのメッシュに伝播します。例えば、対象のゲームの舞台が1つの都市である場合、ビル、コンクリートの歩道、屋根などのために1つの環境シェーダーを用意し、スキンキャラクターのためにもう1つの環境シェーダーを用意することができます。すると、パイプラインの状態によって破壊される心配をすることなく、容易に複数のメッシュをマージして、メッシュLOD (詳細レベル)やHLOD (詳細の階層レベル)を作成できるようになります。
役立つヒントとして、すべての物体に異なる入力テクスチャーを使用することができますが、同じシェーダーを使用する物体のうち、できる限り多くの物体にテクスチャー配列と1つのルックアップIDを使用する(またはテクスチャーアトラスを作成する)ことにより、確実にsetpassを最小化し一括処理を最大化することができます。
RenderDocを使用して、現在選択されているドローコールの入力テクスチャー/ユニフォームを見る方法を次に示します。
LOD (詳細レベル)システムを利用すると、近くで見た場合の複雑で詳細なメッシュを、その同じモデルを遠くから見た状態の近似となる低レベルのジオメトリメッシュと差し替えることができます。優れたLODでは、スケールが同じである限り、距離が遠くなるにつれてメッシュがカバーするピクセル数が減るため、モデルの切り替え時にポッピングが発生しているようには見えません。実際に画面上で数ピクセル程度しか占めない場合、500ワールドユニット離れた像の10万頂点メッシュをレンダリングする必要はありません。最新の商用ゲームエンジンに含まれる組み込みLODシステムを十分に活用してください。
LODシステムの健全性チェックを実施することによって、遠景クリップ平面の近くに表示される遠くの物体で、関連する最低のLODが使用されるようにすることができます。このためには、RenderDocで頂点カウントを調べてその物体のピクセルカバレッジに一致させます。その上で、それらの問題をアセットまでトレースして遡り、アセットを差し替えるための適切なカメラ距離をLODが使用するようにできます。
下記の画像に示されているシーンでは、カメラから非常に遠く離れた所に、幾何学的に入り組んだ暖炉のモデルが配置されています。
HLOD (詳細の階層レベル)システムは、LODシステムを拡張したものであり、大量の遠景オブジェクト群のためのドローコールを節約するのに役立ちます。以下、例を使ってわかりやすく説明します。
大都市をレンダリングする場合に、物体ごとに独自のドローコールを行ってレンダリングするブロックが数多く存在するとしたらどうでしょうか?遠くのブロックに含まれるのは都市のブロック全体を構成する各メッシュの最下位のLODだとしても、低レベルのLODをそれぞれ単に描画するだけでもドローコールの回数が膨れ上がってしまいます。HLODシステムを利用すれば、遠景ブロックの最下位LODのメッシュすべてをオフラインでマージし、1回のドローコールでブロック全体をまとめてレンダリングするようにエンジンに指示することができます。テクスチャーアトラスまたはテクスチャー配列ルックアップを(前述したとおり)使用するuber環境シェーダーがあれば、メッシュをマージしてUVをマッピングし直すだけで済むことを覚えておいてください。HLODシステムのデメリットとしては、消費メモリが多くなることがあります。これは、実質的には1つのHLODを構成する低レベルのLODすべてについてジオメトリが複製されるためです。したがって、使用可能なメモリが十分あることを確認してください。
不要なドローコールをスキップしてゲームのレンダリングをできるだけ効率化するためにエンジンが使用するドローコール/カリングシステムには、いくつかのタイプがあります。このセクションでは、主なタイプについて説明します。
フラスタムカリングは、カメラフラスタムの外にはみ出たドローコールをスキップする手法です。フラスタムの外にある物体は画面空間のどのピクセルにも投影されないため、ドローコールを破棄しても安全です。これにより、ジオメトリ処理のためのGPUの負荷や無意味なドローコール設定のためのCPUの負荷を回避できます。開発者は、フラスタムサイズ(近景/遠景クリップ平面+FOV)を判別するためのパラメーターをいくつか設定することができます。しかしMeta Questでは、それらをOVRCameraRigのデフォルト設定のままにしておくのが最善です。快適さを維持する上でこれは重要です。
RenderDocでは、[Mesh Viewer (メッシュビューアー)]タブを使用することにより、入力メッシュおよび生成されたバウンディングボリュームを確認できます。さらには、VS出力セクションを選択することにより、メッシュが画面空間にどう投影されたかを確認することもできます。
距離描画カリングは、カメラから一定の距離離れている物体の描画をスキップする手法です。フラスタムカリングのセクションで説明したように、遠景クリップ平面より遠くにある物体については、エンジンが自動的にスキップします。しかし、特定の物体について、カメラフラスタム内の少数のピクセルしかカバーしない小さい物体のドローコールは回避するほうが良い場合もあります。そのような視覚的に影響の少ないドローコールをスキップすることによりメイン/レンダリングスレッドのCPU時間を節約することができ、それらのオブジェクトのジオメトリ処理を回避できます。LODのセクションの暖炉の例にこれを適用して、ゲームのコンテキストに影響しないメッシュを無効にすることができます。
これは、カメラフラスタム自体の内部で行われるカリング手順です。オクルージョンカリングは、別のメッシュの背後に完全に隠れているメッシュのドローコールをスキップするという考えに基づいています。その一般的な例として、森の中に窓のない小屋があり、その入口が閉まっていて、プレイヤーがその小屋の外にいる場合を想定します。そのような状況では、小屋の中にあるものをレンダリングする必要はありません。まずCPUで計算を行いどの物体が他の物体の背後に隠れるかを判別してから、レンダリングスレッドの設定を開始できます。こうすれば、余分なドローコールやGPUでのジオメトリ処理時間を節約できます。オクルージョンカリングについては、レンダーキューにも関係があるため、そのセクションでもすでに言及されています。z排除ではジオメトリが処理されてメッシュのドローコールが追加されます(オクルージョン対象のピクセルではフラグメントシェーダーが実行されない)。それに対して、オクルージョンカリングではオクルージョン対象メッシュの全体(ドローコール準備、ジオメトリ処理、フラグメントシェーダーの全操作)が節約されます。この2つの手法の主な相違点はここにあります。
RenderDocでは、フレーム内でレンダリングされるすべてのオブジェクトを確認できるため、オクルージョンカリング手法を実装することでゲームにメリットがあるかどうかを、状況に応じて判断することができます。下記のスクリーンショットは、最終的に完成したレンダリングターゲットを示します。その後に、ドローコールを順次実行したRenderDocの、フレームの20%ほどに達した段階のスクリーンショットが示されています。第2のスクリーンショットでは、描画不要の多くのオブジェクトが、小屋の中にレンダリングされているのをはっきりと確認できます。
オクルージョンカリングのアルゴリズムによってCPUで行われる計算には、オクルージョン対象メッシュを判別するための処理能力が必要になりますが、ゲームのタイプや状況によってはそれを行うだけの価値があります。選択できる手法は数多くあり、UnityとUnrealの両方のエンジンには、タイミングや効率のテストのために試すことができる組み込みのソリューションが含まれています。どれがうまく動作するかは状況に応じて異なるため、使用する実装、または作成する実装を選択する際には、シーンのレイアウト/プレイスタイルを考慮するようにしてください。
Unityでは、可能であればDOTSを利用するようにしてください。これは非常に並列化しやすい処理で、数学的な要素が多いため、非常に大きな効果が期待できます。
Unrealエンジンでは、マルチコアの使用によるメリットを得るため、並列コンテナーを活用するようにしてください。
カリングシステムの最適化に関する一般的なアドバイス
前述のすべてのカリングシステムをできるだけ効果的な方法で用いるために、バッチにジオメトリの希薄な領域がないようにし、バッチに含まれるジオメトリの範囲が大きくなり過ぎないようにしてください。幾何学的密度の高い複数のメッシュをマージして1つの領域に集中させた後、細長いメッシュがいくつか飛び出ている場合は、バッチ用に生成されるバウンディングボリュームが非常に大きくなるため、カリングアルゴリズムで誤検出が多く発生してしまいます。このため、バッチ内のすべてのジオメトリが、場合によっては画素をほとんど、あるいはまったくカバーしなくなります。メッシュバッチの周りに生成されるバウンディングボリュームは、可能な限り密集していることを確認してください。この巨大なメッシュを1つのガラスの箱に入れるとして、一体どれだけの空き空間ができるだろうかと考えてください。空き空間は常に可能な限り少なくし、必要なら別々のバッチに分離するようにします。
以下の画像はジオメトリがそれほど多くないのであまり顕著な例ではありませんが、送信されたバッチを確認してそれを分割する可能性について調べる方法を示しています。この画像は単一のバッチ描画のものです。ただし、プレイヤーが(ワールド空間内の)バウンディングボリュームの真ん中に立っている場合には、これをバッチ1とバッチ2に分割してジオメトリ投影にかかる負荷を削減するという方法も考えられます。結局のところ、バウンディングボリュームのクラスター内に複数の物体が密集して存在する場合には、こちらのほうがずっと重要となります。
素材に透明なブレンド状態を使用している場合、状況によっては物体を完全にフェードアウトしなければなりません。物体が完全にフェードアウト(alpha = 0)した後、開発者がメッシュレンダリングコンポーネントを無効にするのを忘れることがあります。これは負荷に影響することがあり、その度合いは軽度(小さな消耗アイテムのフェードアウトなど)から、重度(全画面ビネット効果のフェードインとフェードアウトなど)まで、さまざまです。物体が完全にフェードアウトした後もメッシュレンダリングコンポーネントが有効になっていると、シーンの背後にあるドローコールはエンジンによって自動的にスキップされるように思えるかもしれませんが、エンジンには開発者の意図がわからないので、実際には自動処理されません。したがって、ドローコールはピクセルをまったく更新せずにグラフィックパイプライン全体をすべて実行することになります。そのためCPU上のドローコールとGPU上のすべてのレンダリング処理が無駄になってしまいます。ピクセルシェーダーの中には、alpha = 0の場合に早期に条件チェックをするものもありますが、それでもジオメトリ処理のため貴重なGPU時間が無駄になります。
要するに、フレームの中で最終的な色バッファに関係しないドローコールを検出し、それらの描画操作をエンジンにトレースバックするようにしてください。これは、スクリプト/ブループリントコードの中でalpha = 0の場合に、関連するレンダリングを無効にするシンプルな条件チェックとなります。
下記のスクリーンショットは、透明な素材でできた、引き延ばされた立方体を示しています。拡散テクスチャーalphaチャネルを通じてalpha値を下げることにより、物体がフェードアウトします。
RenderDocでは、この完全に透明なオブジェクトもレンダリングされることを確認できますが、alpha値が0であるため色はまったく変化しません。
UnityとUnrealのエンジン内では、完全に透明になる物体に関連するレンダリング機能をオフにしてください。
レンダリングアーティファクトを見つけた場合、その問題の原因となっているドローコールを特定することで、検索範囲を絞り込むことができます。RenderDocの.rdcファイルのもう1つのメリットは、それをキャプチャしたデバイスと同じgfxドライバーバージョンを使用している任意のMeta Quest上で再実行できるという点です。これは、QA担当者がパフォーマンスの問題個所を特定し、それをキャプチャしてグラフィックスやパフォーマンスの担当者に提供する場合に非常に役立ちます。