Unity

SRP Batcherの対応方法・利用上の注意点などまとめ【Unity】

【Unity】SRP Batcherの対応方法・利用する上での注意点などまとめ
記事内に商品プロモーションを含む場合があります

UnityのScriptableRenderPipeline(SRP)環境において新しく描画のバッチングシステムであるSRP Batcherが登場しました。

SRP Batcherを使用することで効率良くSetPassCallを削減することができます。最大の特徴として従来は不可能だった別のマテリアルでも同じSetPassになるのは非常に強力です。

しかしながらBuilt-Inパイプラインと同じ考え方で作っているとSRP Batcherが動作せず、パフォーマンスを低下させる罠があります。

この記事ではSRP Batcherを利用する上で気をつけたいことや、シェーダをSRP Batcherに対応する方法について情報をまとめていきます。

以下については知っている前提で話を進めますので、ご了承下さい。

  • シェーダの基礎知識 → 必要なシェーダ知識は当ブログの記事orYoutubeでほぼ全て解説しています
  • DynamicBatching
  • SetPassCall / DrawCall

SetPassCall、DrawCallについては、UnityJapan配信の「パフォーマンスの計測 再入門 〜Unity 2020版〜(5月28日号)」で分かりやすく解説されています。

こちらでSRP Batcherについても少し触れられていました。

SRP Batcherの情報ソース

SRP Batcherの詳細はUnityBlogが分かりやすいです。

公式情報としてはUnityマニュアルが最新です。

内容はUnityBlogとほぼ同じです。

SRP Batcherとは

Built-Inパイプラインではマテリアルが違うと別のSetPassとして扱われCPUパフォーマンスを低下させていました。

そこでマテリアルを使いまわしたり、プロパティをスクリプトから設定する時はMaterialPropertyBlockを使うことで個々にインスタンスが作られることを避けるなどの対策をしていました。

SRP Batcherを使用すると同じシェーダであれば別マテリアルでも同一SetPassで描画してくれます。

あくまでSetPassが減るだけなのでDrawCallの回数自体は変わりません。DrawCallを削減してくれるDynamicBatchingとは役割が異なります。

DynamicBatchingは非推奨に

現行バージョンのUnityでは、DynamicBatchingは非推奨となっており3Dプロジェクトで新規作成するとデフォルトでOFFになっています。

DrawCallを削減する効果は残っているのに非推奨となった理由は、メッシュ結合の処理にかかるコストが大きくDrawCallの処理負荷と大差がなくなってきたためです。

旧バージョンのグラフィックスAPI(OpenGLES2など)ではDrawCall削減による効果の方が大きかったため、DynamicBatchingが有効に働いていたようですが最近はそれほどでもないようです。

2Dプロジェクトでは結合する頂点数が少ないため、現在もDynamicBatchingは有効です。

SRP Batcherを有効にする方法

HDRP、URPを使う場合は、パイプラインアセットの「Advanced -> SRP Batcher」の項目にチェックを付けるだけでOKです。

SRP Batcherの有効化SRP Batcherの有効化

バッチされているかどうか確認する方法

SRP Batcherによってバッチが行われているかどうかは、FrameDebuggerで確認できます。

FrameDebuggerFrameDebugger

「SRP Batch」がSRP Batcherによってバッチされた一連の描画です。

「Draw Calls」の項目を見るとバッチされたメッシュ数が分かります。

バッチされない理由バッチされない理由

下のほうに1つ前のバッチに含まれなかった理由が書かれています。

DynamicBatchingをオフにしても大丈夫か?

現行バージョンでパイプラインアセットを作成するとデフォルトで「SRP Batcher ON」「DynamicBatching OFF」となっていますが、こちらは注意が必要です。

SRP Batcherは後述するようにシェーダの対応が必要で、バッチされない条件がいくつかあります。

DynamicBatchingに依存したプロジェクトでOFFにしてしまうと、パフォーマンスを大きく低下させてしまう恐れがあります。

