Version: 2023.1
言語: 日本語
メッシュとマテリアルの登録
描画コマンドの作成

バッチの作成

BatchRendererGroup (BRG) は自動的にはインスタンスデータを一切提供しません。インスタンスデータには、変換行列、ライトプローブ係数、ライトマップテクスチャ座標など、ゲームオブジェクトに通常組み込まれている多くのプロパティが含まれます。このため、アンビエントライティングのような機能は自分でインスタンスデータを提供しなければ動作しません。これを行うには、バッチを追加して設定します。バッチとはインスタンスの集まりで、1 つ 1 つのインスタンスは、レンダリングされる 1 つの要素に対応しています。各インスタンスが実際に何を表すかは、何をレンダリングするかによって変わります。例えば、プロップ (小道具) オブジェクトレンダラーでは 1 つのインスタンスが 1 つのプロップを表し、Terrain (地形) レンダラーでは 1 つのインスタンスが地形の 1 つのパッチ (部分) を表します。

各バッチはメタデータ値のセットと単一の GraphicsBuffer を持ち、これをバッチ内のすべてのインスタンスが共有します。インスタンスのデータをロードするプロセスとして一般的なのは “メタデータ値を使用して GraphicsBuffer 内の正しい場所から読み込む” というものです。シェーダーマクロの UNITY_ACCESS_DOTS_INSTANCED_PROP ファミリーは、このスキームで動作します (DOTS Instanced プロパティーへのアクセス 参照)。ただし、このインスタンスごとのデータロードスキームは必ず使用しなければならない訳ではなく、独自のスキームを実装することも可能です。

バッチを作成するには BatchRendererGroup.AddBatch を使用してください。このメソッドは、メタデータ値の配列と GraphicsBuffer へのハンドルを受け取ります。Unity はバッチからインスタンスを描画する時にメタデータ値をシェーダーに渡し、GraphicsBuffer を unity_DOTSInstanceData としてバインドします。シェーダーによって使用されるが、バッチ作成時に渡さないメタデータ値は、Unity によって 0 に設定されます。

バッチのメタデータ値は作成後には変更できないため、バッチに渡すメタデータ値はすべて最終的なものになります。メタデータ値を変更する必要がある場合は、新しいバッチを作成して古いバッチを削除してください。バッチの GraphicsBuffer にはいつでも変更を加えることが可能です。これを行うには SetBatchBuffer を使用してください。これは、バッファのサイズを変更したり、既存のバッファの容量が不足した場合により大きなバッファを割り当てるのに役立ちます。

ノート: バッチの作成時にそのサイズを指定する必要はありません。その代わり、シェーダーに渡すインスタンスのインデックスをシェーダーが正しく処理できるようにする必要があります。これが何を意味するかはシェーダーによって変わります。Unity 提供の SRP シェーダーの場合は “渡したインデックスのバッファに、有効なインスタンスデータがなければならない” ことを意味します。

以下のコードサンプルは、メタデータ値と、インスタンスデータの GraphicsBuffer を使用して、バッチを作成する方法の一例です。このコードサンプルは、メッシュとマテリアルの登録 にあるコードをベースに構築されています。

using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Rendering;

public class SimpleBRGExample : MonoBehaviour
{
    public Mesh mesh;
    public Material material;

    private BatchRendererGroup m_BRG;

    private GraphicsBuffer m_InstanceData;
    private BatchID m_BatchID;
    private BatchMeshID m_MeshID;
    private BatchMaterialID m_MaterialID;

    // 計算を便利にするいくつかのヘルパー定数。
    private const int kSizeOfMatrix = sizeof(float) * 4 * 4;
    private const int kSizeOfPackedMatrix = sizeof(float) * 4 * 3;
    private const int kSizeOfFloat4 = sizeof(float) * 4;
    private const int kBytesPerInstance = (kSizeOfPackedMatrix * 2) + kSizeOfFloat4;
    private const int kExtraBytes = kSizeOfMatrix * 2;
    private const int kNumInstances = 3;

