【Unity / iOS】シェーダーのConstantBufferを自作する
はじめに
シェーダーのConstantBuffer(CBUFFER)は自分で定義することができます。
CBUFFER_START(MyCustomLight)
float4 _DirectionalLightColor;
float3 _DirectionalLightDirection;
CBUFFER_END
そして、CBUFFERを定義しておくことで、C#から複数のデータをまとめて渡すことができます。
private LightData _myData;
struct LightData
{
public Color lightColor;
public Vector3 lightDirection;
}
public void Setup(ScriptableRenderContext context, CommandBuffer cmd)
{
Light light = RenderSettings.sun;
LightData lightData = default;
// _DirectionalLightColorの設定
lightData.lightColor = light.color.linear;
// _DirectionalLightDirectionの設定
lightData.lightDirection = -light.transform.forward;
// シェーダーに渡す
ConstantBuffer.PushGlobal(cmd, _myData, Shader.PropertyToID("MyCustomLight"));
context.ExecuteCommandBuffer(cmd);
}
今回は、これについて話を掘り下げていきたいと思います。
環境
Unity6000.1.6f1
Universal RP 17.1.0
XCode 16.3 (16E140)
iPhoneX (iOS)
Chapter 1. ConstantBufferを観察する
まず、ConstantBufferへの理解を深めるための実験をしてみましょう。
シェーダー作成
今回はシェーダーに以下のような変数を定義し、iPhoneXで動かした時のメモリを確認してみる事にします。
- ライト関係のシェーダーパラメータをグローバル定義
- マテリアルのパラメータは UnityPerMaterial というCBUFFERに含める
float4 _DirectionalLightColor;
float3 _DirectionalLightDirection;
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
half4 _BaseColor;
CBUFFER_END
シェーダーへのパラメータ設定(C#)
Shader.SetGlobal系のメソッドを利用することで、グローバル変数へ値を設定できます。
Light light = RenderSettings.sun;
Shader.SetGlobalColor("_DirectionalLightColor", light.color.linear);
Shader.SetGlobalVector("_DirectionalLightDirection", -light.transform.forward);
CommandBufferを利用することでも、グローバル変数に値が設定できます。
// CommandBufferを使用する場合
Light light = RenderSettings.sun;
cmd.SetGlobalColor("_DirectionalLightColor", light.color.linear);
cmd.SetGlobalVector("_DirectionalLightDirection", -light.transform.forward);
context.ExecuteCommandBuffer(cmd);
1. シェーダー変数を確認してみる (Xcode)
XcodeでGPUキャプチャを行い、3Dモデルの描画を行っている描画コールを確認します。
UnityPerMaterial
UnityPerMaterial という名前のBufferがあることが確認できます。
UnityPerMaterial をダブルクリックすると、UnityPerMaterial の中身を見れます。
UnityPerMaterial の中には _MainTex_ST
や _BaseColor
の値が入っていることがわかります。
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
half4 _BaseColor;
CBUFFER_END
FGlobals
FGlobalsというバッファがFragmentにバインドされていることもわかります。
fragmentシェーダーからグローバル変数にアクセスすると、FGlobalsというバッファの中に変数が格納されるようです。
FGlobals
を確認してみると、先ほどシェーダー内で定義したグローバル変数の値が入っています。
VGlobals
グローバル変数にvertexシェーダーからアクセスした場合、変数の値はVGlobals というバッファの中に入ります。
VGlobalsの中を見ると、行列値やグローバル変数が確認できます。
2. CBufferを自分で作る
CBUFFERは自分で作ることができます。
これも確認してみましょう。
今回は、MyCustomLight
という名前のバッファを定義してみます。
この名前は別の名前でも構いません。
+ CBUFFER_START(MyCustomLight)
float4 _DirectionalLightColor;
float3 _DirectionalLightDirection;
+ CBUFFER_END
struct Light {
float4 color;
float3 direction;
};
CBufferをシェーダーへ渡す (C#)
ConstantBuffer.PushGlobal
を利用して、構造体をシェーダーに送信します。
- CBUFFERと同じデータを定義したstructを用意して、値を設定する
-
MyCustomLight
という名前を指定して、structをGPUへ渡す
private LightData _myData;
struct LightData
{
public Color lightColor;
public Vector3 lightDirection;
}
public void Setup(ScriptableRenderContext context, CommandBuffer cmd)
{
Light light = RenderSettings.sun;
LightData lightData = default;
lightData.lightColor = light.color.linear;
lightData.lightDirection = -light.transform.forward;
ConstantBuffer.PushGlobal(cmd, _myData, Shader.PropertyToID("MyCustomLight"));
context.ExecuteCommandBuffer(cmd);
}
メモリ確認
再度メモリを確認してみます。
先ほどは FGlobals
というバッファがありましたが、
FGlobals
は消え、代わりにMyCustomLight
というバッファがあることが確認できます。
MyCustomLight
は 28 bytes となっており、FGlobals
(4MiB)より小さいです。
Chapter1. まとめ
- シェーダーの変数を
CBUFFER_START(Name) ~ CBUFFER_END
で囲むと、バッファName
の中に変数が格納される - CBUFFERの中に入れない場合、4MiBのバッファの中に変数が格納される
- vertexからアクセスするシェーダー変数は
VFlobals
に入る - fragmentsからアクセスする変数は
FGlobals
に入る
- vertexからアクセスするシェーダー変数は
Chapter2. パフォーマンス比較
シェーダーのグローバル変数の数が多い場合、
SetGlobal系のメソッドでパラメータを1つずつ渡すより、ConstantBuffer.PushGlobalでパラメータをまとめてシェーダーに渡したほうがパフォーマンスが良くなります。
ここでは、シェーダーのグローバルパラメータが500個あるケースを想定して、
パフォーマンスを計測して比較してみる事にします。
計測にはiPhoneXを使用しました。
検証1 : Shader.SetGlobalFloat vs ConstantBuffer.PushGlobal
以下の2つのケースを比較してみます。
- Shader.SetGlobalFloatを500回実行した場合
- ConstantBuffer.PushGlobalを実行した場合
Shader.SetGlobalFloat を 500回実行 (C#)
Shader.SetGlobalFloatを500回実行します
const int N = 500;
void SetGlobalParameter()
{
for (int i = 0; i < N; i++)
{
Shader.SetGlobalFloat(_properties.PropertyIds[i], globalFloat);
}
}
ConstantBuffer.PushGlobal を 実行 (C#)
floatパラメータを500個定義したstructを作成し、
ConstantBuffer.PushGlobalで設定します。
struct MyData
{
public float _Float0;
public float _Float1;
public float _Float2;
public float _Float3;
...
public float _Float496;
public float _Float497;
public float _Float498;
public float _Float499;
}
private MyData _myData;_
void PushConstantBuffer()
{
_myData._Float0 = globalFloat;
_myData._Float1 = globalFloat;
_myData._Float2 = globalFloat;
_myData._Float3 = globalFloat;
...
_myData._Float496 = globalFloat;
_myData._Float497 = globalFloat;
_myData._Float498 = globalFloat;
_myData._Float499 = globalFloat;
ConstantBuffer.PushGlobal(_myData, bufferID);
}
結果
ProfileAnalyzerを利用して、iPhoneX上でのCPU処理時間を見てみました。
- Shader.SetGlobalFloat を 500回実行したケース : 0.63ms
- ConstantBuffer.PushGlobal を実行したケース : 0.13 ms
CommandBufferの方が0.5msほど高速という結果になりました (約 4.8 倍高速)
検証2 : CommandBuffer.SetGlobalFloat vs ConstantBuffer.PushGlobal
CommandBufferを利用してシェーダーパラメータを設定した場合の処理負荷の違いも確認してみました。
今回は、レンダーパイプライン上で実行してみる事にします。
CommandBuffer.SetGlobalFloat を 500回実行 (C#)
void SetGlobalParameter(CommandBuffer cmd)
{
for (int i = 0; i < ShaderProperties.N; i++)
{
cmd.SetGlobalFloat(_properties.PropertyIds[i], globalFloat);
}
}
ConstantBuffer.PushGlobal を 実行 (C#)
CommandBufferを利用して、 ConstantBuffer.PushGlobalを実行します。
struct MyData
{
public float _Float0;
public float _Float1;
public float _Float2;
public float _Float3;
...
public float _Float496;
public float _Float497;
public float _Float498;
public float _Float499;
}
private MyData _myData;_
void PushConstantBuffer(CommandBuffer cmd)
{
_myData._Float0 = globalFloat;
_myData._Float1 = globalFloat;
_myData._Float2 = globalFloat;
_myData._Float3 = globalFloat;
...
_myData._Float496 = globalFloat;
_myData._Float497 = globalFloat;
_myData._Float498 = globalFloat;
_myData._Float499 = globalFloat;
ConstantBuffer.PushGlobal(cmd, _myData, bufferID);
}
結果
- CommandBuffer.SetGlobalFloat を 500回実行したケース : 0.54ms
- ConstantBuffer.PushGlobal を実行したケース : 0.15 ms
ConstantBufferの方が0.39msほど高速という結果になりました (約 3.5倍高速)
検証3 : CommandBuffer.SetGlobalFloatArray vs ConstantBuffer.PushGlobal (2025/06/11 夜追記)
Floatの配列をシェーダーに渡した時の場合の負荷も見てみました。
private float[] _floatArray = new float[500];
void SetGlobalFloatArray(CommandBuffer cmd)
{
for (int i = 0; i < ShaderProperties.N; i++)
{
_floatArray[i] = globalFloat;
}
cmd.SetGlobalFloatArray(ShaderProperties.FloatArray, _floatArray);
}
結果
- CommandBuffer.SetGlobalFloatArray を 1回実行したケース : 0.02ms
- ConstantBuffer.PushGlobal を実行したケース : 0.13 ms
SetGlobalFloatArrayの方が0.11msほど高速という結果になりました。
Chapter2. まとめ
- SetGlobal系のメソッドを大量に呼ぶと、パフォーマンスの低下につながる
- ConstantBuffer.PushGlobalでパラメータをまとめて設定した方が、パフォーマンスがが良い
2025/06/11 夜追記 : CommandBuffer.SetGlobalFloatArray の方が速い
サンプルコード
Chapter2で使用したコードは以下に置いてます
参考記事
SRPでゼロからレンダリングパイプラインを作成するチュートリアルです。
GenerateHLSLを利用して、シェーダーコードを生成する記事ですが、
その中でConstantBuffer.PushGlobalについても軽く紹介されてます。
Discussion