【Unity / SRP Batcher】CBUFFER外パラメータによる描画崩れ

に公開

はじめに

SRP Batcherを利用している場合、
CBUFFER_START(UnityPerMaterial) ~ CBUFFER_END で囲った変数は、
マテリアル固有のパラメータとして扱われます。

CBUFFERの外にあるパラメータはグローバルなものとして扱われます。

half4 _Color; // グローバルなパラメータ
CBUFFER_START(UnityPerMaterial)  
float4 _MainTex_ST; // マテリアル固有のパラメータ
CBUFFER_END

この時、マテリアルに対して上記のグローバルパラメータをセットすると、
他のマテリアルの描画に影響を与えてしまいます。

material.SetColor("_Color", color); // 他のマテリアルにも影響を与える

今回の記事では、こちらについて掘り下げていきたいと思います。

環境

【UnityEditor】
Unity6000.1.6f1
Universal RP 17.1.0 (RenderGraphは有効化)

【iOS】
XCode 16.3 (16E140)
iPhoneX (iOS)

【Android】
RenderDoc v1.38
Samsung Galaxy S9 (SM-G960F/DS)
GraphicsAPI : Vulkan

Unityプロジェクト

今回の検証に使用したUnityプロジェクトは、以下のGitHubに置いています。
https://github.com/rngtm/Unity6-ConstantBufferTest

Chapter1. 不具合を確認する

まず、material.SetColorによって他のマテリアルの描画に影響を与えてしまう状況を再現してみたいと思います。

シェーダーの作成

以下のシェーダーを使用します。

Shader "Unlit/NewUnlitShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            half4 _Color;
            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(float4(v.vertex.xyz, 1.0));
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                half4 col = tex2D(_MainTex, i.uv) * _Color;
                return col;
            }
            ENDHLSL
        }
    }
}

マテリアルの作成

2つのマテリアルA, Bを作成します。

A -> Bという順番で描画されたいので、RenderQueueは
A = 2000
B = 2001
としてみました。

マテリアルの適用

オブジェクトを2つ用意し、マテリアル Aと Bを適用します。

コンポーネントの作成

マテリアルに対して、色を設定するようなコンポーネントを作成します。

using UnityEngine;

public class NewMonoBehaviourScript : MonoBehaviour
{
    [SerializeField] private Color _color = new Color(1, 1, 1, 1);
    private Material _duplicatedMaterial;

    private void Start()
    {
        _duplicatedMaterial = GetComponent<Renderer>().material; // マテリアルが複製される
    }

    private void Update()
    {
        _duplicatedMaterial.SetColor("_Color", _color);
    }
}

コンポーネントの追加

先ほど作成したオブジェクトに、上記のコンポーネントをアタッチします。
片方はColorを赤に、もう片方は青に設定してみました。

Unityを再生

Unityを再生すると、両方のSphereが赤くなりました。
material.SetColorで設定した色が、他のマテリアルの描画でも利用されてしまっています。

RenderQueueを変えてみる

マテリアルのRenderQueueを変更してみましょう。
A = 2002
B = 2001
とします。

B -> Aの順番で描画されるようになります。

すると、Bの色(青)がAの描画にも利用されるようになりました。

最初に描画されたマテリアルの_Color が、他のマテリアルの描画にも利用されていることが確認できました。

Chapter1 まとめ

  1. SRP Batcherを使用する際は、マテリアル固有のパラメータは必ずCBUFFER_START(UnityPerMaterial)内で定義する必要がある

  2. CBUFFER外で定義したパラメータは、すべてのマテリアルで共有されてしまう

Chapter2. Xcodeで描画の中身を見てみる (iOS)

シーンをiOS向けにビルドし、iPhoneXで動かし、
GPU System Traceをキャプチャしてドローコールを確認してみる事にします。

RenderLoop.DrawSRPBatcherというドローコールが確認できます。
ここで3Dオブジェクトの描画を行なっているようです。

drawIndexedPrimitivesによって描画が実行されます。

リソースの確認 (UnityPerMaterial)

ここで、drawIndexedPrimitivesにバインドされているリソースを確認してみましょう。

Compute_8 というリソースが、VertexのUnityPerMaterialとしてバインドされていることがわかります。

Compute_8の中を見ると、_MainTex_STが入っていることが確認できます。

UnityPerMaterialというCBUFFERはCompute_***というリソースでメモリ上に確保され、
これがFragmentのUnityPerMaterialパラメータとしてバインドされるようです。

CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
CBUFFER_END

