グラフィックスパフォーマンスの最適化
最適なパフォーマンスのためのキャラクターモデリング

ドローコールバッチング

オブジェクトをスクリーンに描画するために、エンジンはグラフィック API (OpenGL や Direct3D 等) に対してドローコールを発しなければいけません。グラフィック API ではドローコールごとに重要な役割が果たされることから、ドローコールは高負荷で、CPU 側のパフォーマンスオーバーヘッドの原因になります。よくある原因は、ドローコール間でのステートチェンジ (例えばマテリアルの変更など) であり、これがグラフィックドライバでの validation と translation のステップが高負荷になってしまう原因となります。

Unity では、それに対処するために 2 つのテクニックが使用されます。

  • 動的バッチ: かなり小さいメッシュ用に、その頂点を CPU 上に変換して多数の類似したものを群にし、一緒にドローします。
  • 静的バッチ: 静的 (つまり、動かない) オブジェクトを大きなメッシュと結合して、それを高速でレンダーします。

手動でゲームオブジェクトをマージするのに比べ、ビルトインのバッチにはいくつか利点があります。特に、ゲームオブジェクトは依然として個々にカリングされることが可能です。ただし、それには、欠点もあります。静的バッチでは、メモリとストレージのオーバーヘッドを招きます。そして、動的バッチでは、CPU オーバーヘッドがいくらか発生します。

注意 動的バッチはグラフィックスジョブと互換性がありません(Player Settings を参照)。グラフィックスジョブが有効になっている場合、スタンドアロンのビルドでは動的バッチは無効になります。

バッチに必要なマテリアル設定

同じマテリアルを共有しているオブジェクトがバッチング可能になります。そのため、より効果的なバッチングをするには、できる限り違うオブジェクト間で多くのマテリアルを共有する必要があります。

もし 2つのマテリアルがあり、テクスチャでのみ違うだけの場合、2つのテクスチャを組み合わせて 1つの大きなテクスチャに合成できます。この処理は、しばしばテクスチャアトラス化と呼ばれます (Wikipedia の Texture atlases を参照)。いったんテクスチャを同じアトラスにしてしまえば、1つのマテリアルとして使うことができるようになります。

もしスクリプトから共有しているマテリアルプロパティーにアクセスするならば、以下の情報が重要です。 Renderer.material の変更はマテリアルのコピーが生じます。共有しているマテリアルをキープしたいのであれば、代わりに Renderer.sharedMaterial を使いましょう。

シャドウキャストのレンダリングにおいては、仮にそれらのマテリアルが異なっている場合でも、バッチングする事ができます。 Unity のシャドウキャストは、マテリアルが異なっていても、シャドウパスが必要とするマテリアルの値が同じであれば、ダイナミックバッチングを利用する事が可能です。例えば、たくさんの木枠のマテリアルではそれぞれ異なるテクスチャを使うことができますが、シャドウキャスターはテクスチャを考慮せずにレンダリングします。このような場合、互いにバッチングすることが可能です。

動的バッチ

同じマテリアルを共有していて他の条件を満たせば、Unity は自動的に動いているオブジェクトをバッチングします。動的バッチは自動的に処理されるので、何かの手間が必要というわけではありません。

  • 動的なオブジェクトのバッチは 頂点ごとに ある程度オーバーヘッドがあります。ですので、バッチはトータル頂点数が 900 以下のメッシュでしか適応されません。
    • シェーダーが頂点位置や法線や一つの UV 情報を使っていたら、300 頂点までになります。一方で頂点位置、法線、UV0、UV1、タンジェントを使っていたら 180 頂点までになります。
    • 注意: この制限値は今後変更されるかもしれません。
  • トランスフォームにミラーリングを含む場合は、オブジェクトはバッチされません。例えば、+1 スケールのオブジェクト A と –1 スケールのオブジェクト B は一緒にバッチできません。
  • 違うマテリアルのインスタンスを使用すると、たとえ基本的に同じものであっても、ゲームオブジェクトが一緒にバッチングされない原因になります。シャドウキャスターレンダリングは例外です。
  • ライトマップを持つゲームオブジェクトは追加のレンダラーマテリアルパラメーターとしてライトマップのインデックスとオフセット/スケールを持っています。一般的に、動的にライトマップされたゲームオブジェクトは、バッチのためにまったく同一のライトマップの位置を指しています。
  • マルチパスのシェーダーはバッチングを阻害します。
    • ほぼすべての Unity の用意するシェーダーはフォワードレンダリングで複数ライトをサポートしていますが、それらは効果的に追加のパスを使っています。“追加のピクセルライティング” のドローコールはバッチングされません。
    • 旧 Deferred (ライト pre-pass) レンダリングパスは、動的バッチを無効にします。なぜなら、旧 Deferred (ライト pre-pass) レンダリングパスは、オブジェクトを 2回描かなければならないからです。

