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

ドローコールバッチング

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

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

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

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

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

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

もし、テクスチャだけが違う 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 チェックボックスをオンにします。

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

Internally, static batching works by transforming the static GameObjects into world space and building one shared vertex and index buffer for them. If you have enabled Optimized Mesh Data (in the Player settings) then Unity removes any vertex elements that are not being used by any shader variant when building the vertex buffer. There are some special keyword checks to perform this; for example, if Unity does not detect the LIGHTMAP_ON keyword, it removes lightmap UVs from a batch. Then, for visible GameObjects in the same batch, Unity performs a series of simple draw calls, with almost no state changes in between each one. Technically, Unity does not save API draw calls, but instead saves on state changes between them (which is the resource-intensive part). Batch limits are 64k vertices and 64k indices on most platforms (48k indices on OpenGLES, 32k indices on macOS).

ヒント

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

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

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

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


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

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

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