DynamicBatchingをONにした場合でも、条件を満たしているメッシュはSRP Batcherでバッチされます。SRP Batcherの優先度が高いということですね。

頂点数が少ないメッシュではDynamicBatchingも有効なので、優先度を変更したりDynamicBatchingの対象とする頂点数の上限を変更するような機能が欲しいなとは思いました。

シェーダの対応

ShaderGraphを使う

最も簡単なのはShaderGraphを使うことです。ShaderGraphで作ったシェーダはSRP Batcher対応です。

自作シェーダ(CBuffer対応)

自作シェーダを使う場合はSRP Batcherが動作するように対応します。

当たり前ですが、バッチする必要のないポストエフェクトなどは対応しなくても大丈夫です。

URPでCg言語はNG?

URPではHLSLで書かなければならずCgを使ってはいけないという記載を見かけることがありますが、シェーダによってはCgでも問題ありません。

ただし、例えばライティングに対応したシェーダを本格的に作成するのであればURPのコアライブラリをインポートしないとまともに作成できません。その際、UnityCG.cgincをインポートするとプロパティ名の重複が発生してエラーになります。

ではUnityCG.cgincをインポートしなければOKなのかというとそうでもなく、CGPROGRAM〜ENDCGで書かれている場合はHLSLSupport.cgincというファイルを自動でインポートする仕組みがあるようで同じくエラーが発生します。ライティングを行うシェーダはHLSLへの移行が必須でしょう。

Built-In(Cg) → URP(HLSL)への移行は素晴らしいまとめ(英語)があるのでこちらを参考にすると良いでしょう。

↑2020年12月現在、リンク切れしてます…。以下は中国語のコピー。画像化されちゃってるので検索かけられなくて不便ですが一応使えると思います。

SRP Batcherの対応自体は簡単です。SRP Batcherが有効になる条件は以下の通りです。

SRP Batcher発動条件

Propertiesブロックに定義していて、かつシェーダ内で参照しているプロパティ全てが「UnityPerMaterial」というCBuffer(Constant Buffer)に入っている

要するにマテリアル毎に変更したいプロパティは全て「UnityPerMaterial」という名前のCBufferに含めてくださいね。という事です。

逆にPropertiesブロックに定義していないプロパティをCBufferに含めてはいけません。

マテリアルの利用者には変更してほしくないが、スクリプトからマテリアル毎に変更したい。という場合はHideInInspector属性を使ってInspectorに表示されないように定義しておきましょう。

もう1つの条件としてUnity組み込みのプロパティを全て「UnityPerDraw」というCBufferに含める必要がある。というものがありますがUnity側が用意したコアライブラリをインクルードしていれば特に問題ありません。

プロパティをCBufferに入れる対応について解説していきます。重要なポイントは以下の通りです。

  • テクスチャ&サンプラはCBufferに入れなくてもOK
  • GLES2系ではCBufferは使えない
  • CBufferはSubShader内で1種類しか作れない

コード上の書き方

CBufferに入れるには以下のように書きます。

cbuffer UnityPerMaterial {
    half4 _Color;
};

ただし、これだと先述した通りGLES2系ではCBufferが使えないためエラーとなります。そこでUnity側で定義されているマクロを使います。

CBUFFER_START(UnityPerMaterial) // GLES2系では空行になる
half4 _Color;
CBUFFER_END

これだけでOKです。簡単ですね。

対応がうまくできている場合はシェーダのInspectorで「SRP Batcher」の項目に「compatible」と表示されます。

SRP Batcherが有効SRP Batcherが有効

問題がある場合は、その内容が同じ場所に表示されます。

CBufferはSubShader内で1種類しか作れない

Passが1つしかないようなシンプルなシェーダの場合は大丈夫ですが、実際に運用する上で厄介な問題がこちらです。

「種類」と言っているのは、CBufferに含まれるプロパティの組み合わせのことです。

例えばURPのForwardレンダリングパスである”LightMode”=”UniversalForward”のPassで以下のようなCBufferを定義したとします。

