Instancing 与 Indirect Draw¶
实例多起来以后¶
场景里的草和树多起来以后,CPU 每帧都要遍历对象、做视锥裁剪,再把 Draw Call 提交给 GPU。实例数量到几万时,即使材质都一样,主线程也会先顶不住。
这种情况可以先试 GPU Instancing,再考虑 Indirect Draw。两者不是替代关系,Indirect 只是把“画多少个”交给 Buffer 决定。
先分清两种提交方式¶
| 方式 | 谁决定实例数量 | 适合场景 |
|---|---|---|
| GPU Instancing | CPU 调用时传入 | 数量中等、对象变化不频繁 |
| Indirect Draw | GraphicsBuffer 或 Compute Shader | 大规模实例、GPU 裁剪 |
普通的 Graphics.RenderMeshInstanced 已经能处理很多情况。只有当实例数量大到 CPU 裁剪和提交成为瓶颈时,Indirect Draw 才有意义。
用 Buffer 提交绘制参数¶
Unity 的 Graphics.RenderMeshIndirect 从 GraphicsBuffer 读取绘制参数。不要自己按 int 拼布局,使用 GraphicsBuffer.IndirectDrawIndexedArgs 更稳,因为不同图形 API 的字段布局可能不同。1
private GraphicsBuffer _argsBuffer;
private void Draw(Mesh mesh, Material material, int instanceCount)
{
var args = new GraphicsBuffer.IndirectDrawIndexedArgs[1];
args[0].indexCountPerInstance = mesh.GetIndexCount(0);
args[0].instanceCount = (uint)instanceCount;
args[0].startIndex = mesh.GetIndexStart(0);
args[0].baseVertexIndex = (uint)mesh.GetBaseVertex(0);
args[0].startInstance = 0;
if (_argsBuffer == null)
{
_argsBuffer = new GraphicsBuffer(
GraphicsBuffer.Target.IndirectArguments,
1,
GraphicsBuffer.IndirectDrawIndexedArgs.size);
}
_argsBuffer.SetData(args);
var rp = new RenderParams(material)
{
worldBounds = new Bounds(Vector3.zero, Vector3.one * 500f)
};
Graphics.RenderMeshIndirect(rp, mesh, _argsBuffer, 1);
}
private void OnDestroy()
{
_argsBuffer?.Release();
_argsBuffer = null;
}
worldBounds 不要随手写一个特别大的值。Unity 会把它当成整批实例的包围盒做剔除和排序,设得过大,整批内容会被一直保留在可见列表里。
把裁剪移到 GPU¶
Indirect Draw 常见流程如下:
- 把实例位置、缩放和包围盒写入
GraphicsBuffer。 - Compute Shader 做视锥裁剪,把可见索引写入另一个 Buffer。
- 更新 Indirect Args 里的实例数量。
- 提交一次 Indirect Draw。
- 调试时回读数量,检查裁剪结果是否符合预期。
裁剪的核心代码不复杂:
[numthreads(64, 1, 1)]
void Cull(uint3 id : SV_DispatchThreadID)
{
if (id.x >= _InstanceCount)
return;
Instance inst = _Instances[id.x];
if (!VisibleAgainstPlanes(inst.Bounds, _Planes))
return;
uint slot;
InterlockedAdd(_Args[1], 1, slot);
_VisibleIndices[slot] = id.x;
}
这里 _Args[1] 表示索引绘制参数中的实例数量,但不同平台的参数布局可能不同。实际项目最好用 Unity 提供的结构体或平台宏来定位,不要直接把下标当成跨平台常量。2
什么时候不值得¶
如果场景只有几百个对象,CPU 提交成本通常还不高。加 Compute Shader、同步和额外 Buffer 之后,反而可能更慢。
可以先看三个指标:
- 主线程的
Camera.Render和批处理耗时是否明显。 - GPU 是否已经吃满,单纯减少提交不会带来收益。
- 实例数量是否足够大,且大部分时间真的在相机外或被遮挡。
另外,透明物体通常还需要 CPU 排序。Indirect Draw 解决的是提交和裁剪,不会自动解决透明排序问题。
一个调试技巧¶
先不启用 GPU 裁剪,只用固定的 instanceCount 跑一遍 Indirect Draw,确认矩阵、索引和材质属性都正确。再把 Compute 裁剪接上,对比可见数量。
如果画面突然少了一大片,先检查 worldBounds、视锥平面和矩阵的空间是否一致。很多时候不是裁剪算法写错了,而是实例数据还在本地空间,平面却传了世界空间。