GPU Driven 的静态物件渲染,听起来很高级,其实具体操作很简单,基础就是直接调用 Graphics.DrawMeshInstancedIndirect 这个 Unity 内置接口就可以了。但我们项目对这个流程做了一些优化,使得支持的实体数量有大幅提升。
这套系统主要也是公司的 TA 实现的,这里我也只简明扼要地介绍一下原理。
1、实现思路
整个 GpuDriven 的实现简图如下图所示:
CPU端收集好渲染所需要的数据之后,传给GPU进行剔除和整理,之后得到需要上屏的数据。之后通过异步回读,拿到各个类型是否需要渲染的结果,然后再调用 Graphics.DrawMeshInstancedIndirect 命令进行绘制。
与传统流程相比,主要是增加了剔除和异步回读,使得 DrawCall 数量没有任何浪费,在保证渲染结果的同时,尽可能地减少了实际绘制的实体数量。
2、数据结构
部分数据结构简单介绍如下:
2.1、实体数据:InstanceData
public struct InstanceData
{
public int MeshType;//表示这个实体的类型
public float3 Position;//世界坐标的位置
public uint ScaleAndRoate;//缩放和旋转值,用位预算合并在一起(会降低精度)
public float4 CustomData;//自定义数据,根据项目需求设置长度;
}
这里为了性能考虑,对数据尽可能地做了压缩,例如旋转和缩放就整合成了一个unit,4个字节。自定义数据,可以理解为一些特殊 Shader 特殊效果用到的数据,例如描边颜色、顶点形变等。这个数据是最大的一块,不建议每帧都传给GPU,而是通过AOI来收集,在数据有变化的时候再传给 GPU 。
2.2、网格类型数据:MeshData
网格类型记录每一个网格上屏需要的参数,包括网格、材质球等:
public class MeshData
{
public int MeshType;//网格类型ID,需要顺序排列
public Mesh mesh;//网格
public Material material;//材质球
//-----------------以下是一些自定义参数,可自行扩展------------------------
public float2 lodDistance;//Lod参数
public bool reciveShaodw;//阴影参数
}
网格类型数据的改变频率就很低了,基本上可以全局共用一套(或者一个文明、国家用一套)。这里需要注意的是,数据存储时需要将 MeshData 存储为一个数组,且数组的 Index 和 MeshType 一一对应。
2.3、裁剪后的 Buffer 数据
裁剪后的 Buffer 数据是写在 GPU 里的,除了标记哪些类型需要渲染外,其他数据都是不会异步回读到 CPU 的。这里的数据就随便写了,举个例子:
StructuredBuffer<InstanceData> _IndirectAllInstanceDatas;//所有Instance的几何信息以及自定义的数据
StructuredBuffer<uint> _IndirectInstanceTypeIndexStart;//下标为Meshid,val为起始下标
StructuredBuffer<uint> _IndirectInstanceTypeIndexCount;//下标为Meshid,val为渲染数量
在回读 _IndirectInstanceTypeIndexCount 这个数据时,我们做了一个优化:由于CPU只需要知道每个类型是否需要渲染,所以是需要1位就能表示。我们将每8个类型合并成一个 int 返回,这样可以节省一点带宽。
3、关于裁剪
裁剪我们用了视锥体剔除、遮挡剔除和 LOD 剔除,一般来讲这三个结合起来就基本可以剔除得很干净了。剔除顺序以及介绍如下:
- LOD 剔除:将距离相机距离大于 LOD 设置的直接剔除视为不渲染;
- 视锥体剔除:根据相机矩阵计算,剔除掉不在视野内的物体;
- 遮挡剔除:根据上一帧生成的深度图,对当前帧的实体进行遮挡剔除计算。
在剔除的时候,用到了一些加速手段。例如使用 Cluster(或者叫Chunk)剔除加速优化:例如草,数量很多时,将其用莫顿码编每64个编成一个Chunk,然后整体做剔除。在视锥体剔除的时候,也可以先构建稀疏八叉树等等。(涉及的相关技术和参考文档我会放到最后)
这一套整体下来,百万物体的剔除在编辑器上测试只有 0.163 ms (GTX 1050ti),非常高效。
当然,我们实际实现的时候也遇到了不少问题,最常见就是单位闪烁的问题(主要原因是异步回读和深度图的数据并不是本帧的数据),后面也通过各种手段解决了。但这个技术路线本身原理是可行的。
参考文章
Graphics-DrawMeshInstancedIndirect - Unity 脚本 API
https://zhuanlan.zhihu.com/p/468542418
计算机图形学:使用体素锥体跟踪实现全局渲染_技术交流_牛客网
【Unity】相机视锥体剔除算法_unity视椎体剔除-CSDN博客
【Unity】LODGroup 计算公式_unity lod 计算viewdistance-CSDN博客
GPU Driven Occlusion Culling(Hiz)_hiz遮挡剔除-CSDN博客