CBUFFER_START(UnityPerMaterial)
half4 _Color;
float4 _MainTex_ST;
half _Cutoff;
CBUFFER_END

次にShadowCasterのPassを実装するとして、色の情報は要らないので消したとしましょう。

CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
half _Cutoff;
CBUFFER_END

これをやってしまうと、SubShader内に2種類のUnityPerMaterialというCBufferが存在することになり、SRP Batcherは無効になります。

そのためCBufferを作成する際は必要なプロパティ全てを必ず含めて作るようにします。

余計なプロパティが混ざっていたとしても、そのPass内で参照されていないならコンパイラが自動で削除してくれるので気にする必要はありません。

URP標準のシェーダを参考にすると良い感じに対応していることが分かります。例えばURPのUnlitシェーダでは「UnlitInput.hlsl」というファイルを全てのPassでインクルードしています。

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"

CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
half _Cutoff;
half _Glossiness;
half _Metallic;
CBUFFER_END

マテリアル毎に変更するプロパティだけをまとめたファイルです。

これを全てのPassでインクルードすれば、CBufferは1種類だけです。基本的にこれと同じ設計で作っていくのが良いでしょう。

ちなみに「SurfaceInput.hlsl」の中にはCBufferに入れなくてもいいテクスチャ&サンプラしか書かれていません。もし他の場所でも「UnityPerMaterial」のCBufferが宣言されていたらCBufferが複数あることになり、SRP Batcherは無効になります。

CBuffer内でシェーダバリアントによる分岐はNG

同じくCBuffer内で分岐を作成してはいけません。

先述した通り、使わないプロパティが混じったとしてもコンパイラによって削除されるので気にしなくてもOKです。

またSRP Batcherを最大限に活用するならバリアントを必要最小限に留めておくほうが良いでしょう。別バリアントは別シェーダとして扱われるため同一バッチになりません。

GPUよりもCPUがボトルネックになっている場合であれば、いっそのことバリアントではなくif文を使ってしまうのもアリかもしれません。

SRPBatcherProfiler

SRP Batcherを利用したドローと通常のドローそれぞれのCPU処理時間を見れるProfilerがGitHubに置かれています。

これをプロジェクトに入れて、コンポーネントになっているのでシーン内の適当なオブジェクトに付けます。

再生中にF8を押すと開きます。

SRPBatcherProfilerSRPBatcherProfiler

使い方はUnityBlogUnityマニュアルに載っています。

F9を押すとSRP BatcherのON/OFFが切り替わると書かれていますが、Unity2019.3で確認した限りでは切り替わりません。

ちなみにスマートフォン実機で表示してみたところ表示はされますが、計測値は全て0になっていました。

サポートされているプラットフォーム

Unityバージョンによって対応状況に違いがありますが、Unity2019.2以上を使っていればほぼすべてのプラットフォームをサポートしています。最新情報はUnityマニュアルで確認をお願いします。

ただしモバイルはOpenGLES3.1以上が必要です。WebGL2.0はOpenGLES3.0で動作するので、WebGLに関してはおそらく非対応でしょう。

XRでSRP Batcherが有効になるのはSinglePassInstancedモードの時のみです。

SRP Batcher非対応環境との両立

モバイルではOpenGLES3.0以前のバージョンが非対応なので、サポート対象としてES3.0以前も含めるなら両立できるよう対応が必要でしょう。

SRP Batcherなしでも十分なパフォーマンスが出せるようなプロジェクトであれば、いっそのことSRP Batcherを使用しないのも良いと思います。

SRP Batcherが有効かどうか取得する

以下のように取得することができます。

GraphicsSettings.useScriptableRenderPipelineBatching

ただし、これはパイプラインアセットに設定された値をそのまま取得するだけです。

非対応なプラットフォームなのかどうかは判定できません。今のところ判定する方法も分かっていないです(もし判定する方法があれば教えて頂けると助かります)。

DynamicBatchingのON/OFFを切り替える

例えばOpenGLES3.0ではSRP Batcherが非対応なのでDynamicBatchingを使用するとしましょう。

