UnityのScriptableRenderPipeline(SRP)環境において新しく描画のバッチングシステムであるSRP Batcherが登場しました。
SRP Batcherを使用することで効率良くSetPassCallを削減することができます。最大の特徴として従来は不可能だった別のマテリアルでも同じSetPassになるのは非常に強力です。
しかしBuilt-Inパイプラインと同じ考え方で作っていると、うまくSRP Batcherが効かずにパフォーマンスが大きく落ちてしまうような罠があります。
この記事では、SRP Batcherを活用する上で気をつけたいことや、シェーダをSRP Batcherに対応する方法について情報をまとめていきます。
以下については知っている前提で話を進めますので、ご了承下さい。
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で描画してくれます。
内部的な仕組みについては高度すぎて僕にはよく分かりませんが、主にCBuffer(ConstantBuffer)を活用して実現しているようです。
あくまで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によってバッチが行われているかどうかは、FrameDebuggerで確認できます。
「SRP Batch」がSRP Batcherによってバッチされた一連の描画です。
「Draw Calls」の項目を見ると、バッチされたメッシュの数が分かります。
下のほうに1つ前のバッチに含まれなかった理由が書かれています。
DynamicBatchingをオフにしても大丈夫か?
現行バージョンでパイプラインアセットを作成するとデフォルトで「SRP Batcher ON」「DynamicBatching OFF」となっていますが、こちらは注意が必要です。
SRP Batcherは後述するようにシェーダの対応が必要で、バッチされない条件がいくつかあります。
DynamicBatchingに大きく依存したプロジェクトでOFFにしてしまうと、パフォーマンスを大きく低下させてしまう可能性があります。
DynamicBatchingをONにした場合でも、SRP Batcherの条件を満たしているメッシュはSRP Batcherでバッチされます。SRP Batcherのほうが優先度が高いということですね。
頂点数が少ないメッシュではDynamicBatchingも活用の余地があるので、優先度を変更したりDynamicBatchingの対象とする頂点数の上限を変更するような機能が欲しいなとは思いました。
シェーダの対応
ShaderGraphを使う
最も簡単なのはShaderGraphを使うことです。ShaderGraphで作ったシェーダはSRP Batcher対応です。
自作シェーダ(CBuffer対応)
自作シェーダを使う場合はSRP Batcherが動作するように対応します。
当たり前ですが、バッチする必要のないポストエフェクトのシェーダなどは対応しなくても大丈夫です。
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が有効になる条件は以下の通りです。
Propertiesブロックに定義していて、かつシェーダ内で参照しているプロパティ全てが「UnityPerMaterial」というCBuffer(Constant Buffer)に入っている
要するにマテリアル毎に変更したいプロパティは全て「UnityPerMaterial」という名前のCBufferに含めてくださいね。という事です。
逆にPropertiesブロックに定義していないプロパティをCBufferに含めてはいけません。
マテリアルの利用者には変更してほしくないが、スクリプトからマテリアル毎に変更したい。という場合はHideInInspector属性を使って定義しておきましょう。
もう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」と表示されます。
問題がある場合は、その内容が同じ場所に表示されます。
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を有効活用するならバリアントも最小限に留めておくほうが良いでしょう。別バリアントは別シェーダの扱いなので、SRP Batcherが効かなくなってしまいます。
GPUコストよりもCPUコストを優先的に落とさないといけない状況なら、いっそのことバリアントではなくif文を使ってしまうのもアリかもしれません。
SRPBatcherProfiler
SRP Batcherを利用したドローと通常のドローそれぞれのCPU処理時間を見れるProfilerがGitHubに置かれています。
これをプロジェクトに入れて、コンポーネントになっているのでシーン内の適当なオブジェクトに付けます。
再生中にF8を押すと開きます。
使い方はUnityBlogやUnityマニュアルに載っています。
F9を押すとSRP BatcherのON/OFFが切り替わると書かれていますが、Unity2019.3で確認した限りでは切り替わりません。
ちなみにスマートフォン実機で表示してみたところ表示はされますが、計測値は全て0になっていました。
サポートされているプラットフォーム
モバイル以外は全く問題ない対応状況です。ただし、Unityバージョンによって対応状況に違いがあるので注意して下さい。
あくまで現在マニュアルに記載されている情報となりますが、以下の通りです。
Windows DirectX11 | Unity2018.2〜 |
---|---|
Windows DirectX12 | Unity2019.1〜 |
XboxOne DirectX11 | Unity2019.2〜 |
XboxOne DirectX12 | Unity2019.1〜 |
PlayStation4 | Unity2018.2〜 |
Vulkan | Unity2018.3〜 |
Metal | Unity2018.3〜 |
Nintendo Switch | Unity2018.3〜 |
OpenGL4.2以上 | Unity2019.1〜 |
OpenGLES3.1以上 | Unity2019.1〜 |
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は一括削除するようなツールを用意しておくと捗りそうです。
EnumKeyword属性やToggle属性はKeywordの生成を伴うので、なるべく避けた方が良いでしょう。Enum属性であればKeywordを生成しないので大丈夫です。
もしかしたらビルド時に使っていない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の使用が前提になっていきそうです。
今後の機能強化も期待できます。楽しみに待つとしましょう(笑)