ProjectS中的GPU Driven
架构
前阵子敲定了ProjectS GI方案的大体架构,但只对着那几个Cube可看不出落地效果,也没法做针对性的优化,所以准备启动地形相关的开发工作
立项之时就考虑到美术成本问题,所以采用程序化生成作为构建ProjectS世界的主要手段,程序化生成意味着大量重复instance和大地形,这时候就得利用GPU Driven来作为整个PCG系统的底层支柱之一了,我们常说的GPU Driven包括以下几个方面
地形渲染
制作
- Houdini 生成高度图+Mask VS 直接生成Mesh
- Houdini生成高度图+Mask方案对于Houdini来说更加轻量,不需要处理地形Mesh
- 直接生成最终Mesh可以离线对Mesh做处理,包括岩壁重展UV,LOD计算等
但对于ProjectS来说,没有太特殊的需求对Mesh做处理,一些岩壁拉伸问题也可以通过处理贴图渲染进行解决,最终选用高度图方案
渲染
- DrawInstanceIndirectly通用Quad Mesh + LOD + 高度图采样 实现地形渲染 VS Mesh Cluster Rendering + LOD 实现地形渲染
- 通过Transform信息+高度图采样绘制Quad Mesh,并根据距离相机的信息得到LOD信息
- Mesh Cluster Rendering核心思想是将Mesh离线转换成固定顶点的Mesh(例如64),顶点数相同了,那么我们就可以保存一份顶点的索引和Transform信息,即可通过一次DrawCall完成绘制。但由于这种绘制方式类似于乱序重绘,无法使用常规的渲染阴影,需要自己实现一套阴影系统
- 将Mesh拆分成固定顶点数的Cluster,即每64个顶点组成的一组Mesh,而每个cluster中,都存在16组quad,每个quad是4个顶点,也就是两个并在一起的三角形,这样相当于在绘制无数个64个顶点组成的模型
- 保存每个顶点的索引和Transform信息,运行时通过SSBO传入GPU,用于后续的GPU DrawCall
- 在VS中根据vertex index求出cluster index,然后再从cluster信息中读取instance index,最后从Transform Buffer中获取到相应数据的进行绘制即可
考虑到Mesh Cluster Rendering的实现成本,最终选用Quad Mesh Patch方案
场景流式
- 四叉树场景管理方案,细分地图区块,抽象成Node,Streaming到GPU做剔除渲染
剔除
- Hi-z剔除方案,通用于地形剔除和Instance剔除
地面材质
- 传统Weight Blend方案:通过纹理 + Mask图实现多纹理混合渲染
- 每多一张纹理贴图,就要多一份遮罩数据。
- 由于每点的mask总和为1,所以改变一层需要动到其他所有层的数据,耦合度太高,不方面大规模修改迭代。
- 编辑器在处理edit layer时,由于需要全局的归一化操作,所以会让上层的layer表现非常奇怪。
- 随着edit layer的增加,内存和操作延时也会是个问题。
- 渲染时由于每个地块使用的weightmap各不相同,所以加大了合批处理的难度。
- Material Id方案:确保每个点只有一种纹理贴图的情况下,通过一张Material Id图控制,最后通过双线性插值混合平滑,
这里的材质不是指材质球,而是指一个数据结构,其中包含材质参数,贴图索引以及其他信息
- 优点是这张MaterialId,它相当于一个间接的索引,每个值表示一张纹理图。8位的单通道materialID图就足以支持超过200种纹理图。它的大小可控,不会随着纹理数量的增加而增加。同时,由于不需要归一化操作,在支持多层edit layer方面也比较简单,保证上层layer覆盖下层即可。
- 刷地形纹理的时候记录纹理值,例如0,1,2,3。。。然后从materialID图中得到具体的纹理
所有纹理通过Unity的TextureArray进行存储
- materialID的使用让纹理数量不成问题,但相较于weightmap算法有个明显的缺点,就是边缘过渡比较生硬。为了解决这个问题,可以增加materialID的信息,做双层material混合。可以想象成为抹在蛋糕上的奶油(爆炒。
- 8位Material Id A + 8位Material Id B + 混合权重 + 8位其他, B会覆盖在A上
- 如果追求高品质的地表效果,可以在TextureArray加上法线,粗糙度,ao贴图等,通过自定义的Index规则进行索引
- 优点是这张MaterialId,它相当于一个间接的索引,每个值表示一张纹理图。8位的单通道materialID图就足以支持超过200种纹理图。它的大小可控,不会随着纹理数量的增加而增加。同时,由于不需要归一化操作,在支持多层edit layer方面也比较简单,保证上层layer覆盖下层即可。
Instance
场景中会有大量的草,花,石头等instance物件,之前看到过地平线零之曙光的GDC分享,思路是设计一套运行时算法,实时根据各种mask做混合算法达到实时种植的目的,这种方案的优势在于:
- 相较于流水线化的instance mask制作方案:需要同整个PCG管线耦合,如果修改了一些会影响其余PCG模块的Mask,需要rollback到Houdini后端做重烘焙,烘焙完再根据变化后的mask进行调整,运行时的种植方案可以让美术从制作,调整灰度图到引擎看到效果时间成本非常低,基本上可以做到引擎里实时更改,所见即所得
- 整套贴图混合算法由自己决定,更易于掌控各种instance的层级,blend
与之相对应的,就是这一整套流程开发下来有极高的成本,需要保证算法的高效,稳定,以及一系列相关的工具链,例如预览,debug等,就像这样:
所以ProjectS最终还是选择了更加常见的同地形PCG紧密结合的Instance种植方案,在Houdini里离线处理好不同layer的instance mask图,虽然整个验证过程耗时会长很多,但胜在简单,稳定,开发量低
RVT
大概的过程就是FeedBack取出当前所需的各位置下的mipmap等级贴图,然后填入pagetable这张表,pagetable取出信息加载对应等级贴图(RVT实时烘焙,SVT从预处理贴图提取),然后烘焙到TileTexture上。加载完后会去更新lookup贴图,把当前显示的信息存入到里边供VT渲染地形使用。在渲染地形时,通过uv去lookup贴图找出当前格子在TileTexture上的格子坐标,以及uv偏移,取出TileTexture上的Diffuse,法线,Mask等参与光照计算。
意义:
- 预渲染地面材质到RT,减少地表材质渲染消耗,当然了渲染到RT的过程和正常地表着色一样的消耗,优势只是在于多帧缓存的tile不需要重复渲染,即未使用RVT渲染1000个tile,使用RVT,第一帧渲染1000个tile,然后平均每帧渲染几十个tile
- SSD的过度OverDraw问题也可以通过RVT解决,并且可以很好的处理贴花和表面的过渡效果
- 简单处理就能很好处理地表和物体之间的过渡效果
- 更加精确的解决草,道路等物体渲染不贴地问题
GamePlay
既然整套地形和Instance都是GPU Driven的,那么传统依赖地形的GamePlay模块就需要进行修改了,例如地形碰撞,寻路等
详情可参考:《天谕》手游的体素方案实践
参考
文中提及的各种技术方案经过这些年的发展,已经比较成熟,社区也可以轻松找到相应的教程和资源,有兴趣的同学可以了解一下
GPU Driven Pipeline — 工具链与进阶渲染