変更は以下のように行うことができます(URPの場合)。

if (SystemInfo.graphicsDeviceVersion.Contains("OpenGL ES 3.0"))
{
	var pipeline = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
	pipeline.useSRPBatcher = false;
	pipeline.supportsDynamicBatching = true;
}

このようにパイプラインアセットの設定を直接書き換えます。

設定の書き換えをエディタ上で行ってしまうとアセット側に変更が反映されてしまうので注意して下さい。

useSRPBatcherをfalseにしておけば

GraphicsSettings.useScriptableRenderPipelineBatching

で取得できる値もfalseになるので、SRP Batcherが有効かどうかチェックして処理を切り分けたい時にも使えます。

SRP Batcherの注意点

パーティクルはバッチされない

対象となるのはメッシュかスキンメッシュです。

なおUnityBlogとマニュアルにはスキンメッシュ非対応の記述がありますが、Unity2019.3から対応されています(マニュアルのほうは対応してるって記述と対応してないって記述が両方あってカオス)。

Unity2019.3.0のリリースノートにも「Added Skinned Mesh rendering support in the SRP Batcher.」と記載があります。これより前のUnityバージョンではスキンメッシュも非対応なので注意して下さい。

MaterialPropertyBlockは使用禁止

SRP Batcherが有効であればマテリアルのインスタンスを作っても差し支えないので、インスタンスを作成してプロパティを直接設定するようにします。

ただし先述したようにSRP Batcherが非対応なプラットフォームと両立させる場合は、SRP Batcherが有効な時のみマテリアルインスタンスを作成するといった対策が必要です。

ShaderKeywordに注意

調査した結果、特にバリアントを生成していない場合(shader_feature / multi_compileを使用していない場合)でもToggle属性などでKeywordがマテリアルに付加されると別シェーダとして扱われてしまうことが分かりました。

バッチするかどうかの条件にKeywordの完全一致が含まれているようです。

マテリアルのシェーダを変更・修正して使わなくなったKeywordなどもマテリアル内に残り続けてしまうため注意が必要です。

これはマテリアルのInspector上からは全く判別できないので厄介です。使わなくなったKeywordを一括削除するようなツールを用意しておくと安心です。

僕はこちらのツール内にKeywordの削除機能も組み込んで使っています。

EnumKeyword属性やToggle属性はKeywordの生成を伴うので、なるべく避けた方が良いでしょう。Enum属性であればKeywordを生成しないので大丈夫です。

Toggleについては以下のようなenumで置き換えるといいでしょう。

もしかしたらビルド時に使っていないKeywordは除外してくれるかもしれませんが、エディタ上でバッチを正しく確認できないという点で問題です。

Stats表示のバグ?

SRP Batcherを使用すると「Saved by batching」の値がマイナス表示されます。

マイナス表示されているマイナス表示されている

またDirectX11のみ「Tris」と「Verts」の値にSRP Batcherでバッチされたメッシュがカウントされないバグが発生しています。

フォーラムでDirectX12に変更したら治ったと書いている方がいたので試してみたら治りました。Macの場合は気にしなくても大丈夫ですがWindowsではデフォルトがDirectX11になっているので注意です。

DirectX12を動作させるにはWindows10である必要があります

まとめ

あくまでSetPassを削減できるものでDrawCallは減りませんが、Built-Inパイプラインでは実現不可能だったSetPassの削減策を取れるのはとても便利です。

DrawCallの削減には、使える場面は限られますがGPUインスタンシングを活用するのが良いでしょう。

たくさんのマテリアルを使用せざるを得ないような状況では、SRP Batcherを有効活用していきたいところです。

UnityBlogにも書かれているようにSRP Batcher & DOTS レンダラー(2020年7月現在開発中)の組み合わせによって従来では実現できなかった高速なレンダリングが可能になるとのことで、今後のUnityではSRP Batcherの使用が前提となりそうです。

今後の機能強化も期待できそうなので、楽しみに待つとしましょう(笑)