BatchRenderGroup演示Demo

本文最后更新于 2026年7月8日 上午

1
2
本文实现了一个最小的BatchRendererGroup演示Demo,本例子将会绘制一个可配置位置、旋转、缩放和颜色的Mesh实例。
参考了brg-shooter项目中的数据布局和使用方式。

brg_demo.gif

BatchRendererGroup是什么

Batch Renderer Group (BRG) 是Unity在2020年推出的一个重要组件,旨在优化渲染流程并提升渲染效率。
它主要解决了多线程Batch处理的问题,并作为Hybrid Renderer V2[1]的底层基础发布。
实际上,Batch Renderer Group是一套暴露给业务层进行Batch处理的API。
另外,BatchRendererGroup的开发者看到了这套API对性能提升带来的巨大潜力,在Unity 2022中对BRG进行了进一步的易用性重构。
现在,BRG可以脱离Hybrid Renderer V2独立使用。

BatchRendererGroup的使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//1、创建BatchRendererGroup,注册Mesh和Material资源,以及回调函数OnPerformCulling
m_BRG = new BatchRendererGroup(this.OnPerformCulling, IntPtr.Zero);
m_MeshID = m_BRG.RegisterMesh(mesh);
m_MaterialID = m_BRG.RegisterMaterial(material);

//2、为batch 准备数据.
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, };

//3、最后AddBatch
m_BatchID = m_BRG.AddBatch(metadata, m_InstanceData.bufferHandle, (uint)BufferOffset, (uint)BufferWindowSize);

//4、OnPerformCulling回调,在回调中处理每个相机的裁剪,裁剪之后组织自己的渲染数据等
public unsafe JobHandle OnPerformCulling( BatchRendererGroup rendererGroup,
BatchCullingContext cullingContext,BatchCullingOutput cullingOutput, IntPtr userContext)
{...}

