距离上次写GPU-Driven Rendering Pipelines的文章已经块一年了,当时有一个疑问,为什么要把mesh拆分成固定拓扑结构的Cluster,带着这个疑问,再次出发,发现之前写的文章只是GPU-Driven Rendering Pipelines的皮毛,这次想写一些进阶的知识,其实后续还有很多问题需要处理,先不急一步一步来。
在说GPU-Driven Rendering Pipelines之前先聊聊合批技术,毕竟GPU-Driven Rendering Pipelines有一个口号是一个DrawCall渲染整个场景,这篇文章不讲虚拟贴图,只说mesh的处理部分。
静态合批
所谓静态合批就是在编辑器里将不同的Mesh,Instance合并成一个大Mesh的做法。
优点
- 合并过程不占用运行时
- 可以对不同Mesh进行合批
缺点
- mesh不容易被剔除,因为合并的Mesh拥有一个比较大的包围盒,且包围盒不紧致。
- 顶点buffer中有大量重复的顶点和索引,因为相同的mesh,不同的instance,会被当成不同的mesh进行合并。
- 合并的索引数不能超过65536(16bit)
Instancing技术
D3D12只有两个DrwaCall接口DrawInstanced以及DrawIndexedInstanced,这两个接口都支持Instance渲染,所谓的Instance渲染,就是对于具有相同mesh的Instance,我们可以通过一个Drawcall把它们渲染出来。我们需要一个Instance buffer记录每个Instance的特有的数据比如Transfrom,在VS中我们根据Instance_Id来获取这些数据。
优点
- 顶点buffer没有重复的顶点和索引
- 单个mesh可以使用更多的索引
- 不需要占用CPU时间进行顶点和索引数据的合并
缺点
- 不支持多Mesh多Instance合批
Merge-Instancing
硬件Instance技术有一个缺点就是不能处理不同mesh,不同Instance的合并,Merge-Instancing技术就为解决这个问题提出的。首先我们需要将不同的mesh的顶点和索引,合并到一个大的Vertex Buffer及Index Buffer中,为了突破DrawCall接口的限制,我们需要手动fetch vertex。算法如下图:

无论怎样我们需要执行vs的次数=instace_count * perInstace_Index_count,上图的vertex_count就是vs执行的次数。因为每个mesh的顶点不同,我们不能使用Instance技术,我们只能将这些Instance当成是一个大Instance渲染(1个Instance,包含vertex_count个索引)。为了完成VS我们需要获取vertex数据(通过Index获取)以及Instacne data(通过Instance_id获取)。而目前的VS中只能获得vertex_id(这个id只是vs执行到了第几个顶点,也就是上图中的vertex)我们该如何获取顶点索引以及intance_id呢?
这就是Merge-Instancing的核心技术,每个mesh具有相同的拓扑结构,也就是具有相同的索引数量(上图的freq),如果拓扑结构固定,那么intance_id和顶点的索引就可以通过vertex_id求得了,如上图。使用Merge-Instancing就可以使用一个DrawCall渲染整个场景了(不考虑shader,贴图,buffer限制等其他因素)。
固定拓扑结构也就意味着如果某个mesh的索引数量小于这个值,就需要添加退化三角形来补充,这也是这套方案的缺点之一。另外我们注意到vs的执行次数和每个mesh的索引数量相关因此使用TRIANGLE_LIST和TRIANGLE_STRIP会直接影响vs的执行次数,TRIANGLE_LIST执行的次数会是TRIANGLE_STRIP的3倍。
Mesh Cluster Rendering
Mesh Cluster Rendering的主要目的是希望利用gpu高并发的特点,进行更精确的剔除操作。这个技术在预处理阶段会把mesh拆分成多个cluster,每个cluster会配备包围盒,backface mask,cone culling data等,这些数据都是为了做更精确的剔除,后面会去讲。
为什么每个Cluster包含三角面片是固定的?因为每个Cluster是由一个线程组处理,每个三角形由一个线程处理,为了保证算力能够充分利用,所以cluster需要固定数量的三角面数量。
为什么刺客信条是64 vertex strip,这个是根据不同的显卡以及项目实际性能测量确定的,对于n卡是32的倍数,对于a卡是64的倍数。
Merge-Instancing + Mesh Cluster Rendering
这两个技术加起来可以实现一个DrawCall渲染不同mesh不同Instance并且可以进行更加精确的剔除操作。但是它也由缺点,Assassin’s Creed Unity中指出其缺点:
- Memory increase due to degenerate triangles,每个Instacne之间需要通过退化三角形补齐,每个Mesh的最后Cluster也需要退化三角形补齐。
- Non-deterministic cluster order,首先为什么要对cluster排序,因为如果按照深度排序,可以减少overdraw(还有什么其他原因我就不知道了)。看Assassin’s Creed Unity的Paper中说到这个方法使用的是DrawInstanced接口,也就是说VS中只能拿到vertex_id。不过此时的情况和Merge-Instancing不同,这里的vertex buffer经历了各种剔除已经不是完整的vertex buffer了,因此肯定需要一个间接索引缓存,这个索引缓存中存储的是实际的vertex_id,然后通过这个vertex_id计算instance_id以及顶点的Index,这个间接的索引缓存是通过一个全量缓存计算得到的(全量缓存中标记所有三角面片是否被剔除,全量保证线程的并行性)。使用这种技术就决定了Cluster无法进行排序。
MultiDrawIndexedInstancedIndirect + Mesh Cluster Rendering
首先D3D12没有这个接口,这个接口的意思就是间接调用多次DrawIndexedInstanced,与上面的技术不同之处在于:
- One (sub-)drawcall per instance
- Requires appending index buffer on the fly
- 不需要退化三角形了
- 也可以Cluster排序了
这里有个细节上面的方案没有使用索引Buffer,主要原因是使用了三角形条带(顶点顺序即使索引),现在这套方案使用了索引Buffer,这里就有两种实现方式,一种是自己管理索引Buffer(不SetVB及SetIB),一种是使用VB和IB,如果使用三角条带的话,这两种方案区别不大,但是如果使用三角列表的话,自己管理索引Buffer则会调用更多次的VS,因为我们不会去做vertex fetch cache。所以这里建议使用VB和IB利用硬件的vertex fetch cache可以减少vs的调用,这也是为什么上面的方案使用的是DrawInstanced接口而这套方案使用的是DrawIndexedInstanced。
具体实现方式我在后面会详细介绍,先看一个完整的Pipeline:

第一步粗裁剪
这一步通常是大面积的剔除,比如四叉树剔除,Assassin’s Creed Unity是在cpu端做的,Far cry 5的地形系统是在gpu端做的,具体放在哪里做看实际需求。
第二步更新Instance数据
第三步Batch DrawCall
- 如何设置输出的Index buffer,预先设置固定大小的Buffer,大小是128K的N倍,为什么是128K,应该是和显卡的缓存相关。
- 异步计算,culling和rendering需要异步进行,如果是异步的话index buffer至少是同步的2倍,否则就需要同步等待了。
第四步Instance裁剪
- 根据Instance的包围体进行视锥体及遮挡剔除
- 保留的Instan


1377

被折叠的 条评论
为什么被折叠?



