再谈GPU-Driven Rendering Pipelines

距离上次写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
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值