Version: 2023.1
言語: 日本語
描画コマンドの作成
レンダリング統計ウィンドウ

DOTS Instancing シェーダー

BatchRenderGroup (BRG) は、多数のインスタンスの効率的なレンダリングを行うために、DOTS Instancing (DOTS インスタンシング) と呼ばれる新しいシェーダーインスタンシングモードを使用します。BRG が使用する全てのシェーダーは DOTS Instancing をサポートしている必要があります。従来のインスタンス化されたシェーダーでは、シェーダーは、定数バッファあるいはユニフォームバッファ内のインスタンス化されたプロパティごとに 1 つの配列を渡され、各配列内の各要素が、描画内の 1 つのインスタンスのプロパティ値を含みます。DOTS Instanced シェーダーでは、Unity は DOTS Instanced プロパティごとに 1 つの 32 ビット整数をシェーダーに渡します。この 32 ビット整数はメタデータ値と呼ばれます。この整数は何でも表せますが、通常は、シェーダーがレンダリングするインスタンス用にプロパティデータをロードする (ロード元の)、バッファ内のオフセットを表します。

従来のインスタンシングと比較して、DOTS Instancing には多くの利点があります。これには例えば以下が含まれます。

  • インスタンスデータが GraphicsBuffer に格納されて GPU 上で永続的に保持されるので、Unity はインスタンスをレンダリングするたびにそれを再設定する必要がありません。インスタンスが実際に変更される時にだけデータ設定を行うことで、インスタンスデータがほとんど (あるいは全く) 変更されない場合に、パフォーマンスを大幅に向上させることができます。これは、エンジンがフレームごとに全インスタンスの全データを設定しなければならない従来のインスタンシングよりも、はるかに効率的です。
  • インスタンスデータの設定の処理が、ドローコールの設定と別になっています。これにより、ドローコールの設定が軽量かつ効率的になります。BRG では、(各ドローコール用の処理を最小限に抑えた) スクリプタブルレンダーパイプライン (SRP) バッチャーの特殊な高速パスによって、これが可能になっています。開発者が自分でこの処理 (作業) を行うことになるので、各描画呼び出しで何をレンダリングするかを、より自在に制御できます。
  • ドローコールのサイズが、定数あるいはユニフォームバッファに収まるインスタンスデータの量によって制限されることがなくなります。このため BRG は、1 回のドローコールでより多数のインスタンスのレンダリングを行えます。
    ノート: 各インデックスがデータを必要とすることは変わらないので、インスタンスインデックスの数によるドローコールサイズの制限はなくなりません。ただし、インデックスのメモリ消費量は、インスタンス化されたプロパティの完全なセットのメモリ消費量よりもはるかに少ないので、定数あるいはユニフォームバッファ内に収まるインスタンス数が大幅に多くなります。例えば、各インデックスは 16 バイトを必要とするので、特定のプラットフォームでバッファごとのメモリ制限が 64 キロバイトであれば、4096 個のインデックスがバッファに収まります。
  • 特定のプロパティに関して全てのインスタンスが同じ値を使用する場合、全てのインスタンスにメモリ内の同じ場所から値をロードさせることができます。これにより、インスタンスごとの値の複製に費やされるメモリと GPU サイクル数を節約できます。

DOTS Instancing のサポート

シェーダーで DOTS Instancing をサポートするには、以下が必要です。

  • シェーダーモデル 4.5 以降を使用する必要があります。#pragma target 4.5 以上を指定してください。
  • キーワード DOTS_INSTANCING_ON をサポートする必要があります。これを #pragma multi_compile _ DOTS_INSTANCING_ON を使用して宣言してください。
  • 最低 1 つのプロパティを含む、DOTS Instanced プロパティのブロックを、少なくとも 1 つ宣言する必要があります。詳細は DOTS Instanced プロパティの宣言 を参照してください。

ノート: Shader Graph と、Unity が URP と HDRP で提供するシェーダーは、DOTS Instancing をサポートしています。

DOTS Instanced プロパティの宣言

変換行列などのインスタンスデータをロードするには、シェーダーは DOTS Instanced プロパティを定義する必要があります。以下は、単純な DOTS Instanced プロパティブロックの例です。

UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
    UNITY_DOTS_INSTANCED_PROP(float4, Color)
UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)

プロパティブロックの開始と終了を示すには、UNITY_DOTS_INSTANCING_START およびUNITY_DOTS_INSTANCING_END マクロの後にブロック名を付加します。この例では MaterialPropertyMetadata という名前を使用しています。使用可能なブロック名は以下の 3 つです。

  • BuiltinPropertyMetadata
  • MaterialPropertyMetadata
  • UserPropertyMetadata

シェーダーはそれぞれを 1 つずつ宣言できるので、DOTS Instanced シェーダーはこのようなブロックを 0 個から 3 個持つことができます。Unity 定義のシェーダーコードは UserPropertyMetadata を使用しないので、この名前は確実に自由に使用できます。URP と HDRP は、その提供するシェーダーのそれぞれに BuiltinPropertyMetadata を定義し、そのほとんどに MaterialPropertyMetadata も定義します。このため、UserPropertyMetadata を使用することが推奨されます。カスタムシェーダーは、使用可能な 3 つの名前を全て同時に使用できます。

ブロックは、以下のようなフォーマットで、DOTS Instanced プロパティの定義をいくつでも含むことができます。

UNITY_DOTS_INSTANCED_PROP(PropertyType, PropertyName)

PropertyType は、ブールベクトル以外の任意の HLSL ビルトイン型 (uint、float4、float4x4、int2x4 など) にすることができ、PropertyName は DOTS Instanced プロパティの名前です。DOTS Instanced プロパティは 通常のマテリアルプロパティ とは完全に別になっており、これには他の通常のマテリアルプロパティと同じ名前を付けることができます。これが可能な理由は、UNITY_DOTS_INSTANCED_PROP マクロが、Unity によって認識され他のプロパティ名と競合しない、特殊な定数名を生成するからです。Unity 提供のシェーダーは DOTS Instanced プロパティに通常のマテリアルプロパティと同じ名前を付けますが、この規則に従う必要はありません。

内部的には、Unity は、シェーダーが宣言する DOTS Instanced プロパティごとに、32 ビット整数のメタデータ値をシェーダーに提供します。Unity は、コードが (描画に関連するバッチを作成するために) BatchRendererGroup.AddBatch を呼び出す時に、メタデータ値を設定します。メタデータ値は、Unity が設定しなければデフォルトの 0 になります。またシェーダーは、BatchRendererGroup.AddBatch に引数として渡される GraphicsBuffer に Unity が設定する、ByteAddressBuffer unity_DOTSInstanceData にもアクセスできます。このバッファは通常、シェーダーがインスタンスデータをロードする (ロード元の) 場所です。複数のバッチで 1 つの GraphicsBuffer を共有することもできますが、バッチごとに個別に unity_DOTSInstanceData 用の GraphicsBuffer を使用することも可能です。

ノート: Unity は DOTS Instanced データを自動的に提供しません。開発者は、各バッチの unity_DOTSInstanceData バッファに正しいデータが含まれていることを確認する必要があります。インスタンスデータは、Unity がゲームオブジェクト用に通常提供する多数のプロパティ (変換行列、ライトプローブ係数、ライトマップテクスチャ座標など) を含んでいる必要があります。

DOTS Instanced プロパティへのアクセス

DOTS Instanced プロパティにアクセスするにあたって、シェーダーは、Unity が提供するアクセスマクロのいずれかを使用できます。アクセスマクロは、unity_DOTSInstanceData 内のインスタンスデータが以下のレイアウトを使用していることを想定します。

  • メタデータ値の最下位 31 ビットが、unity_DOTSInstanceData バッファ内のバッチ内の最初のインスタンスのバイトアドレスを含んでいる。
  • メタデータ値の最上位ビットが 0 の場合は、全てのインスタンスがインスタンスインデックス 0 の値を使用する。(これは、各インスタンスがメタデータ値のバイトアドレスから直接ロードされることを意味します。この場合、バッファは、インスタンスごとに 1 つではなく、単一の値を格納するだけで済みます。)
  • メタデータ値の最上位ビットが 1 の場合、アドレスには、AddressOfInstance0 + sizeof(PropertyType) * instanceID を使用して見つけられるインスタンスインデックス instanceID の値を持つ配列が含まれている。(この場合、レンダリングされる全てのインスタンスインデックスがバッファ内に有効なデータを持っていることを確認する必要があります。そうしないと、境界外アクセスや未定義の動作が発生する可能性があります。)

