Shader Graphはノードベースで簡単に高度な表現を実現できる便利なツールですが、数学的な最適化や複雑なループ処理、レイマーチングなどの特殊な描画を行うために、「Custom Function(カスタム関数)」ノードを使用してHLSLコードを直接記述することがよくあります。しかし、カスタムHLSLを書き込んだ途端、マテリアルがSRP Batcherと互換性を失い(Incompatibleになり)、描画負荷が数倍〜数十倍に跳ね上がるという罠が存在します。本記事では、この定数バッファ宣言の破綻メカニズムと、SRP Batcherの高速バッチングを維持したままカスタムHLSLを統合するための正しいコーディング手法について詳しく解説します。

1. SRP Batcherの前提条件:定数バッファ(CBUFFER)の仕組み

まず、SRP Batcherがどのようにしてドローコール(Draw Call)を極限まで削減しているのかを理解することが重要です。従来のレンダリングシステムでは、オブジェクトを描画するたびにCPUからGPUに対して「マテリアルのパラメータ(色、タイリング値、ベクトルなど)」を個別にバインドして転送していました。これがCPUオーバーヘッドの原因となっていました。

これに対し、SRP Batcherは「あらかじめマテリアルのプロパティをGPUメモリ上の定数バッファと呼ばれるメモリ領域(CBUFFER)にすべてまとめて保存しておく」というアプローチを取ります。マテリアルが切り替わっても、CPUはGPUに対して「このマテリアル用のCBUFFERのオフセット位置(アドレス)を切り替えてください」という非常に軽量な指示を送るだけで描画を継続できます。この定数バッファは、シェーダーコード内で必ず以下の形式で定義されなければなりません。

CBUFFER_START(UnityPerMaterial)
    float4 _BaseColor;
    float4 _BaseMap_ST;
    float _Smoothness;
CBUFFER_END

このブロック名が必ず「UnityPerMaterial」であること、そしてマテリアルで変更可能なプロパティがすべてこのバッファに漏れなく定義されていることが、SRP Batcherが機能するための「鉄の掟」です。

2. なぜCustom FunctionノードでCBUFFERが破綻するのか

Custom FunctionノードでHLSLコードを書く際、多くの開発者が犯してしまう最大のミスが、「HLSLコードの中で直接、シェーダープロパティの変数を宣言して使用すること」です。

例えば、Shader Graphのプロパティウィンドウで `_Speed` という変数を定義しているとします。そして、Custom Functionノード内に以下のようなHLSLコードを記述したと仮定します。

// 誤ったカスタムHLSLコードの例
void ApplyWaveEffect_float(float3 InPos, out float3 OutPos)
{
    // Shader Graph側で定義したシェーダープロパティに直接アクセスして宣言しようとする
    // あるいは、別のカスタム定数バッファを独自に宣言してしまう
    OutPos = InPos + sin(InPos.y * _Speed) * 0.1f;
}

このコードをコンパイルすると、Unityのシェーダーコンパイラは `_Speed` というグローバル変数(あるいはローカル変数)がCustom Function内で参照されていることを検知しますが、これが自動生成される `UnityPerMaterial` 定数バッファブロック内に正しく組み込まれません。結果として、`_Speed` は定数バッファの外部の「グローバルスコープ(Global Scope)」に宣言されてしまいます。

このように、マテリアルのパラメータが `UnityPerMaterial` の内側と外側に分散した瞬間、SRP Batcherはそのシェーダーを「不適合(Incompatible)」と判定し、一括バッチ処理の対象から除外します。その結果、同じマテリアルを使用していても、描画バッチがバラバラに引き裂かれ、オブジェクト単位でドローコールが発行される事態になります。

3. 正しいアプローチ:Inputsポートによる完全な値の転送

このバグを防ぐ解決策は非常に単純ですが、徹底する必要があります。それは、「Custom FunctionノードのHLSL内では、マテリアルのプロパティ(変数)を直接参照せず、すべて引数(Inputポート)として受け取る」という設計にすることです。

実装のステップ:

  1. Shader Graphのプロパティの準備: 通常通り、プロパティウィンドウで必要な変数(例: `Speed (_Speed)`)を定義します。
  2. Custom Functionノードのポート設定: Custom Functionノードを選択し、インスペクターの設定ウィンドウを開きます。
    • 「Inputs」リストに新しいポートを追加し、名前を `Speed`(型は `Float`)とします。
    • 「Outputs」リストに必要な出力ポートを追加します。
  3. ノードの接続: Shader Graph上で、プロパティノード(`Speed`)をCustom Functionノードの `Speed` 入力ポートに直接ワイヤーで接続します。
  4. HLSLコードの修正: HLSLコード内では、接続した引数(`Speed`)をそのまま使用し、プレフィックスのアンダースコア(`_`)が付いたプロパティ名(`_Speed`)を直接書くのを完全に廃止します。
// 正しいカスタムHLSLコードの例
void ApplyWaveEffect_float(float3 InPos, float Speed, out float3 OutPos)
{\
    // 引数として受け取った 'Speed' を使用する(_Speedではない)
    OutPos = InPos + sin(InPos.y * Speed) * 0.1f;
}

このアプローチを取ることで、マテリアル変数 `_Speed` の宣言自体は、Shader Graphのジェネレーターが `UnityPerMaterial` CBUFFERブロックの中に自動で正しく出力してくれます。Custom Functionノードにはその値が「引数」として流し込まれるだけになるため、定数バッファの適合性を100%維持したまま、完全にバッチングが適用された超高速な描画が可能になります。