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

ドローコールバッチング

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

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

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

手動でオブジェクトを統合するのに比べ、ビルトインのバッチにはいくつかの利点があります (もっとも注目するのは、オブジェクトがまだ、個別に抜粋できることです)。しかし、いくつかの欠点もあります (静的バッチはメモリーとストレージのオーバーヘッドを生じさせ、動的バッチはいくつかの CPU オーバーヘッドを生じさせます)。

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

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

もし 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 インデックス)。

ヒント

現在は、メッシュレンダラー のみがバッチングされます。すなわち、スキンメッシュ、クロス、トレイルレンダラーおよびその他の種類のレンダリングコンポーネントはバッチングされません。

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

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

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