细节说明

  • 数据对齐:float3x4是按照列存储的,而Matrix4x4是按照行存储的,BRG使用的数据为前者,所以当我们表示一个Vector3
    position时,对应的float3x4如下

    1
    2
    3
    4
    5
    float3x4(
    1, 0, 0, 0,
    1, 0, 0, 0,
    1, x, y, z
    );
  • 数据布局:也就是如何在GraphicsBuffer中计算数组的GPU地址,一个float3x4对应的字节数是sizeof(float) * 3 * 4 = 48
    ,一个float4对应的字节数是16字节。
    例子中渲染一个物体需要 objectToWorld + worldToObject + color 三个属性,也就是 48 * 2 + 16 = 112 字节。

  • UBO[2] vs SSBO[3]:BatchRendererGroup支持两种不同的缓冲区类型,分别是Uniform Buffer Object (UBO) 和 Shader Storage
    Buffer Object (SSBO),
    通过BatchRendererGroup.BufferTarget可以查询当前使用的缓冲区类型。之所以不同是因为不同的平台对UBO和SSBO的支持程度不同。我们需要当前BatchRendererGroup使用的缓冲区类型来决定我们创建GraphicsBuffer的Target类型。
    如果目标缓冲区类型是UBO,那么意味我们要手动计算窗口大小

    1
    2
    _alignedWindowSizeBytes = BatchRendererGroup.GetConstantBufferMaxWindowSize();
    _maxInstancePerWindow = Math.Max(1, _alignedWindowSizeBytes / kGpuItemSizeBytes);

    如果目标缓冲区类型是SSBO,那么我们可以直接创建一个足够大的缓冲区 把一个instace的数据都放进去

    1
    2
    _alignedWindowSizeBytes = (kGpuItemSizeBytes + 15) & ~15; // 16-byte align
    _maxInstancePerWindow = 1; // single window for one instance
  • 创建GraphicsBuffer时的Target类型

    1
    2
    3
    4
    5
    6
    _windowSizeInFloat4 = _alignedWindowSizeBytes / kFloat4Size;

    if (useUBO)
    _gpuBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Constant, _windowSizeInFloat4, kFloat4Size);
    else
    _gpuBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Raw, _alignedWindowSizeBytes / 4, 4);
  • 组织Batch的Metadata信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var meta = new NativeArray<MetadataValue>(3, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
    meta[0] = CreateMetadataValue(ObjectToWorldID, 0, true); // obj2world
    meta[1] = CreateMetadataValue(WorldToObjectID, _maxInstancePerWindow * 3 * kFloat4Size, true); // world2obj
    meta[2] = CreateMetadataValue(BaseColorID, _maxInstancePerWindow * 3 * 2 * kFloat4Size, true); // color

    private static MetadataValue CreateMetadataValue(int nameID, int gpuOffset, bool isPerInstance)
    {
    const uint kIsPerInstanceBit = 0x80000000;
    return new MetadataValue
    {
    NameID = nameID,
    Value = (uint)gpuOffset | (isPerInstance ? kIsPerInstanceBit : 0u)
    };
    }
  • 组织Batch

    1
    _batchID = _brg.AddBatch(meta, _gpuBuffer.bufferHandle, 0u, useUBO ? (uint)_alignedWindowSizeBytes : 0u);
  • 更新Buffer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    int iInWindow = 0;
    int windowOffset = 0; // only one window

    // Build rotation (uniform scale applied)
    float3x3 rot = math.float3x3(rotation) * uniformScale;
    float3 bpos = worldPos;

    // obj2world (SoA layout used in brg-shooter debris)
    _sysmemBuffer[windowOffset + iInWindow * 3 + 0] = new float4(rot.c0.x, rot.c0.y, rot.c0.z, rot.c1.x);
    _sysmemBuffer[windowOffset + iInWindow * 3 + 1] = new float4(rot.c1.y, rot.c1.z, rot.c2.x, rot.c2.y);
    _sysmemBuffer[windowOffset + iInWindow * 3 + 2] = new float4(rot.c2.z, bpos.x, bpos.y, bpos.z);

    // world2obj (packed the same way as brg-shooter)
    _sysmemBuffer[windowOffset + _maxInstancePerWindow * 3 * 1 + iInWindow * 3 + 0] =
    new float4(rot.c0.x, rot.c1.x, rot.c2.x, rot.c0.y);
    _sysmemBuffer[windowOffset + _maxInstancePerWindow * 3 * 1 + iInWindow * 3 + 1] =
    new float4(rot.c1.y, rot.c2.y, rot.c0.z, rot.c1.z);
    _sysmemBuffer[windowOffset + _maxInstancePerWindow * 3 * 1 + iInWindow * 3 + 2] =
    new float4(rot.c2.z, -bpos.x, -bpos.y, -bpos.z);

    // color
    _sysmemBuffer[windowOffset + _maxInstancePerWindow * 3 * 2 + iInWindow] =
    new float4(color.r, color.g, color.b, color.a);
  • 上传数据到GPU

    1
    _gpuBuffer.SetData(_sysmemBuffer, 0, 0, kGpuItemFloat4Count);
  • 组织DrawCommand

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    var drawCommands = new BatchCullingOutputDrawCommands
    {
    drawCommandCount = 1,
    drawRangeCount = 1,
    drawRanges = (BatchDrawRange*)UnsafeUtility.Malloc(sizeof(BatchDrawRange), 16, Allocator.TempJob),
    visibleInstances = (int*)UnsafeUtility.Malloc(sizeof(int), 16, Allocator.TempJob),
    drawCommands = (BatchDrawCommand*)UnsafeUtility.Malloc(sizeof(BatchDrawCommand), 16, Allocator.TempJob),
    instanceSortingPositions = null,
    instanceSortingPositionFloatCount = 0
    };

    drawCommands.drawRanges[0] = new BatchDrawRange
    {
    drawCommandsBegin = 0,
    drawCommandsCount = 1,
    filterSettings = new BatchFilterSettings
    {
    renderingLayerMask = 1,
    layer = 0,
    motionMode = MotionVectorGenerationMode.Camera,
    shadowCastingMode = castShadows ? ShadowCastingMode.On : ShadowCastingMode.Off,
    receiveShadows = true,
    staticShadowCaster = false,
    allDepthSorted = false
    }
    };

    drawCommands.visibleInstances[0] = 0;

    drawCommands.drawCommands[0] = new BatchDrawCommand
    {
    visibleOffset = 0,
    visibleCount = 1,
    batchID = _batchID,
    materialID = _matID,
    meshID = _meshID,
    submeshIndex = 0,
    splitVisibilityMask = 0xff,
    flags = BatchDrawCommandFlags.None,
    sortingPosition = 0
    };

    cullingOutput.drawCommands[0] = drawCommands;

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Rendering;

namespace Assembly
{
// Self-contained minimal BRG demo:
// - Single instance
// - SoA layout identical to brg-shooter: obj2world (3 float4) + world2obj (3 float4) + color (1 float4)
// - Uses BatchRendererGroup directly (no external helpers)
public unsafe class BRGDemo : MonoBehaviour
{
[Header("Instance Params")] public Vector3 worldPos = Vector3.zero;
public Quaternion rotation = Quaternion.identity;
public float uniformScale = 1.0f;
public Color color = Color.white;

[Header("Render Resources")] public Mesh mesh;
public Material material;
public bool castShadows = true;

// BRG core
private BatchRendererGroup _brg;
private BatchID _batchID;
private BatchMeshID _meshID;
private BatchMaterialID _matID;
private GraphicsBuffer _gpuBuffer;
private NativeArray<float4> _sysmemBuffer;

// Constants aligned with brg-shooter
private const int kFloat4Size = 16;
private const int kGpuItemFloat4Count = 7; // 3 (obj2world) + 3 (world2obj) + 1 (color)
private const int kGpuItemSizeBytes = kGpuItemFloat4Count * kFloat4Size;

// Cached layout info
private int _alignedWindowSizeBytes;
private int _maxInstancePerWindow;
private int _windowSizeInFloat4;

// Shader property IDs
private static readonly int ObjectToWorldID = Shader.PropertyToID("unity_ObjectToWorld");
private static readonly int WorldToObjectID = Shader.PropertyToID("unity_WorldToObject");
private static readonly int BaseColorID = Shader.PropertyToID("_BaseColor");

private void Awake()
{
InitBRG();
UploadGPUData(); // initial upload
}

private void LateUpdate()
{
UploadGPUData();
}

private void OnDestroy()
{
_brg.RemoveBatch(_batchID);
_brg.UnregisterMesh(_meshID);
_brg.UnregisterMaterial(_matID);
_brg.Dispose();

_gpuBuffer?.Dispose();
if (_sysmemBuffer.IsCreated) _sysmemBuffer.Dispose();
}

private void InitBRG()
{
// Determine buffer mode (SSBO vs UBO)
bool useUBO = BatchRendererGroup.BufferTarget == BatchBufferTarget.ConstantBuffer;

// Compute window sizes
if (useUBO)
{
_alignedWindowSizeBytes = BatchRendererGroup.GetConstantBufferMaxWindowSize();
_maxInstancePerWindow = Math.Max(1, _alignedWindowSizeBytes / kGpuItemSizeBytes);
}
else
{
_alignedWindowSizeBytes = (kGpuItemSizeBytes + 15) & ~15; // 16-byte align
_maxInstancePerWindow = 1; // single window for one instance
}

_windowSizeInFloat4 = _alignedWindowSizeBytes / kFloat4Size;

// Allocate system buffer (one window is enough for a single instance)
_sysmemBuffer = new NativeArray<float4>(_windowSizeInFloat4, Allocator.Persistent,
NativeArrayOptions.ClearMemory);

// Allocate GPU buffer
if (useUBO)
_gpuBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Constant, _windowSizeInFloat4, kFloat4Size);
else
_gpuBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Raw, _alignedWindowSizeBytes / 4, 4);

// Create BRG
_brg = new BatchRendererGroup(OnPerformCulling, IntPtr.Zero);

// Metadata
var meta = new NativeArray<MetadataValue>(3, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
meta[0] = CreateMetadataValue(ObjectToWorldID, 0, true); // obj2world
meta[1] = CreateMetadataValue(WorldToObjectID, _maxInstancePerWindow * 3 * kFloat4Size, true); // world2obj
meta[2] = CreateMetadataValue(BaseColorID, _maxInstancePerWindow * 3 * 2 * kFloat4Size, true); // color

_batchID = _brg.AddBatch(meta, _gpuBuffer.bufferHandle, 0u, useUBO ? (uint)_alignedWindowSizeBytes : 0u);
meta.Dispose();

_meshID = _brg.RegisterMesh(mesh);
_matID = _brg.RegisterMaterial(material);

// Huge bounds to disable frustum cull for this demo
_brg.SetGlobalBounds(new Bounds(Vector3.zero, Vector3.one * 100000f));
}

[BurstCompile]
private void UploadGPUData()
{
// Single instance index = 0
int iInWindow = 0;
int windowOffset = 0; // only one window

// Build rotation (uniform scale applied)
float3x3 rot = math.float3x3(rotation) * uniformScale;
float3 bpos = worldPos;

// obj2world (SoA layout used in brg-shooter debris)
_sysmemBuffer[windowOffset + iInWindow * 3 + 0] = new float4(rot.c0.x, rot.c0.y, rot.c0.z, rot.c1.x);
_sysmemBuffer[windowOffset + iInWindow * 3 + 1] = new float4(rot.c1.y, rot.c1.z, rot.c2.x, rot.c2.y);
_sysmemBuffer[windowOffset + iInWindow * 3 + 2] = new float4(rot.c2.z, bpos.x, bpos.y, bpos.z);

// world2obj (packed the same way as brg-shooter)
_sysmemBuffer[windowOffset + _maxInstancePerWindow * 3 * 1 + iInWindow * 3 + 0] =
new float4(rot.c0.x, rot.c1.x, rot.c2.x, rot.c0.y);
_sysmemBuffer[windowOffset + _maxInstancePerWindow * 3 * 1 + iInWindow * 3 + 1] =
new float4(rot.c1.y, rot.c2.y, rot.c0.z, rot.c1.z);
_sysmemBuffer[windowOffset + _maxInstancePerWindow * 3 * 1 + iInWindow * 3 + 2] =
new float4(rot.c2.z, -bpos.x, -bpos.y, -bpos.z);

// color
_sysmemBuffer[windowOffset + _maxInstancePerWindow * 3 * 2 + iInWindow] =
new float4(color.r, color.g, color.b, color.a);

// Upload exactly 7 float4s
_gpuBuffer.SetData(_sysmemBuffer, 0, 0, kGpuItemFloat4Count);
}

private static MetadataValue CreateMetadataValue(int nameID, int gpuOffset, bool isPerInstance)
{
const uint kIsPerInstanceBit = 0x80000000;
return new MetadataValue
{
NameID = nameID,
Value = (uint)gpuOffset | (isPerInstance ? kIsPerInstanceBit : 0u)
};
}

[BurstCompile]
public JobHandle OnPerformCulling(BatchRendererGroup rendererGroup, BatchCullingContext cullingContext,
BatchCullingOutput cullingOutput, IntPtr userContext)
{
// Single draw command, single visible instance
var drawCommands = new BatchCullingOutputDrawCommands
{
drawCommandCount = 1,
drawRangeCount = 1,
drawRanges = (BatchDrawRange*)UnsafeUtility.Malloc(sizeof(BatchDrawRange), 16, Allocator.TempJob),
visibleInstances = (int*)UnsafeUtility.Malloc(sizeof(int), 16, Allocator.TempJob),
drawCommands = (BatchDrawCommand*)UnsafeUtility.Malloc(sizeof(BatchDrawCommand), 16, Allocator.TempJob),
instanceSortingPositions = null,
instanceSortingPositionFloatCount = 0
};

drawCommands.drawRanges[0] = new BatchDrawRange
{
drawCommandsBegin = 0,
drawCommandsCount = 1,
filterSettings = new BatchFilterSettings
{
renderingLayerMask = 1,
layer = 0,
motionMode = MotionVectorGenerationMode.Camera,
shadowCastingMode = castShadows ? ShadowCastingMode.On : ShadowCastingMode.Off,
receiveShadows = true,
staticShadowCaster = false,
allDepthSorted = false
}
};

drawCommands.visibleInstances[0] = 0;

drawCommands.drawCommands[0] = new BatchDrawCommand
{
visibleOffset = 0,
visibleCount = 1,
batchID = _batchID,
materialID = _matID,
meshID = _meshID,
submeshIndex = 0,
splitVisibilityMask = 0xff,
flags = BatchDrawCommandFlags.None,
sortingPosition = 0
};

cullingOutput.drawCommands[0] = drawCommands;
return default;
}
}
}
1
2
3
references:
- https://unity.com/cn/blog/engine-platform/batchrenderergroup-sample-high-frame-rate-on-budget-devices
- https://gamedev.center/trying-out-new-unity-api-batchrenderergroup/
  1. Entity Graphics的前生
  2. UBO(Uniform Buffer Object)/ ConstantBuffer:各图形 API 的“常量缓冲区”(D3D 的 Constant Buffer、Vulkan 的 Uniform Buffer、Metal 的 Constant buffer)。小且快速,有严格的大小/对齐限制(常见单次绑定窗口上限 64KB,具体平台不同)。只能在着色器里只读(渲染阶段),由驱动高效缓存。
  3. SSBO(Shader Storage Buffer Object)/ Structured/Raw Buffer:大容量、灵活的“存储缓冲区”(D3D 的 StructuredBuffer/ByteAddressBuffer,Vulkan 的 Storage Buffer,Metal 的 device buffer)。限制更少,容量可远大于 UBO;可在计算着色器中读写(图形管线阶段一般读)。某些平台上访问成本可能略高于 UBO,但更通用

BatchRenderGroup演示Demo
https://nicoier.github.io/2025/12/18/BatchRenderGroup演示Demo/
作者
NicoIer
发布于
2025年12月18日
许可协议