BatchRendererGroupを使って大量の物体を効率的に描画する

こんにちは。バーチャルキャストクライアント開発のnotargs(のたぐす)です。今回は技術ブログシリーズの第2回を担当させていただくことになりました。

さて、Unity2019.1からしれっと追加されていたマイナーなAPI、BatchRendererGroupをご存知でしょうか。

このAPIはDOTS/SRP向けに最適化されたメッシュを描画するためのAPIで、Graphics.DrawMeshなどを置き換えるものとして設計されています。

ECS向けの描画APIであるHybrid Renderer V2の基盤としても使われているため、より深く知りたい人はそちらのコードを追ってみてください。

この記事では、BatchRendererGroupの使い方について紹介していきます。

Graphics.DrawMesh系との主な違い

  • DOTSベースでJob/NativeArrayに対応しているため、マルチスレッド・Burstで動かすことができる
  • カメラごとの任意のカリング処理を挟むことができる
  • Updateなどで毎フレーム呼び出す必要がない
  • SRP Batcherフレンドリー
  • ユーザーが直接触ることをあまり想定していないのか、わりと容赦なくUnityがクラッシュする(!)

前提知識

  • Unityの基本的な知識
  • Burst、JobSystemを使ったことがある
  • Graphics.DrawMesh系のAPIを使ったことがある

今回用意したサンプル

この記事では、1000個のMeshがクルクルと回るサンプルを用意しました。

Unity2019.2.8f1、Burst1.1.2を使用しています。

100個のMeshが回るサンプル

こちらがEditor上でのProfilerのスクリーンショットです。

Jobへ処理を逃がしているので、MainThreadへの負荷はほとんど掛かっていないことがわかります。

生成・破棄

それでは、コードを見ていきましょう。

BatchRenderGroupの生成・破棄はシンプルで、newで生成、Disposeで破棄するだけです。

コンストラクタでカリング処理のCallbackが要求されますが、一旦カリングを行わず、全てのオブジェクトを表示する実装を渡しておきます。

Disposeを忘れてしまうと、実行を終了しても処理が残り続けてしまう点に注意しましょう。

Meshを表示する

続いて、Meshをグリッド状に並べてみます。

次がBatchを追加し、Matricesを更新するコードを追記したコードです。

[SerializeField]で公開されているメッシュとマテリアルは、インスペクタからお好みのものを渡してあげましょう。

ここではUnityプロジェクトにデフォルトで入っている「Cube」と「Default-Diffuse」を使用しました。

たくさんのMeshを表示することができました。

たくさんのMeshが表示された

JobSystemを使ってインスタンスを回転させる

ただ動かないMeshが置いてあるだけでも味気ないので、Jobを使って回転させるコードを書いてみましょう。

Matrixを更新するJob

IJobParallelForを使い、それぞれのインスタンスの更新を並列で行うようにしてみました。

呼び出し側(Update)

JobHandleを保持しておき、同時に複数のJobから同じNativeArrayが触られてしまわないように注意しましょう。

それぞれのMeshが回転しました。

回転するたくさんのMesh

カリング処理をもう少し真面目に書く

BatchRendererGroupでは、Graphics.DrawMesh系にない特徴として、「カメラごとの任意のカリング処理を挟み込むことができる」点が挙げられました。

せっかくなので、簡単なカリング処理を用意してみましょう。

カリングを行うJob

CallBack処理

解説

BatchGroupごとにIJobを生成し、CullingContext.cullingPlanesを元にカリングする仕組みにしました。

CullingContext.cullingPlanesからNativeArray<Plane>を取得できるため、それを用いてインスタンスがカリング範囲外かどうかを判定します。

今回は実装をシンプルにするために原点のみを用いてカリングの内外判定を行いましたが、画面端でパッと消えるような挙動になってしまうので、実運用するならMesh.boundsなどを元にカリングを行ってあげると良さそうです。

画面端でパッと消えるような挙動

カリングあり・なしの比較

実際にStatusを表示しながらカメラを動かしてみました。

カリングなしの場合はBatchesが一定ですが、カリングありの場合はカメラに映る物体の数によってBatchesの数が変動している事がわかります。

カリングなし

カリングなし

カリングあり

カリングあり

サンプルコード全文

まとめ

BatchRendererGroupを使うことで、これまで以上に深い部分へのパフォーマンスチューニングが出来るようになりました。

使い方は少し難しいですが、既にUnity標準のAPIとして使える状態になっているため、ぜひパフォーマンスが要求される箇所に活用してみてください!