メタデータの値を直接設定することもできます。これは、テクスチャなどの、上記のレイアウトを使用しないカスタムデータソースを使用する場合に便利です。

Unity は以下のアクセスマクロを提供しています。

アクセスマクロ 説明 
UNITY_ACCESS_DOTS_INSTANCED_PROP(PropertyType, PropertyName) 上記のレイアウトを使用して unity_DOTSInstanceData からロードされた値を返します。Unity が提供するシェーダーは、フォールバックするデフォルト値を持たない DOTS Instanced 組み込みプロパティにこのバージョンを使用します。
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(PropertyType, PropertyName) UNITY_ACCESS_DOTS_INSTANCED_PROP と同じ値を返しますが、メタデータの最上位ビットが 0 の場合はデフォルト値を返します。デフォルト値は、DOTS Instanced プロパティと同じ名前の通常のマテリアルプロパティの値です。Unity 提供のシェーダーが “DOTS Instanced プロパティは通常のマテリアルプロパティと同じ名前を持つ” という規則を採用しているのはこのためです。デフォルト値を使用する場合、アクセスマクロは unity_DOTSInstanceData に一切アクセスしません。Unity 提供のシェーダーはこのアクセスマクロを DOTS Instanced マテリアルプロパティに使用するため、ロードはマテリアルに設定された値にフォールバックします。
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT(PropertyType, PropertyName, DefaultValue) UNITY_ACCESS_DOTS_INSTANCED_PROP と同じ値を返します。ただし、メタデータの最上位ビットが 0 の場合は、代わりに DefaultValue を返し、unity_DOTSInstanceData にはアクセスしません。
UNITY_DOTS_INSTANCED_METADATA_NAME(PropertyType, PropertyName) 何にもアクセスせずにメタデータの値を直接返します。これは、カスタムのインスタンスデータロードスキームに役立ちます。

これらのマクロの使用例は、アクセスマクロの例 で参照できます。

Unity は、アクセスマクロと並んで、描画コマンドデータから定数の値を直接ロードするシェーダー関数を提供しています。Unity が提供するシェーダーは、これらの関数を使用します。

Unity は以下のシェーダー関数を提供しています。

シェーダー関数 説明 
LoadDOTSInstancedData_RenderingLayer 描画コマンドの renderingLayerMask を返します。
LoadDOTSInstancedData_MotionVectorsParams 描画コマンドの モーションベクトル生成モード を返します。これは、Unity シェーダーの想定する float4 としてフォーマットされます。
LoadDOTSInstancedData_WorldTransformParams インスタンスの描画を、反転した三角ワインディングで行うかどうかを返します。FlipWinding を参照してください。
LoadDOTSInstancedData_LightData シーンのメインのディレクショナルライトがインスタンスに対してアクティブかどうかを返します。メインのライトは、様々な理由で、非アクティブになっている場合があります。例えば、ライトがすでにライトマップに含まれている場合などです。
LoadDOTSInstancedData_LODFade LODCrossFade フラグ が設定されている場合に、設定した 8 ビットのクロスフェード値を返します。このフラグが設定されていない場合、戻り値は未定義です。

アクセスマクロの例

このセクションでは、Unity が提供するアクセスマクロの例と、これらのマクロを使用してインスタンス単位のデータや定数データにアクセスする方法を説明します。

インスタンス単位

以下の例の説明:

  • Color のメタデータ値は 0x80001000 です。
  • インスタンスインデックスは 5 です。
  • インスタンス 0 のデータはアドレス 0x1000 から開始します。
  • インスタンス 5 のデータは、アドレス 0x1000 + 5 * sizeof(float4) = 0x1050 にあります。

最上位ビットはすでに設定されているので、アクセサーマクロはデフォルトをロードしません。つまり、c0c1c2 は全て、unity_DOTSInstanceData アドレス 0x1050 からロードされる同じ値になります。

