描画コマンドの作成には BatchRendererGroup.OnPerformCulling コールバックを使用します。具体的には、このコールバックの BatchCullingOutput パラメーターを使用します。このパラメーターには、単一の要素を持つ NativeArray が 1 つ含まれています。このレイアウトは、配列の要素のコンテンツを (Unity によって不必要にデータがコピーされることなく) 直接変更できることを意味します。NativeArray 内の要素は、実際の描画コマンド を含む BatchCullingOutputDrawCommands 構造体です。
OnPerformCulling の実装は、描画コマンドをいくつでも生成できます。単一のメッシュと単一のマテリアルのみを使用する単純な実装では単一の描画コマンドしか出力できませんが、より複雑な実装では、数千の、それぞれメッシュとマテリアルが異なる描画コマンドを出力できます。
ノート: 最大限の柔軟性を提供するために、Unity は配列を BatchCullingOutputDrawCommands 出力構造体内に事前に割り当てず、生ポインターとして保存します。そのため、簡単に割り当て、Burst ジョブから使用することができます。配列は UnsafeUtility.Malloc を使用して Allocator.TempJob アロケーターで割り当てる必要があります。このコールバックはメモリを解放しません。その代わり、Unity が描画コマンドを使用してレンダリングを終了した後にメモリを解放します。
描画コマンドの作成方法については、以下のコードサンプルを参照してください。このコードサンプルは、バッチの作成 に掲載のものをベースに構築されています。
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。
// インスタンス化された各種プロパティーの開始アドレスを計算します。unity_ObjectToWorld は
// アドレス 64 ではなくアドレス 96 から開始するので、32 ビットが初期化されずに残ります。これは、
// computeBufferStartIndex パラメーターが、"開始オフセットがソース配列の要素の型のサイズで割り切れる"
// ことを必要とするためです。ここでは、これは PackedMatrix のサイズ、つまり 48 です。
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);
// Set up metadata values to point to the instance data. Set the most significant bit 0x80000000 in each
このインスタンスデータを指すメタデータ値を設定します。それぞれ最上位ビット 0x80000000 を設定します。
// これは "このデータは、インスタンスインデックスによってインデックスされる、インスタンス毎に 1 つの値を持つ配列である" とシェーダーに指示します。
// シェーダーが使用するメタデータ値でここに設定されていないものはすべて 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)
{
// UnsafeUtility.Malloc() はアラインメントを必要とするので、
// (合理的なデフォルトとして) 最大の int 型のアラインメントを使用してください。
int alignment = UnsafeUtility.AlignOf<long>();
// BatchCullingOutputDrawCommands 構造体へのポインターを取得し、
// それを簡単に直接変更できるようにします。
var drawCommands = (BatchCullingOutputDrawCommands*)cullingOutput.drawCommands.GetUnsafePtr();
// 出力配列にメモリを割り当てます。より複雑な実装では、
// 何が可視であるかに応じて動的に割り当てるメモリのサイズを計算します。
// この例では、すべてのインスタンスが可視であると仮定するので、
// そのそれぞれにメモリを割り当てます。必要な割り当ては以下の通りです。
// - 単一の描画コマンド (kNumInstances インスタンスを描画する)
// - 単一の描画範囲 (単一の描画コマンドをカバーする)
// - kNumInstances 可視インスタンスのインデックス
// 必ず Allocator.TempJob を使用して配列を割り当てる必要があります。
drawCommands->drawCommands = (BatchDrawCommand*)UnsafeUtility.Malloc(UnsafeUtility.SizeOf<BatchDrawCommand>(), alignment, Allocator.TempJob);
drawCommands->drawRanges = (BatchDrawRange*)UnsafeUtility.Malloc(UnsafeUtility.SizeOf<BatchDrawRange>(), alignment, Allocator.TempJob);
drawCommands->visibleInstances = (int*)UnsafeUtility.Malloc(kNumInstances * sizeof(int), alignment, Allocator.TempJob);
drawCommands->drawCommandPickingInstanceIDs = null;
drawCommands->drawCommandCount = 1;
drawCommands->drawRangeCount = 1;
drawCommands->visibleInstanceCount = kNumInstances;
// この例ではデプスソートを使用していないため、instanceSortingPositions は null のままになります。
drawCommands->instanceSortingPositions = null;
drawCommands->instanceSortingPositionFloatCount = 0;
// この単一の描画コマンドを、"配列内でオフセット 0 から開始し、Start() メソッド内に登録されたバッチ ID と
// マテリアル ID とメッシュ ID を使用して kNumInstances インスタンスを描画する" ように設定します。
// 特別なフラグは設定しません。
drawCommands->drawCommands[0].visibleOffset = 0;
drawCommands->drawCommands[0].visibleCount = kNumInstances;
drawCommands->drawCommands[0].batchID = m_BatchID;
drawCommands->drawCommands[0].materialID = m_MaterialID;
drawCommands->drawCommands[0].meshID = m_MeshID;
drawCommands->drawCommands[0].submeshIndex = 0;
drawCommands->drawCommands[0].splitVisibilityMask = 0xff;
drawCommands->drawCommands[0].flags = 0;
drawCommands->drawCommands[0].sortingPosition = 0;
// オフセット 0 にあるこの単一の描画コマンドをカバーするように描画範囲を設定します。
// is at offset 0.
drawCommands->drawRanges[0].drawCommandsBegin = 0;
drawCommands->drawRanges[0].drawCommandsCount = 1;
// この例ではシャドウやモーションベクトルは考慮してないので、すべてがデフォルトの 0 の値のままになります。
// ただし、すべての 1 に設定される renderingLayerMask は除きます。これによって
// Unity がマスクの設定に関わらずインスタンスをレンダーするようになります。
drawCommands->drawRanges[0].filterSettings = new BatchFilterSettings { renderingLayerMask = 0xffffffff, };
// 最後に、実際の可視のインスタンスのインデックスを配列に書き込みます。
// より複雑な実装では、この出力は何か可視であるかによって変わりますが、
// この例ではすべてが可視であると仮定しています。
for (int i = 0; i < kNumInstances; ++i)
drawCommands->visibleInstances[i] = i;
// この単純な例はジョブを使用しないので、空の JobHandle を返します。
// パフォーマンス負荷の高いアプリケーションの場合は、Burst ジョブを使用して
// カリングと描画コマンドの出力を実装することが推奨されます。その場合、この関数は
// Burst ジョブの終了時に完了するハンドルをここに返します。
return new JobHandle();
}
}
以上が、BRG 用の完全で最終的なコードサンプルです。このコンポーネントをゲームオブジェクトに添付し、Inspector でメッシュと DOTS インスタンシング 対応のマテリアルを設定し、再生モードに入ると、Unity がこのマテリアルを使用してメッシュの 3 つのインスタンスをレンダーします。