Unity の URP (Universal Render Pipeline) を使用してモバイルやコンソール向けにゲームを開発する際、描画パフォーマンスの要となるのが SRP Batcher (Scriptable Render Pipeline Batcher) です。SRP Batcher は、同じシェーダーを使用する異なるマテリアル同士の描画処理をまとめ、CPU が GPU に命令を送る際のオーバーヘッド(SetPassCall やドローコール)を劇的に削減する非常に強力な最適化機能です。
しかし、プロジェクトの最適化を進めていると、Frame Debugger やマテリアルのインスペクターで 「SRP Batcher: Compatible: No (SRP Batcher 非対応)」 と表示され、バッチングが機能せずドローコールが跳ね上がってしまう問題に遭遇することが多々あります。
本記事では、SRP Batcher が非互換になる具体的な原因をコードレベルで解明し、手書き HLSL シェーダーや Shader Graph で SRP Batcher 互換を維持するための適切なコーディング規約と最適化アプローチを解説します。
1. 基礎知識:SRP Batcher の仕組みと従来のバッチング(GPU Instancing等)との違い
従来の GPU Instancing や Dynamic Batching は、「同一メッシュかつ同一マテリアル」であるオブジェクトの頂点データをまとめたり、1つのドローコールで一括描画するアプローチでした。そのため、マテリアルのパラメータ(色やテクスチャなど)が少しでも異なると、バッチングが解除されてドローコールが増加していました。
一方、SRP Batcher はアプローチが異なります。メッシュやマテリアルが異なっていても、「同じシェーダーバリアント」を使用していれば、GPU 側の定数バッファ (Constant Buffer) にマテリアルのパラメータを事前にまとめてアップロードしておき、描画時の CPU/GPU 間のコンテキストスイッチを最小化します。
【通常(バッチングなし)】 DrawCall(Material A) ──> SetPassCall(Bind Params) ──> DrawMesh DrawCall(Material B) ──> SetPassCall(Bind Params) ──> DrawMesh ※毎回重いバインドが発生 【SRP Batcher】 GPUメモリ上に定数バッファを確保(マテリアルA, Bのパラメータを保持) CBUFFER: [ Material A Params ][ Material B Params ] └─> CPUはマテリアルの切り替え時に「オフセット値」を伝えるだけになり、バインド処理が極小化!
図:SRP BatcherによるGPU定数バッファを活用した高速なマテリアルパラメータ切り替えの仕組み
これにより、モデルやマテリアルの種類が多くても、シェーダーが共通であればドローコールのオーバーヘッドが極めて低くなります。しかし、これを利用するにはシェーダー側が 「SRP Batcher 互換」 のルールに従って記述されている必要があります。
2. SRP Batcher 非互換 (Compatible: No) になる 3 大原因
SRP Batcher が効かない原因は、主に以下の3点に集約されます。
(1) マテリアルプロパティが UnityPerMaterial 定数バッファ内にない
手書きの HLSL シェーダーで最も多い原因です。SRP Batcher はマテリアルごとのプロパティ(テクスチャのタイリング値、カラー、数値パラメータなど)を UnityPerMaterial という名前の CBUFFER (Constant Buffer) 内に配置することを要求します。
この CBUFFER の外に変数が宣言されていると、Unity はマテリアルパラメータのバインドを一括化できず、非互換(Compatible: No)となります。
(2) テクスチャやサンプラーステートが定数バッファ内に宣言されている
CBUFFER(定数バッファ)に入れられるのは、float や float4 などの数値データのみです。Texture2D や SamplerState などのリソースオブジェクトは、ハードウェアの制約上 CBUFFER 内に含めることはできません。これらを誤って CBUFFER の中に書いてしまうと、コンパイルエラーになるか、SRP Batcher 非互換になります。
(3) MaterialPropertyBlock (MPB) を使用したスクリプト制御
従来の Built-in レンダリングパイプラインでは、個々のオブジェクトの色をスクリプトから動的に変更する際、MaterialPropertyBlock を使用してアロケーションを防ぎつつパラメータを上書きするのがベストプラクティスでした。
しかし、URP / SRP 環境において MaterialPropertyBlock を使用すると、そのオブジェクトは SRP Batcher の対象外となり、個別のドローコールが発生するようになります。これは SRP Batcher の定数バッファによる一括管理の仕組みと競合するためです。
3. 対策コード:手書き HLSL シェーダーの互換化リファクタリング
以下に、SRP Batcher 非互換のアンチパターンコードと、それを互換化するための修正例を示します。
非互換コード(アンチパターン)
Shader "Custom/UnoptimizedShader"
{
Properties
{
_BaseColor ("Base Color", Color) = (1,1,1,1)
_Metallic ("Metallic", Range(0,1)) = 0.0
_BaseMap ("Base Map", 2D) = "white" {}
}
SubShader
{
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// 定数バッファに囲まれておらず、グローバルスコープで宣言されている
float4 _BaseColor;
float _Metallic;
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
// (中略) 頂点・フラグメントシェーダー処理
ENDHLSL
}
}
}
上記のコードは、_BaseColor 和 _Metallic が通常のグローバル変数として宣言されているため、SRP Batcher に対応しません。
互換コード(推奨パターン)
Shader "Custom/OptimizedShader"
{
Properties
{
_BaseColor ("Base Color", Color) = (1,1,1,1)
_Metallic ("Metallic", Range(0,1)) = 0.0
_BaseMap ("Base Map", 2D) = "white" {}
}
SubShader
{
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// 1. テクスチャは CBUFFER の外で宣言する
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
// 2. テクスチャ以外のマテリアルプロパティはすべて CBUFFER (UnityPerMaterial) に入れる
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float _Metallic;
float4 _BaseMap_ST; // タイリング・オフセット値も数値データなのでここに入れる
CBUFFER_END
// (中略) 頂点・フラグメントシェーダー処理
ENDHLSL
}
}
}
このように、CBUFFER_START(UnityPerMaterial) と CBUFFER_END のマクロでプロパティを囲むだけで、シェーダーは即座に SRP Batcher 互換となり、マテリアルパラメータが異なるオブジェクト同士でも同一バッチで高速にレンダリングされます。
4. Shader Graph で SRP Batcher 互換を保つポイント
Unity の Shader Graph を使用している場合、基本的に生成されるコードは自動的に SRP Batcher 互換になります。しかし、以下の操作を行うと非互換になるため注意が必要です。
- 「Custom Function Node」内でのグローバル変数の直参照: カスタムコード内で、Properties セクションで定義されていない外部のグローバル変数や、構造が異なる定数バッファを参照すると互換性が崩れます。パラメータは必ずノードの入力ポート(Input Ports)を経由して渡すようにしてください。
- Hybrid Renderer(DOTS / ECS)向けのカスタムシェーダー設定: ECS 環境で使用する際、
DOTS Instancingを有効にすると、SRP Batcher ではなく DOTS のインスタンシングバッファが優先されるため、別形式の互換性チェックが必要になります。
5. スクリプト側での動的パラメータ変更の最適化(MaterialPropertyBlockの代替)
マテリアルの色をスクリプトからリアルタイムに変動させる場合、従来の MaterialPropertyBlock に代わるアプローチが必要です。
アプローチ1:1つの汎用マテリアルからインスタンスを作成し、バッチを分ける(許容できる場合)
オブジェクトが数点程度で、かつ描画順序が近い場合、renderer.material.color = color のように直接書き換えることでマテリアルインスタンスが生成されます。これは SRP Batcher によってバッチングはされませんが、コードは簡潔になります。ただし、数千個のオブジェクトでこれを行うとメモリの浪費とドローコールの爆発を招きます。
アプローチ2:GPU Instancing を併用する(同一メッシュの場合)
同じメッシュ(例:大量の雑魚敵、フィールド上の草木)で、色だけをランダムに変更したい場合は、SRP Batcher よりも GPU Instancing の方が適しています。シェーダー側に #pragma multi_compile_instancing を記述し、スクリプトから MaterialPropertyBlock を介して描画することで、GPU インスタンシングによる1ドローコールでの描画が可能です。この際、SRP Batcher はそのマテリアルに対して一時的にバイパスされますが、トータルのレンダリングコストは下がります。
アプローチ3:頂点データ(Vertex Color)やテクスチャ配列の活用
頂点カラーに動的な色の値を書き込んでおく、あるいは複数の色パターンを Texture2DArray(テクスチャ配列)にまとめ、マテリアルプロパティ側ではなく頂点データ側やUVインデックスで描画色を決定することで、マテリアルパラメータ自体を一切変更せずに、単一マテリアルとして SRP Batcher の恩恵を最大化できます。
6. まとめ
- SRP Batcher は「シェーダーが同じ」ならマテリアルが異なってもバッチング可能: 従来のバッチングの限界を超え、コンテキストスイッチを劇的に削減します。
- 定数バッファ
UnityPerMaterialの記述を徹底する: 自作 HLSL シェーダーでは、数値プロパティ(カラー、数値、ベクトル)を必ず CBUFFER マクロ内に配置します。 - MaterialPropertyBlock の多用に注意: URP 環境で
MaterialPropertyBlockを使うと SRP Batcher が解除されます。動的パラメータ変更には頂点カラーやテクスチャ配列の活用、あるいは GPU Instancing への切り替えを検討しましょう。