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

DrawCallBatching

ドローコールのバッチ処理

オブジェクトをスクリーンに描画するためには、グラフィックス API (OpenGL や Direct3D 等) に対してドローコールが必要です。グラフィックス API はドローコールごとに非常に多くの処理をするため、ドローコールは高負荷で、CPU 側のパフォーマンスオーバーヘッドの原因になります。これは大抵、ドローコール間に状況の変更 (例えばマテリアルの切り替えなど) が発生することに起因し、グラフィックスドライバーで高負荷な検証と 変換処理の原因となります。

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

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

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

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

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

まとめてバッチ処理できるのは、同じマテリアルを共有しているゲームオブジェクトに限られます。そのため、より効果的にバッチ処理するには、できる限り多くの異なるオブジェクト間でマテリアルを共有するようにします。

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

共有しているマテリアルプロパティーにスクリプトを通してアクセスする必要がある場合は、以下に注意してください。Renderer.material を変更するとマテリアルの複製を作成します。そうでなく、マテリアルを共有したいのであれば、Renderer.sharedMaterial を使います。

シャドウキャスターは、そのマテリアルが異なっていても、レンダリングのときに一緒にバッチ処理を行う事ができます。Unity のシャドウキャスターは、シャドウパスに必要なマテリアルの値が同じである限り、マテリアルが異なっていても動的なバッチ処理を行う事が可能です。例えば、たくさんの木箱には、異なるテクスチャを持つ複数のマテリアルを使用することもあるでしょうが、シャドウキャスターのレンダリングにはテクスチャは関係ありません。そのため、まとめてバッチ処理されます。

動的なバッチ処理 (メッシュ)

ゲームオブジェクトが同じマテリアルを共有し他の条件を満たす場合、動的なゲームオブジェクトは自動的に 1 つのドローコールにバッチ処理されます。動的なバッチ処理は自動的に行われるので、追加の操作は必要ありません。

  • 動的なゲームオブジェクトのバッチ処理では 頂点ごとにある程度オーバーヘッドが発生します。そのため、頂点属性 900 以下で、頂点数が 300 以下のメッシュにしかバッチ処理は適用されません。
    • シェーダーが頂点位置、法線、1 つの UV を使用する場合は、頂点数 300 までバッチ処理できます。また、頂点位置、法線、UV0、UV1、接線を使用する場合は、頂点数 180 までになります。
    • 注意: 属性数の制限は今後変更されることがあります。
  • Transform にミラーリングを含む場合は、オブジェクトはバッチ処理されません。例えば、+1 スケールのゲームオブジェクト A と –1 スケールのゲームオブジェクト B は一緒にバッチ処理できません。
  • 違うマテリアルのインスタンスを使用すると、たとえ基本的に同じものであっても、ゲームオブジェクトが一緒にバッチ処理されない原因になります。シャドウキャスターのレンダリングは例外です。
  • ライトマップを持つゲームオブジェクトには追加のレンダラーパラメーターである Lightmap Index と Offset/Scale が含まれます。一般的に、動的にライトマップされたゲームオブジェクトがバッチ処理されるためには、ライトマップ上で正確に同じ位置を参照する必要があります。
  • マルチパスのシェーダーはバッチ処理を妨げます。
    • Unity のシェーダーはほとんどすべて、フォワードレンダリングでいくつかのライトをサポートし、効果的に追加のパスを加えます。「追加のピクセルごとのライト」のためのドローコールはバッチ処理されません。
    • 古い Deferred (Light pre-pass) レンダリングパスでは、動的バッチ処理が無効です。なぜなら、オブジェクトを 2 回描画しなければならないためです。

動的バッチ処理は、CPU 上でゲームオブジェクトのすべての頂点をワールド空間に変換することによって機能します。そのため、動的バッチ処理の処理負荷がドローコールよりも小さい場合にのみ利点があります。ドローコールが実際どの程度の負荷になるかは、多くの要素によりますが、主に、使われているグラフィックス API によるところが大きいと言えます。例えば、コンソールや Apple の Metal のような最新の API では通常、ドローコールの処理負荷がかなり低く、動的バッチ処理を使用する利点が全くないことがしばしばあります。

動的なバッチ処理 (パーティクルシステム、ラインレンダラー、トレイルレンダラー)

Unity によって動的に生成されるジオメトリを持つコンポーネントの場合、動的なバッチ処理はメッシュの場合と異なります。

  • 互換性のある各レンダラータイプの場合は、Unity はバッチ処理できるすべてのコンテンツを 1 つの大きな頂点バッファーにまとめます。
  • レンダラーはバッチ処理のためのマテリアルの状況を設定します。
  • Unity は頂点バッファーをグラフィックスデバイスにバインドします。
  • バッチ処理の各レンダラーごとに、Unity は頂点バッファーにオフセットを更新します。それから、新しいドローコールを送信します。

グラフィックスデバイスの呼び出しの負荷を計る場合、コンポーネントのレンダリングの最も遅い部分はマテリアルの設定部分です。ですから、共有する頂点バッファーに異なるオフセットでドローコールを送信すると非常に高速です。

このアプローチは、静的なバッチ処理を使用するとき Unity がドローコールを送信する方法によく似ています。

静的なバッチ処理

静的バッチ処理では、静止していて同じマテリアルを共有するジオメトリであれば、どんなサイズのものにでもドローコールを減らすことができます。静的バッチ処理は CPU 上で頂点の変換を行わないため動的バッチ処理よりも低負荷ですが、メモリを多く消費します。

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

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

内部的には、静的バッチ処理は、静的オブジェクトをワールド空間に変換し、それらが共有する頂点とインデックスのバッファーを作成することによって可能です。Player SettingsOptimized Mesh Data を有効にしている場合、頂点バッファーを構築するときにシェーダーバリアントによって使用されていない頂点要素はすべて削除されます。 これを実行するための特別なキーワードチェックがいくつかあります。例えば、Unity が LIGHTMAP_ON キーワードを検出しない場合、ライトマップの UV をバッチから削除します。その後、同じバッチ内の可視のゲームオブジェクトのために、状態の変化はほぼなしで一連の単純なドローコールが実行されます。技術的には、Unity はAPI ドローコール分を節約するのではなく、それらの状態の変更 (これが、負荷の高い部分) 分を節約します。バッチ制限は、ほとんどのプラットフォームでは 64k の頂点と 64k のインデックスです (OpenGLES では 48k のインデックス、macOS では 32k のインデックス) 。

ヒント

現在、メッシュレンダラートレイルレンダラーラインレンダラーパーティクルシステムスプライトレンダラー だけがバッチ処理可能です。つまり、スキンしたメッシュ、クロス、その他のタイプのレンダリングコンポーネントはバッチ処理できません。

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

半透明シェーダーでは通常、透明度を出すためにオブジェクトを後方から前方の順番でレンダリングする必要があります。Unity は最初に、この描画順をオブジェクトに指示し、その後、それらをバッチ処理します。しかし描画順を厳密に適用する必要があるので、大抵の場合、不透明オブジェクトほど多くのバッチ処理はできません。

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


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

  • グラフィックスジョブと互換性のない動的なバッチ処理に関するノートは 2017.2に追加

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