グラフィックスコマンドバッファ
スパーステクスチャ

GPU インスタンシング

イントロダクション

GPU インスタンシングを使うと、少ないドローコールで同一オブジェクトをたくさん描画することができます。ただし、いくらか制限事項があります。

  • インスタンス化するオブジェクトでは、同じメッシュとマテリアルを共有している必要があります。ただし、インスタンスごとに追加できる情報もあります。詳細は以下の Adding per-instance data を参照してください。
  • MeshRenderer コンポーネントと Graphics.DrawMesh API がサポートされています。
  • GPU インスタンシングは、以下のプラットホームで利用可能です。
    • Windows: SM 4.0 以降を伴う DX11/DX12, OpenGL 4.1 以降
    • OS X & Linux: OpenGL 4.1 以降
    • PlayStation 4
    • Xbox One

オブジェクトにインスタンシングを加える

インスタンシングをサポートする Standard Surface Shader があります。プロジェクトに加えるには、 Shader > Standard Surface Shader (Instanced) の順に選択します。

Standard Instanced Shader を加える
Standard Instanced Shader を加える

このシェーダーをゲームオブジェクトのマテリアルに適用します。マテリアルの Inspector ウィンドウで、Shader ドロップダウンで Instanced を選び、リストから自分のインスタンスシェーダーを選択します。

Standard Instanced Shader をマテリアルに指定
Standard Instanced Shader をマテリアルに指定

インスタンスごとのデータを加える

インスタンス化したオブジェクトが同じメッシュとマテリアルを共有していても、MaterialPropertyBlock API を使用してインスタンス化したオブジェクトごとにシェーダープロパティーを設定できます。以下の例では、各オブジェクトに _Color プロパティーを使ってランダムな色の値が指定されます。

MaterialPropertyBlock props = new MaterialPropertyBlock();
MeshRenderer renderer;

foreach (GameObject obj in objects)
{
   float r = Random.Range(0.0f, 1.0f);
   float g = Random.Range(0.0f, 1.0f);
   float b = Random.Range(0.0f, 1.0f);
   props.SetColor("_Color", new Color(r, g, b));
   
   renderer = obj.GetComponent<MeshRenderer>();
   renderer.SetPropertyBlock(props);
}

自分のシェーダーにインスタンシングを加える

簡単な unlit シェーダーをインスタンシングできるようにしてみましょう。

Shader "SimplestInstancedShader"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                UNITY_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                UNITY_INSTANCE_ID
            };

            UNITY_INSTANCING_CBUFFER_START (MyProperties)
            UNITY_DEFINE_INSTANCED_PROP (float4, _Color)
            UNITY_INSTANCING_CBUFFER_END
           
            v2f vert (appdata v)
            {
                v2f o;

                UNITY_SETUP_INSTANCE_ID (v);
                UNITY_TRANSFER_INSTANCE_ID (v, o);

                o.vertex = UnityObjectToClipPos (v.vertex);
                return o;
            }
           
            fixed4 frag (v2f i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID (i);
                return UNITY_ACCESS_INSTANCED_PROP (_Color);
            }
            ENDCG
        }
    }
}

追加したコード

追加したコード 機能
#pragma multi_compile_instancing multi_compile_instancing は 2 つのバリアントを 持つシェーダーを生成します。1 つはビルトインのキーワード INSTANCING_ON が決められているもの (インスタンシング可能)、もう 1 つは何も決められていないものです。これにより、GPU でインスタンシングがサポートされていない場合、シェーダーがインスタンシングしていないバージョンにフォールバックできます。
UNITY_INSTANCE_ID 頂点シェーダーの入力や出力構造体でインスタンス ID を定義するために使用します。詳しくは、SV_InstanceID を参照してください。
UNITY_INSTANCING_CBUFFER_START(name) / UNITY_INSTANCING_CBUFFER_END それぞれのインスタンスのプロパティーはすべて、予約された定数バッファで定義する必要があります。この一組になったマクロを使って、各インスタンスに固有化したいプロパティーをラップします。
UNITY_DEFINE_INSTANCED_PROP(float4, color) それぞれのシェーダープロパティーの型と名前を定義します。この例では、color プロパティーが固有のものです。
UNITY_SETUP_INSTANCE_ID(v); インスタンス ID がシェーダー関数にアクセス可能になります。頂点シェーダーの最初に使用する必要があります。また、フラグメントシェーダーには、オプションとして使用されます。
UNITY_TRANSFER_INSTANCE_ID(v, o); 頂点シェーダーで入力構造体から出力構造体へインスタンス ID をコピーします。フラグメントシェーダーでは、インスタンスごとのデータにアクセスするときのみ必要です。
UNITY_ACCESS_INSTANCED_PROP(color) インスタンスごとのシェーダープロパティーにアクセスします。インスタンス ID を使用してインスタンスデータ配列を表します。