リソースの確認 (FGlobals)

また、ScratchBuffer0_2というリソースがあり、これがFGlobalsパラメータとして、Fragmentのリソースにバインドされていることもわかります。

ScratchBuffer0_2の中を見ると、_Colorが入っていることが確認できます。

_ColorはCBUFFERの中に定義していないため、グローバルなバッファの中に入ります。

half4 _Color; // ScratchBuffer0_2に格納される
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
CBUFFER_END

コマンドの確認

コマンド一覧ビューに戻ると、Compute_8Compute_9を設定しているAPIコール
setVertexBuffersが確認できます。

プリミティブの描画を呼ぶたびに、UnityPerMaterialを更新しているようです

それに対して、ScratchBuffer0_2 (グローバルなバッファ)の更新処理は1度しか呼ばれていません。

Chapter2. まとめ

描画コマンド

  • RenderLoop.DrawSRPBatcherというドローコールで3Dオブジェクトの描画を実行
  • drawIndexedPrimitives を使用してプリミティブの描画を実施

リソース

  • UnityPerMaterial:
    • Compute_8のようなリソースとしてメモリ上に確保される
    • シェーダー内のUnityPerMaterial CBUFFERに対応
    • 例:_MainTex_STなどのマテリアル固有のパラメータを格納
  • FGlobals:
    • ScratchBuffer0_2のようなリソースとしてメモリ上に確保される
    • Fragmentsからアクセスするグローバルなシェーダーのパラメータは、この中に入る
    • CBUFFERに定義されていないパラメータは、この中に格納される
half4 _Color; // ScratchBuffer0_2 のようなバッファに格納される
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST; // Compute_8 のようなバッファ に格納される
CBUFFER_END

Chapter3. RenderDocで描画の中身を見てみる (Android)

UnityをAndroid向けにビルドし、RenderDocでキャプチャしてみます。
端末は Galaxy S9 を使用しました。

Event Browser

Event Browserを確認してみます。
RenderLoop.DrawSRPBatcherというイベントがあります。
ここで3Dオブジェクトの描画を実行しています。

Pipeline State

オブジェクト描画時のパイプラインステートを確認してみます。

FS (Fragment Stage)

FS を確認してみましょう。

1つ目の球と2つ目の球のどちらを描画する時も、Scratchbuffer page というバッファがバインドされており、
Byte Rangeが同じになっています。

Scratchbuffer pageの中身のデータを見ると、_Color が入っていることも確認できます。

half4 _Color; // Scratchbuffer page のようなバッファに格納される
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST; // Buffer1620 のようなバッファ に格納される
CBUFFER_END

VS (Vertex Stage)

Pipeline State の VS を確認してみましょう。

1つ目と2つ目のオブジェクト描画時にバインドされているバッファが異なるものになっています。


Bufferの中を見ると、CBUFFER内部のパラメータが格納されていることが確認できます。

half4 _Color;
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST; // Buffer1620 や Buffer1621 に格納されている
CBUFFER_END

Chapter3. まとめ

  • UnityPerMaterial:
    • Buffer1620のようなリソースとしてメモリ上に確保される
    • シェーダー内のUnityPerMaterial CBUFFERに対応
  • CBUFFER外パラメータ:
    • Scratchbuffers pageのようなリソースとしてメモリ上に確保される
    • CBUFFER内に定義されていないパラメータは、この中に格納される
half4 _Color; // `Scratchbuffers page`のようなリソースとしてメモリ上に確保される
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST; // `Buffer1620` のようなリソースとしてメモリ上に確保される
CBUFFER_END

補足 : CBUFFER外パラメータは、異なるシェーダーバリアント間では共有されない

CBUFFER外で定義したシェーダーパラメータは、異なるシェーダーバリアントを利用するマテリアルでは共有されません。
シェーダーキーワードによる分岐を追加することで確認できます。

Properties
{
    _MainTex ("Texture", 2D) = "white" {}
+    [Toggle(_A)] _IsA("A", Int) = 0
}

...

HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
+ #pragma shader_feature _A
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

記事まとめ

SRP Batcherを利用している場合、

  1. マテリアル固有のパラメータは必ずCBUFFER_START(UnityPerMaterial)内で定義する必要がある
  2. CBUFFER外で定義したパラメータは、同一のシェーダーバリアントを利用する他のマテリアルと共有される

関連記事

https://docs.unity3d.com/ja/2021.1/Manual/SRPBatcher.html

https://zenn.dev/r_ngtm/articles/unity-reduce-constantbuffer

Discussion