CPU 上ではオブジェクトの頂点は全てワールド空間の座標で動いているため、 ドローコールよりも処理負荷が小さい場合のみ、有効となります。ドローコールが実際どの程度の負荷になるかは、多くの要素に依存しますが、使われているグラフィックス API によるところが大きいです。例えば、コンソールやアップルの Metal のような現代的な API では通常、ドローコールの処理負荷がかなり低く、動的バッチは全く効果的ではありません。

静的バッチ

一方で静的バッチはどんなサイズのジオメトリでもドローコールを減らすことができます(動かなくて同じマテリアルを共有していることが条件です)。静的バッチは動的バッチよりも顕著に効果的(CPU上で頂点変換を行わないため)ですが、よりメモリを消費します。

静的バッチを利用するには、そのオブジェクトがゲーム中、動いたり、回転したり、スケールしたりすることが ない ということを明示的に設定する必要があります。そのためには、 Inspector の Static チェックボックスをオンにします。

静的バッチを使うと、合成したジオメトリ情報を保存しておくための余分なメモリが必要になります。静的バッチの前に、複数のオブジェクトで同じジオメトリを共有しているのであれば、オブジェクト毎にそのジオメトリのコピーが生成されます(エディター上でもランタイムでも)。このため、いつでも有効な方法とは言えません。つまり場合によっては、メモリ使用量を小さいまま維持するために、静的バッチを避けて、レンダリングのパフォーマンスを犠牲にしなければならないこともあるでしょう。例えば、密集した森林において、木を静的にすると、メモリにはかなりの負担になるかもしれません。

内部的には、静的バッチは各静的オブジェクトの位置座標をワールドスペースに変換し、それらから頂点+インデックスの巨大なバッファを作成しています。その後、同じバッチ内にある可視のゲームオブジェクトについては、ステートの変化がない場合、一連の“低負荷”なドローコールが割り当てられます。技術的に言うと“3D API draw calls”は節約できませんが、それらオブジェクト間で発生するステート変化は節約されます (これこそが計算量の多い部分です)。バッチはたいていのプラットフォームで 64k 頂点数と 64k インデックスに制限されます (OpenGLES では 48k インデックス、macOS では 32k インデックス)。

ヒント

現在、Mesh RenderersTrail RenderersLine RenderersParticle SystemsSprite Renderers だけがバッチ可能です。つまり、Skinned Meshes, Cloth, その他のタイプのレンダリングコンポーネントはバッチできません。

レンダラーは同じタイプのレンダラーとしかバッチできません。

半透明シェーダーでは大抵、透明度を正しくするために、オブジェクトを後方から前方の順番でレンダリングする必要があります。Unity は最初に、この描画順をオブジェクトに指示し、その後、バッチングを試みます。しかし描画順を厳密に適用する必要があるので、大抵の場合、不透明オブジェクトに比べるとバッチングが少なくなります。

互いに接近しているオブジェクトを手動で合成するのも、ドローコールバッチングにはとても効果的な方法です。例えば、引き出しがたくさんある静的な食器棚は、 3D モデリングアプリケーションか Mesh.CombineMeshes を使って、単一のメッシュにまとめてしまう事がよくあります。


  • 2017–10–26 限られた 編集レビュー で修正されたページ

  • ダイナミックバッチングにグラフィックスジョブと互換性ができたことに関するノートは 2017.2に追加

グラフィックスパフォーマンスの最適化
最適なパフォーマンスのためのキャラクターモデリング