注意 マテリアルプロパティーがインスタンス化されている限り、たとえ他のレンダラーに異なるインスタンス化したプロパティーを用いたとしても、レンダラーは常にインスタンス化してレンダリングすることができます。普通の「インスタンスされていない」プロパティーはバッチ処理をすることができません。そのため、それらを MaterialPropertyBlock に使用しないでください。代わりに、ほかのマテリアルを作成してください。

UnityObjectToClipPos に関する注意

UnityObjectToClipPos(v.vertex) を使用する方が常に好ましいですが、そうでない場合は mul(UNITY_MATRIX_MVP,v.vertex) を使用します。インスタンス化したシェーダーで__UNITY_MATRIX_MVP__ を基準として使用できる一方、UnityObjectToClipPos は頂点位置をオブジェクト空間から頂点シェーダーの出力する位置 (クリップスペース) へ変換する効果的な方法です。

インスタンス化したシェーダーで UNITY_MATRIX_MVP (他のビルトインマトリックス) は、マトリックスの追加を含めるようわかりやすく変更されています。特に、 mul(UNITY_MATRIX_VP, unity_ObjectToWorld) に展開しています。 unity_ObjectToWorldunity_ObjectToWorldArray[unity_InstanceID] に展開しています。UnityObjectToClipPos は 2 マトリックスベクトル乗算を同時に行うために最適化されています。シェーダーコンパイラーは自動的にこの最適化を行わないため、手動で乗算を行うより効果的です。

注意

  • インスタンス化されたドローコールはフレームデバッガで Draw Mesh (instanced) と表示されます。
  • シェーダーを作成したり変更したりするときに、影も忘れずにインスタンス化してください。サーフェイスシェーダーに関しては addshadow オプションを使い、インスタンス化した影のパスを強制的に生成するようにします。
  • インスタンスごとのプロパティーは定義する必要はありません。ただし、ワールドのマトリックスが必要なため、インスタンス ID の設定は必須です。
  • フォワードレンダリングを使用するとき、複数のライトに影響されるオブジェクトは効果的にインスタンス化できません。基本パスのみでインスタンシングは効果的で、追加パスは効果がありません。
  • ライトマップを使用したり、その他のライトやリフレクションプローブに影響されるオブジェクトはインスタンス化できません。
  • 複数パスシェーダーに関して 2 つ以上のパスがある場合、最初のパスのみがインスタンス化されます。これは、強制的に Unityによって後者のパスが各オブジェクトに対して一緒にレンダリングされるためです。
  • Unity が常に頂点の変形計算するように設定する必要があります。そのためには、追加のパスでまず、M を乗算して、VP (VP * M * v) で乗算します。これにより、浮動小数点エラーに起因する基準や最初のパスの矛盾を避けることができます。 UnityCG.cginc を含む前に UNITY_USE_CONCATENATED_MATRICES を定義してください。サーフェスシェーダーでは自動的に生成されるため、これを行う必要はありません。
  • D3D 定数バッファは最大サイズ 64キロバイトで、OpenGL では通常 16キロバイトです。あまりたくさんインスタンスごとのプロパティーを定義しようとするとこの制限値に達し、シェーダーがコンパイルに失敗したり、さらに悪いケースでは、シェーダーコンパイラーがクラッシュする場合があります。これを避けるには、バッチ処理のサイズとインスタンスごとのプロパティーのサイズのバランスを取ることが必要です。.cginc ファイルを加える前に、整数と一緒に UNITY_MAX_INSTANCE_COUNT を定義すると、インスタンス化されたドローコールが呼び出す最大数を制限できます。これにより、インスタンス化された定数バッファでより多くのプロパティーを可能にします。サーフェスシェーダーを #pragma instancing_options maxcount:number と一緒に使用して、同じ効果を得ることができます。この最大インスタンス数のデフォルト値は 500 で、OpenGLでは、実際の値は指定した値の 1/4 です。つまり、デフォルトで 125 です。
  • 上の例で使用されているシェーダーマクロのすべては、UnityInstancing.cginc で定義されています。このファイルは、 [Unity folder]\Editor\Data\CGIncludes にあります。
グラフィックスコマンドバッファ
スパーステクスチャ