Optimizing graphics rendering in Unity games 読んだ

Optimizing graphics rendering in Unity games のメモ

https://unity3d.com/jp/learn/tutorials/temas/performance-optimization/optimizing-graphics-rendering-unity-games?playlist=44069

Unity 描画パイプライン

毎フレーム、CPUは以下を実行する 1. シーンにある全オブジェクトをチェックし、レンダリングすべきかどうか判定する。 - view frustum に入ってないオブジェクトを除外する - 他 2. レンダリング対象のオブジェクトのレンダリング情報を収集し、ソートする。 - MeshやMaterial - このフェイズで、特定の状況に当てはまると batchingが行われる 3. CPUは、「batch」と呼ばれるデータを作成し、draw call コマンドを作成。

Draw call のステップは以下に分別される。

  • Set pass call: CPUがGPUに render state を送信する。これを Set pass call と呼ぶ。Set pass call は、初めてのメッシュを描画するときや、render stat (たぶんMaterialの変数とか頂点データ)が変化わったときに送信されるコマンド。つまり実質、GPUへデータを転送しているのはこれ。
  • Draw mesh: CPUがGPUへ、 Set pass call で定義された内容を描画する命令をGPUへ送る
  • 複数パスでの描画必要な場合、都度 Set pass call、Draw call がそれぞれ走る

Set pass call 、Draw mesh、これらをひっくるめて Draw callと呼ぶ。 フレームデバッガなどでは分かれて表示されているときは、実質、GPUへデータを転送しているのは Unityの言うところのSet pass call。CPU負荷があるのもSet pass call。

if our game is CPU bound

CPU バウンドな処理をしらべるとき、まずはどのタスクが原因なのかを特定することが重要。 なぜなら、Unityの描画はマルチスレッドで動作しようとするため、重いスレッドで動いているボトルネックをピンポイントで潰さないと効果がないから。

たとえば、culling operationが遅いとき、Set pass callとか別のスレッドで動いているタスクを減らしても効果がない。

また、ハードウェアによってスレッドの本数や割り当てに違いがあるので、実機で確認する必要がある。

Unityには3種のスレッドの使いわけをする - main Thread - 1本 - 基本的にはゲームロジックが実行される - render thread → - 環境によってはマルチスレッドで動作 - マルチスレッドレンダリングは複雑かつハードウェア依存。 - GPUにコマンドを転送する特別なスレッド - worker thread - culling or mesh skinning その他のタスクを実行する - どのタスクがどのワーカスレッドを実行するかは場合による - たとえば、ハードウェアが複数のスレッドを持っていた場合、ワーカースレッドはその分多く生成される - そのため、実機でプロファイリングすることが重要

(裏技)Graphics jobs でマルチスレッド化

Player Settings の、 「Graphics jobs」にチェックを入れると、Unityがメインスレッドでやっているrender loop をworker thread へ逃がすことができる。 つまり、Camera.render 自体が並列になり、めちゃ速くなるということ。

※ただし まだ実験的な機能 なので注意

CPUの描画ボトルネックのみつけかた

CPUバウンドになる最も一般的なボトルネックは、GPUへのコマンド送信。 このタスクは render threadで実行されると考えて良い。(PS4 とかでは worker threadで実行されるらしい)

最も重い処理は、Set pass call。Set pass callを減らすことはCPU負荷軽減に多くの場合に効果が見込める。 送信するデータの量や数を減らすことも効果がある。別々の render stateなオブジェクトはできるだけ減らす。

SetPass Call 回数の軽減になるテクニックは以下↓

  • レンダリングするオブジェクトを減らす
    • 例) カメラの farClipPlane を調整することで、範囲を調整し、カリングされるオブジェクトを増やす
    • 例) カメラの layerCullDistance を設定することで、レイアごとにカリングされる距離を調整することができる
    • 例) Occlusion cullingをつかう
  • 1つのオブジェクトのレンダリング回数を減らす
    • 例) シェーダのパスを減らす。シャドウマップ、dynamic light 他

バッチング

CPUの世界で同一のオブジェクトを収集し、同一のものとしてまとめてGPUに送る

  • 基本的 に同一のMaterial、(同一のMaterialプロパティ、シェーダ変数、テクスチャ) でないとバッチングされない
  • ほぼ同じMaterialで、テクスチャだけが違う、ていうときは、1つのでかいテキスチャに結合して無理矢理Materialを1つにするというテクニックがある
  • スクリプトで Renderer.material のプロパティを書き換えると、 内部ではMaterialが複製されている。sharedMaterialをつかうと、バッチングされているMaterialが対象になる

関連する参考情報や、関連するテクニック - Unity Manual - Optimizing Unity UI - Unite Bangkok 2015 - GPU Instanting - Texture atlas

カリング/バッチングで逆に遅くなるケース

  • カリングはコスト高。カメラの数 x オブジェクトの数 のオーダーで負荷がかかる
  • 使わないカメラはdisableに。使わないRendererもdisableに。
  • バッチングは、逆に負荷になることもあるので要注意

Skinned Meshes

ボーンアニメーション。 スキンメッシュの描画関係の処理は メインスレッド or ワーカースレッド。これもハードウェアに依る。 Skinned Meshes のレンダリングはコスト高。

以下、改善策。 - 現在SkinnedMeshRendererを使用している全てのオブジェクトに対して、本当に使うべきか再検討する - SkinnedMeshRendererが刺さっているけど、実際にはMeshRendererで事足りるオブジェクトがいないかチェック - 特定の一回しかアニメーションしないようなオブジェクトは、アニメーションが終わったら SkinnedMeshRenderrerをMeshRendererに差しかえる。( SkinnedMeshRenderer.BakeMesh という機能がある - 限られたプラットフォームでは、実験的に GPU skinning がサポートされている

メインスレッドの処理は、描画タスクとは無関係

CPUバウンドだからといって、メインスレッドの処理を最適化しても無意味。 なぜならスレッドが別だから。

if our game is GPU bound

GPUの足をひっぱるのは大抵は fill rate。 特にモバイルはGPUに見合わない解像度があるので問題が大きくなる。

memory bandwidth と、頂点の処理も同様に重要ではある。

Fill rate

Fill rate とは、1秒あたりにGPUが描画しなければいけないピクセル数のこと。

Fill rateが問題になっているかどうかは、 Player Settingsで、Screen Resolutionを減らしてみて、前後のGPU timeを比較することで確認できる

Fill rate問題の改善方法は以下 - フレグメントシェーダを最適化する - オーバードロー(同じピクセルを何度も描画すること)を減らす。でかいオブジェクトが描画された後、その上にさらにでかいオブジェクトが描画されるようなとき、2つぶんのFill rateになる。render queue を適切に管理して、フラグメントシェーダの前にカリングする等が対策 - ImageEffects はFill rateがめっちゃ上がる。ImageEffectsを2つ以上使う場合は倍々でFill rateが上がる

Memory bandwidth

GPUは専用のメモリを持っている。このメモリの read/writeが性能の限界になることがある。テクスチャがあまりにもでかすぎるとか。 Memory bandwidthが問題になっているかどうかは、Quality Settingsからテクスチャの解像度を落としてみて、前後の GPU timeを見比べることで確認できる

Vertex processing