    // PackedMatrix は、行列を Unity 提供の SRP シェーダーが想定する形式に
    // 変換する便利な型です。
    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;
        }
    }

    private void Start()
    {
        m_BRG = new BatchRendererGroup(this.OnPerformCulling, IntPtr.Zero);
        m_MeshID = m_BRG.RegisterMesh(mesh);
        m_MaterialID = m_BRG.RegisterMaterial(material);

        AllocateInstanceDateBuffer();
        PopulateInstanceDataBuffer();
    }

    private void AllocateInstanceDateBuffer()
    {
        m_InstanceData = new GraphicsBuffer(GraphicsBuffer.Target.Raw,
            BufferCountForInstances(kBytesPerInstance, kNumInstances, kExtraBytes),
            sizeof(int));
    }

    private void PopulateInstanceDataBuffer()
    {
        // インスタンスデータバッファの先頭にゼロ行列を置くことで、アドレス 0 からの読み込みが 0 を返すようにします。
        var zero = new Matrix4x4[1] { Matrix4x4.zero };

        // 3 つのサンプルインスタンスの変換行列を作成します。
        var matrices = new Matrix4x4[kNumInstances]
        {
            Matrix4x4.Translate(new Vector3(-2, 0, 0)),
            Matrix4x4.Translate(new Vector3(0, 0, 0)),
            Matrix4x4.Translate(new Vector3(2, 0, 0)),
        };

        // 変換行列を、シェーダーの想定するパックされた形式に変換します。
        var objectToWorld = new PackedMatrix[kNumInstances]
        {
            new PackedMatrix(matrices[0]),
            new PackedMatrix(matrices[1]),
            new PackedMatrix(matrices[2]),
        };

        // パックされた逆行列も作成します。
        var worldToObject = new PackedMatrix[kNumInstances]
        {
            new PackedMatrix(matrices[0].inverse),
            new PackedMatrix(matrices[1].inverse),
            new PackedMatrix(matrices[2].inverse),
        };

        // すべてのインスタンスに固有の色を持たせます。
        var colors = new Vector4[kNumInstances]
        {
            new Vector4(1, 0, 0, 1),
            new Vector4(0, 1, 0, 1),
            new Vector4(0, 0, 1, 1),
        };

        // この単純な例では、インスタンスデータは以下のようにバッファ内に配置されます。
        // オフセット | 説明
        //      0 | 64 バイトの 0。つまりアドレス 0 からの読み込みは 0 を返す。
        //     64 | 初期化されていない 32 バイト。SetData を扱いやすくするもので、それ以外には不要。
        //     96 | unity_ObjectToWorld。3 つのパックされた float3x4 行列。
        //    240 | unity_WorldToObject。3 つのパックされた float3x4 行列。
        //    384 | _BaseColor。3 つの float4。

        // インスタンス化された各種プロパティーの開始アドレスを計算します。
        // SetData の computeBufferStartIndex パラメーターはソース配列要素として表されるので、
        // unity_ObjectToWorld はアドレス 64 ではなくアドレス 96 から開始します。
        // このため、sizeof(PackedMatrix) の倍数を使用するほうが簡単です。 
        
        uint byteAddressObjectToWorld = kSizeOfPackedMatrix * 2;
        uint byteAddressWorldToObject = byteAddressObjectToWorld + kSizeOfPackedMatrix * kNumInstances;
        uint byteAddressColor = byteAddressWorldToObject + kSizeOfPackedMatrix * kNumInstances;

        // インスタンスデータを GraphicsBuffer にアップロードしてシェーダーがそれらを読み込めるようにします。
        m_InstanceData.SetData(zero, 0, 0, 1);
        m_InstanceData.SetData(objectToWorld, 0, (int)(byteAddressObjectToWorld / kSizeOfPackedMatrix), objectToWorld.Length);
        m_InstanceData.SetData(worldToObject, 0, (int)(byteAddressWorldToObject / kSizeOfPackedMatrix), worldToObject.Length);
        m_InstanceData.SetData(colors, 0, (int)(byteAddressColor / kSizeOfFloat4), colors.Length);

        // このインスタンスデータを指すメタデータ値を設定します。それぞれ最上位ビット 0x80000000 を設定します。
        // これは "このデータは、インスタンスインデックスによってインデックスされる、インスタンス毎に 1 つの値を持つ配列である" 
        // とシェーダーに指示します。シェーダーの使用するメタデータ値でここに設定されていないものはすべて 0 になります。
        // 0 の値が UNITY_ACCESS_DOTS_INSTANCED_PROP (つまりデフォルトなし) に使用された場合、シェーダーは
        // 0x00000000 のメタデータ値を解釈してバッファの先頭から読み込みます。バッファの先頭はゼロ行列なので、
        // このような読み込みは必ず 0 を返します。これは合理的なデフォルト値です。      
        var metadata = new NativeArray<MetadataValue>(3, Allocator.Temp);
        metadata[0] = new MetadataValue { NameID = Shader.PropertyToID("unity_ObjectToWorld"), Value = 0x80000000 | byteAddressObjectToWorld, };
        metadata[1] = new MetadataValue { NameID = Shader.PropertyToID("unity_WorldToObject"), Value = 0x80000000 | byteAddressWorldToObject, };
        metadata[2] = new MetadataValue { NameID = Shader.PropertyToID("_BaseColor"), Value = 0x80000000 | byteAddressColor, };

        // 最後に、これらのインスタンスのバッチを作成し、そのバッチに、このインスタンスデータを持つ GraphicsBuffer と
        // プロパティの場所を指定するメタデータ値を使用させます。
        m_BatchID = m_BRG.AddBatch(metadata, m_InstanceData.bufferHandle);
    }

    // Raw バッファは int で割り当てられます。これはデータに必要な int の数を計算する
    // ユーティリティメソッドです。
    int BufferCountForInstances(int bytesPerInstance, int numInstances, int extraBytes = 0)
    {
        // バイト数を int の倍数に丸めます。
        bytesPerInstance = (bytesPerInstance + sizeof(int) - 1) / sizeof(int) * sizeof(int);
        extraBytes = (extraBytes + sizeof(int) - 1) / sizeof(int) * sizeof(int);
        int totalBytes = bytesPerInstance * numInstances + extraBytes;
        return totalBytes / sizeof(int);
    }


    private void OnDisable()
    {
        m_BRG.Dispose();
    }

    public unsafe JobHandle OnPerformCulling(
        BatchRendererGroup rendererGroup,
        BatchCullingContext cullingContext,
        BatchCullingOutput cullingOutput,
        IntPtr userContext)
    {
        // この単純な例はジョブを使用しないので、空の JobHandle を返します。
        // パフォーマンス負荷の高いアプリケーションの場合は、Burst ジョブを使用して
        // カリングと描画コマンドの出力を実装することが推奨されます。その場合、この関数は
        // Burst ジョブの終了時に完了するハンドルをここに返します。
        return new JobHandle();

    }
}

これで BatchRendererGroup オブジェクトを使用して必要なリソースをすべて登録できました。さらに詳しくは次のトピック 描画コマンドの作成 を参照してください。

メッシュとマテリアルの登録
描画コマンドの作成