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で描画してくれます。
あくまで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の優先度が高いということですね。
頂点数が少ないメッシュでは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属性を使って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」と表示されます。
問題がある場合は、その内容が同じ場所に表示されます。
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を押すと開きます。
使い方はUnityBlogやUnityマニュアルに載っています。
F9を押すとSRP BatcherのON/OFFが切り替わると書かれていますが、Unity2019.3で確認した限りでは切り替わりません。
ちなみにスマートフォン実機で表示してみたところ表示はされますが、計測値は全て0になっていました。
サポートされているプラットフォーム
Unityバージョンによって対応状況に違いがありますが、Unity2019.2以上を使っていればほぼすべてのプラットフォームをサポートしています。最新情報はUnityマニュアルで確認をお願いします。
ただしモバイルはOpenGLES3.1以上が必要です。WebGL2.0はOpenGLES3.0で動作するので、WebGLに関してはおそらく非対応でしょう。
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の使用が前提となりそうです。
今後の機能強化も期待できそうなので、楽しみに待つとしましょう(笑)