GPU Resident Drawerを有効化したときに、コンソールにこのエラーが表示されたなら、まさにこの記事のために来てくださった方です。
A BatchDrawCommand is using the pass [PassName] from the shader [ShaderName]
which does not define a DOTS_INSTANCING_ON variant.
「何かがおかしいのはわかるが、何がおかしいのかわからない」という状況です。カスタムシェーダーを使うプロジェクトでGPU Resident Drawerを導入するには、必ず乗り越えなければならない関門があります。DOTSインスタンシングのサポートです。
名前にDOTSが付いているのでEntitiesパッケージが必要と誤解しがちですが、まったくそんなことはありません。DOTSインスタンシングはシェーダーレベルのインスタンスデータ受け渡し方式であり、通常のGameObjectベースのプロジェクトにもそのまま適用できます。
目次
- DOTSインスタンシングとは何か
- カスタムシェーダーへのDOTSインスタンシング追加
- ステップ1:pragma宣言
- ステップ2:プロパティブロックの宣言
- ステップ3:SRP Batcher互換性の維持
- ステップ4:Instance IDの伝播
- Project Settingsで必ず確認すべき項目
- 互換性の確認方法
- 実務でよくはまる落とし穴
DOTSインスタンシングとは何か
両者は混同しやすいですが、本質的な違いはデータがどこにあるかです。
従来のGPUインスタンシング(UNITY_INSTANCING_BUFFER_START方式)では、CPUが毎フレーム、インスタンスデータ(トランスフォーム、マテリアルプロパティなど)をConstant Bufferにパックして、GPUに転送します。バッチあたり最大1,023個(Unityの内部上限。インスタンスあたりのプロパティ数によって64KB cbuffer範囲内でさらに少なくなる場合があります)という制約があり、データ転送自体がCPU負荷となります。
DOTSインスタンシングは異なります。インスタンスデータをGPUメモリのGraphicsBuffer(ByteAddressBuffer)に常駐(Resident)させておき、シェーダーにはそのバッファのどこを読めばいいかを示す32ビットのメタデータ値を1つだけ渡します。CPUはデータが変わったときだけアップロードすればよく、毎フレーム繰り返す必要はありません。
| 項目 | 従来のGPUインスタンシング | DOTSインスタンシング |
|---|---|---|
| インスタンスデータの場所 | CPU → Constant Buffer(毎フレーム) | GPU常駐 GraphicsBuffer |
| シェーダーへ渡すもの | プロパティ配列全体 | 32ビットメタデータ値1つ |
| バッチサイズ上限 | 〜500個(64KB制限) | 数千個以上、実質無制限 |
| SRP Batcher互換 | 不可 | 可能(同時サポート) |
| 宣言マクロ | UNITY_INSTANCING_BUFFER_START |
UNITY_DOTS_INSTANCING_START |
GPU Resident DrawerはBatchRendererGroup(BRG)APIを内部で使用しており、BRGはDOTSインスタンシングのみをサポートしています。従来のGPUインスタンシング方式のシェーダーはBRGでは動作しません。
もうひとつ覚えておくべき重要な点があります。DOTSインスタンシングシェーダーはSRP Batcherと同時に互換性を持つ必要があります。 SRP Batcherの互換条件(UnityPerMaterial cbufferの宣言)も同時に満たさなければ正常に動作しません。この両方の条件を同時に満たすパターンがこの記事の核心です。
カスタムシェーダーにDOTSインスタンシングを追加する
ステップ1:必須のpragma宣言
すべてのPass(パス)に以下の3行が必要です。
#pragma exclude_renderers gles gles3 glcore // SM 4.5非対応プラットフォームを除外
#pragma target 4.5 // DOTSインスタンシングの最低要求シェーダーモデル
#pragma multi_compile _ DOTS_INSTANCING_ON // DOTSインスタンシングバリアントを生成
#pragma multi_compile_instancingも合わせて宣言することで、従来のGPUインスタンシングバリアントも同時に生成されます。両方のパスをサポートするシェーダーを作ることが一般的です。
#pragma multi_compile_instancing
#pragma instancing_options renderinglayer
#pragma multi_compile _ DOTS_INSTANCING_ON
注意: このpragmaはシェーダーのすべてのPassに追加する必要があります。 ForwardパスにはあるのにShadowCaster、DepthOnlyパスに漏れると、そのパスでエラーが発生します。エラーメッセージにパス名が明記されるので、どのパスが問題かはすぐにわかります。
ステップ2:DOTSインスタンシングプロパティブロックの宣言
#ifdef UNITY_DOTS_INSTANCING_ENABLEDガードの中にプロパティブロックを宣言します。
#ifdef UNITY_DOTS_INSTANCING_ENABLED
UNITY_DOTS_INSTANCING_START(UserPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor)
UNITY_DOTS_INSTANCED_PROP(float, _Smoothness)
UNITY_DOTS_INSTANCING_END(UserPropertyMetadata)
#endif
ブロック名として使用できる値は3種類あります:
– BuiltinPropertyMetadata — Unity内部専用
– MaterialPropertyMetadata — URP/HRDPビルトインシェーダーが使用
– UserPropertyMetadata — カスタムシェーダーで使用する名前。これを使います。
UserPropertyMetadataを使用することで、Unityの内部シェーダーコードと名前の衝突なく安全にプロパティを宣言できます。
アクセスマクロは3種類ありますが、実際にはWITH_DEFAULTをデフォルトとして使います。BRGはバッファの最初の64バイトを0で予約しているため、プロパティに値が設定されていない場合は0が返されます。_BaseColorで0が返ると黒になりますが、意図しないゴミ値よりもはるかに診断しやすいです。
| マクロ | 動作 | いつ使うか |
|---|---|---|
UNITY_ACCESS_DOTS_INSTANCED_PROP |
インスタンスデータを直接読む | 常にデータがあると保証される場合 |
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT |
データがなければ0を返す | ほとんどの場合、これをデフォルトに |
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT |
データがなければ指定値を返す | 0以外のデフォルト値が必要な場合 |
ステップ3:SRP Batcher互換性の維持とアクセス方法の統一
ここが最も重要な部分です。DOTSパスと非DOTSパス(SRP Batcher)の両方をサポートするには、UnityPerMaterial cbufferとDOTSブロックを同時に宣言し、#defineトリックでシェーダーコードを統一します。
// SRP Batcher用cbuffer — SRP Batcher互換のために必須
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
float4 _BaseColor;
float _Smoothness;
CBUFFER_END
// DOTSインスタンシングパスでは、cbufferの代わりにGPUバッファから読み込むようにオーバーライド
#ifdef UNITY_DOTS_INSTANCING_ENABLED
UNITY_DOTS_INSTANCING_START(UserPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor)
UNITY_DOTS_INSTANCED_PROP(float, _Smoothness)
UNITY_DOTS_INSTANCING_END(UserPropertyMetadata)
// シェーダー本体のコードはそのままにして、プリプロセッサが読み込みパスを切り替える
#define _BaseColor UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, _BaseColor)
#define _Smoothness UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float, _Smoothness)
#endif
#defineによる再定義のおかげで、頂点/フラグメントシェーダーの本体では_BaseColorをそのまま使えます。どちらのパスでコンパイルされるかに応じて、プリプロセッサが自動的にcbuffer読み込みまたはGPUバッファ読み込みに切り替えます。
ステップ4:Instance IDの伝播
構造体と関数に以下のマクロを追加します。
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID // 頂点入力にInstance IDを追加
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID // フラグメントでもDOTSプロパティアクセス時に必要
};
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input); // unity_InstanceIDを初期化
UNITY_TRANSFER_INSTANCE_ID(input, output); // フラグメントへ転送
// ...
}
half4 Frag(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input); // フラグメントでDOTSプロパティを読む前に必須
return _BaseColor; // #defineによりDOTSバッファまたはcbufferから読まれる
}
完成した最小構成のURP Unlitシェーダー
上記のすべてをまとめた実際に動作するシェーダーです。
Shader "Custom/UnlitDotsInstanced"
{
Properties
{
_BaseMap ("Base Texture", 2D) = "white" {}
_BaseColor ("Base Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags { "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
Pass
{
Name "Forward"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma exclude_renderers gles gles3 glcore
#pragma target 4.5
#pragma vertex Vert
#pragma fragment Frag
#pragma multi_compile_instancing
#pragma instancing_options renderinglayer
#pragma multi_compile _ DOTS_INSTANCING_ON
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
float4 _BaseColor;
CBUFFER_END
#ifdef UNITY_DOTS_INSTANCING_ENABLED
UNITY_DOTS_INSTANCING_START(UserPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor)
UNITY_DOTS_INSTANCING_END(UserPropertyMetadata)
#define _BaseColor UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, _BaseColor)
#endif
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
Varyings Vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
return output;
}
half4 Frag(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input);
half4 tex = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
return tex * _BaseColor;
}
ENDHLSL
}
}
}
Project Settingsで必ず確認すべき項目
実装が完了しているのにビルドだけで問題が発生する場合、ほぼ確実にこの設定が原因です。
Edit > Project Settings > Graphics > Shader Stripping > BatchRendererGroup Variants
この値がStrip AllまたはStrip Unusedになっていると、ビルド時にDOTS_INSTANCING_ONバリアントがすべて削除されます。エディターでは問題ないのにビルドだけでエラーになる典型的なパターンがここで発生します。
必ずKeep Allに設定してください。
互換性の確認方法
Frame Debuggerでバッチングを確認
Window > Analysis > Frame Debuggerを開いてドローコールの一覧を確認します。GPU Resident Drawerが正常に動作しているオブジェクトのドローコールは「Hybrid Batch Group」という名前でまとめて表示されます。カスタムシェーダーを使ったオブジェクトがここに含まれていなければ、BRG処理から除外されています。
Rendering DebuggerでGPUオクルージョンカリングを確認
Window > Analysis > Rendering Debugger > GPU Resident Drawerタブ
Occlusion Test Overlayを有効にすると、シーン/ゲームビューにオーバーレイが描画され、どのオブジェクトがGPUオクルージョンカリングの対象かを視覚的に確認できます。カスタムシェーダーのオブジェクトがこのオーバーレイに表示されていなければ、BRGから除外された状態です。
InspectorのSRP Batcher互換パネル
InspectorでMaterialを選択すると、下部に「SRP Batcher: compatible」または「SRP Batcher: not compatible」と表示されます。DOTSインスタンシングシェーダーはSRP Batcher互換も同時に満たす必要があるため、「not compatible」が表示された場合はUnityPerMaterial cbufferの宣言を再確認してください。
実務でよくはまる落とし穴
長年カスタムシェーダーを扱ってきて、またクライアントプロジェクトでGPU Resident Drawer導入を支援する中で繰り返し目撃してきたパターンです。
pragmaを一部のパスにのみ追加する
Forwardパスには追加したのに、ShadowCaster、DepthOnlyパスに漏れるケースが多いです。エラーメッセージにパス名が明記されているので、エラーが出たらまずどのパスか確認してください。
MaterialPropertyBlockの使用
スクリプトからrenderer.SetPropertyBlock(...)を呼び出した瞬間に、そのRendererはGPU Resident Drawerから丸ごと除外されます。個別のプロパティ1つを変えるだけでも同様です。インスタンスごとのプロパティが必要な場合は、DOTSインスタンシングバッファを通じて渡す必要があります。
ShaderGraphの使用
ShaderGraphのSprite Lit / Sprite UnlitターゲットはDOTS_INSTANCING_ONバリアントを生成しません。必ずURP LitまたはURP Unlitターゲットを使用してください。
UNITY_INSTANCING_BUFFER_STARTによるプロパティ宣言
#pragma multi_compile_instancingと#pragma multi_compile _ DOTS_INSTANCING_ONは同じシェーダーに共存できます。問題はpragmaではなく、UNITY_INSTANCING_BUFFER_STARTでインスタンシングプロパティを宣言するときです。こうするとUnityPerMaterial cbuffer構造が壊れ、SRP Batcher互換性がなくなります。プロパティ宣言は必ずDOTS方式(UNITY_DOTS_INSTANCING_START)のみで行う必要があります。
Light Probe Proxy Volumeの使用
LPPVを使用するオブジェクトはGPU Resident Drawerの対象から除外されます。その理由は前の記事、GPU Resident Drawer 動作原理完全分析で解説しています。
まとめ
シンプルなカスタムシェーダーなのにGPU Resident Drawerが適用されず、かなり時間を費やした記憶があります。構造自体は単純です。pragma宣言、プロパティブロック、#defineによる再定義 — この流れを一度理解してしまえば、既存シェーダーへの追加作業は思ったより速く進みます。
ただし、MaterialPropertyBlockへの依存度が高いプロジェクトでは、この部分を先に整理する必要があります。GPU Resident Drawerの性能上のメリットを十分に得るには、インスタンスデータをDOTSインスタンシング方式で渡すフロー全体を理解している必要があります。
GPU Resident Drawerの動作原理が気になる方は、前の記事GPU Resident Drawer 動作原理完全分析を先に読まれることをお勧めします。
← 前の記事: GPU Resident Drawerの動作原理を徹底解説 — Unity 6はどのようにドローコールを削減するのか
さらに深く学ぶために
この記事を書くにあたって直接確認した資料です。
-
DOTS Instancing shaders in URP — Unity 6 Manual
DOTSインスタンシングの全体概念とBRGとの関係をまとめた公式ページ。この記事の「何であるか」セクションの背景資料。 -
Declare DOTS Instancing properties in a custom shader — Unity 6 Manual
UNITY_DOTS_INSTANCING_STARTブロック名3種類(BuiltinPropertyMetadata/MaterialPropertyMetadata/UserPropertyMetadata)の違いを確認した資料。 -
Access DOTS Instancing properties in a custom shader — Unity 6 Manual
UNITY_ACCESS_DOTS_INSTANCED_PROP系マクロ3種類の動作の違いをまとめたページ。WITH_DEFAULTがなぜ安全かをここで確認しました。 -
Support DOTS Instancing in a custom shader in URP — Unity 6 Manual
URP環境でSRP BatcherとDOTSインスタンシングを同時に満たす方法を扱うページ。#define再定義パターンの公式根拠。 -
Best practice for DOTS Instancing shaders in URP — Unity 6 Manual
Shader Stripping設定、MaterialPropertyBlock制限など「ビルドでだけ壊れる」ケースをまとめたページ。 -
Enable the GPU Resident Drawer in URP — Unity 6 Manual
GPU Resident Drawerの有効化条件と除外対象(LPPVなど)を確認した資料。 -
Writing custom shaders for the BatchRendererGroup API — Unity 6 Manual
BRGレベルでシェーダーがどのように動作するかを理解したいときに。GPU Resident Drawerの内部原理に興味があれば前の記事と合わせて読むのに最適です。