void ExamplePerInstance()
{
    // rawMetadataValue will contain 0x80001000
    uint rawMetadataValue = UNITY_DOTS_INSTANCED_METADATA_NAME(float4, Color);

    float4 c0 = UNITY_ACCESS_DOTS_INSTANCED_PROP(float4, Color);
    float4 c1 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, Color);
    float4 c2 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT(float4, Color, float4(1, 2, 3, 4));
}

定数

以下の例の説明:

  • Color のメタデータ値は 0x00001000 です。
  • インスタンスインデックスは 5 です。
  • インスタンス 0 のデータはアドレス 0x1000 から開始します。
  • 最上位ビットは設定されていないので、インスタンス 5 のデータはインスタンス 0 と同じアドレスにあります。

最上位ビットが設定されていないため、デフォルトにフォールバックするアクセサーマクロは unity_DOTSInstanceData にアクセスしません。これは、以下を意味します。

  • c0 には unity_DOTSInstanceData アドレス 0x1000 の値が入ります。
  • c1 は、通常のマテリアルプロパティ Color の値を格納し、Color プロパティが存在しない場合はコンパイルエラーを発生させます。
  • c2 には (1,2,3,4) が入ります (これが明示的なデフォルト値として渡されたため)。
void ExampleConstant()
{
    // rawMetadataValue will contain 0x00001000
    uint rawMetadataValue = UNITY_DOTS_INSTANCED_METADATA_NAME(float4, Color);
    float4 c0 = UNITY_ACCESS_DOTS_INSTANCED_PROP(float4, Color);
    float4 c1 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, Color);
    float4 c2 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT(float4, Color, float4(1, 2, 3, 4));
}

詳細

全ての unity_DOTSInstanceData バッファの最初の 64 バイトは、ゼロに初期化して未使用のままにすることが推奨されます。なぜなら、バッチ作成中に指定されない全てのメタデータ値に対して Unity が使用するデフォルトのメタデータ値が、ゼロであるためです。具体的には、シェーダーが UNITY_ACCESS_DOTS_INSTANCED_PROP マクロからゼロのメタデータ値をロードする場合、インスタンスインデックスが無視されるため、この値はアドレス zero からロードされます。最初の 64 バイト (最大の値型 [float4x4 行列] のサイズ) を確実にゼロにすることで、このようなロードが予測通りにゼロという結果を返すことを確実にできます。そうしないと、アドレスゼロに何があるかによって、シェーダーが、予測されない何かをロードする可能性があります。

DOTS Instancing を使用する場合、Shader Graph や Unity 提供のシェーダーは、変換行列に特殊な規則を使用します。GPU メモリと帯域幅を節約するために、変換行列は、Float を 16 個フルに使用するのではなく 12 個だけ使用して保存されます (4 つの Float は常に一定であるため)。これらのシェーダーは、行列内の各列の x、y、z が順番に格納される形でフォーマットされた Float を想定します。つまり、最初の 3 つの Float は最初の列の x、y、z であり、次の 3 つの Float は 2 番目の列の x、y、z です。行列は各列の w 要素を保存しません。これが影響する変換行列は以下です。

  • unity_ObjectToWorld
  • unity_WorldToObject
  • unity_MatrixPreviousM
  • unity_MatrixPreviousMI

以下のコードサンプルには、通常の 4×4 行列を “Float 12 個” の規則に変換する構造体が含まれています。

struct PackedMatrix
{
    public float c0x;
    public float c0y;
    public float c0z;
    public float c1x;
    public float c1y;
    public float c1z;
    public float c2x;
    public float c2y;
    public float c2z;
    public float c3x;
    public float c3y;
    public float c3z;

    public PackedMatrix(Matrix4x4 m)
    {
        c0x = m.m00;
        c0y = m.m10;
        c0z = m.m20;
        c1x = m.m01;
        c1y = m.m11;
        c1z = m.m21;
        c2x = m.m02;
        c2y = m.m12;
        c2z = m.m22;
        c3x = m.m03;
        c3y = m.m13;
        c3z = m.m23;
    }
}
描画コマンドの作成
レンダリング統計ウィンドウ