UE_Visibility Buffer & Deferred Material

在这里插入图片描述

一、Visibility Buffer

可见性缓冲区(Visibility Buffer)是延迟渲染的进阶处理。
与延迟渲染结合起来更好理解,即:延迟渲染是将材质与光源照明计算进行解耦,基于此种思路将几何体与材质解耦:

  • 每个像素/三角形的材质仅被处理一次,并且在管线其余过程中不会处理到被遮挡的纹理、缓冲区或资源,这种整体的渲染思路便是Visibility Buffer的处理思路。

在此之前我们先来看一下为什么要使用Visibility Buffer,这就得说到延迟渲染的弊处了:

通常来说,延迟渲染管线都需要一组称之为G-Buffer的Render Target,这些贴图内存储了一切光照计算需要的材质信息。当今的3A游戏中,材质种类往往复杂多变,需要存储的G-Buffer信息也在逐年增加。相同分辨率的情况下,由于材质复杂度和逼真度的提升,G-Buffer需要的带宽足足提高了一倍,这还不考虑逐年提高的游戏分辨率的因素。

对于overdraw较高的场景,G-Buffer的绘制产生的读写带宽往往会成为性能瓶颈。于是学界提出了Visibility Buffer的新渲染管线。基于Visibility Buffer的算法不再单独产生臃肿的G-Buffer,而是以带宽开销更低的Visibility Buffer作为替代

比较其两者的渲染管线区别可见下图所示:

在这里插入图片描述

其中Visibility Buffer管线包含了三个阶段:

  • Visibility Passes: 对场景进行光栅化,将Primitive ID和Instance ID(或Material ID)保存到ID Texture里(顺手做个Depth Prepass),也就是说只有可见的Primitive才会进入后续的阶段;

  • Worklist Pass:构建并Worklist,这一步是为了驱动下一步,将屏幕划分成很多tile,根据使用到某个Material ID的tile加到该Material ID的Worklist里,作为下一步的索引;

  • Shading Passes : 使用Compute Shader对每个Material ID进行软光栅,获取顶点属性并插值,然后再进行表面着色。

接下来我们来看一个以输出一个1080P图像为例不同buffer下的大小:
在这里插入图片描述
在这里插入图片描述
从上图中可以明显看出两种buffer对内存的消耗情况。

而UE5 Nanite 的一大特色是Visibility Buffer。它是一个 R32G32_UINT 纹理,包含每个像素的三角形和深度信息。此时不存在材料信息,具体可视化标识如下:
在这里插入图片描述

一般Visibility Buffer通常需要这些信息:

  • (1)InstanceID,表示当前像素属于哪个Instance(16~24 bits);

  • (2)PrimitiveID,表示当前像素属于Instance的哪个三角形(8~16 bits);

  • (3)Barycentric Coord,代表当前像素位于三角形内的位置,用重心坐标表示(16 bits);

  • (4)Depth Buffer,代表当前像素的深度(16~24 bits);

  • (5)MaterialID,表示当前像素属于哪个材质(8~16 bits);

以上,我们只需要存储大约8~12 Bytes/Pixel即可表示场景中所有几何体的材质信息,同时,我们需要维护一个全局的顶点数据和材质贴图表,表中存储了当前帧所有几何体的顶点数据,以及材质参数和贴图。

在光照着色阶段,只需要根据InstanceID和PrimitiveID从全局的Vertex Buffer中索引到相关三角形的信息;进一步地,根据像该素的重心坐标,对Vertex Buffer内的顶点信息(UV,Tangent Space等)进行插值得到逐像素信息;再进一步地,根据MaterialID去索引相关的材质信息,执行贴图采样等操作,并输入到光照计算环节最终完成着色,有时这类方法也被称为Deferred Texturing。

直观地看,Visibility Buffer减少了着色所需要信息的储存带宽(G-Buffer -> Visibility Buffer);此外,它将光照计算相关的几何信息和贴图信息读取延迟到了着色阶段,于是那些屏幕不可见的像素不必再读取这些数据,而是只需要读取顶点位置即可。基于这两个原因,Visibility Buffer在分辨率较高的复杂场景下,带宽开销相比传统G-Buffer大大降低。但同时维护全局的几何、材质数据,增加了引擎设计的复杂度,同时也降低了材质系统的灵活度,有时候还需要借助Bindless Texture等尚未全硬件平台支持的Graphics API,不利于兼容。

二、Deferred Material

我们都知道实际项目场景中会有大量材质,这些材质可能对应不同的 Shader,即使 Shader 相同还有可能会引用不同的 Textures。Visibility Buffer 本质上属于 Deferred Shading,而 Deferred Shading 最大的缺陷就是难以处理不同材质\Shader。为了解决这个问题,业界也提出了一些解决方案,其中比较有效的是 Deferred Material(也有称 Deferred Texture)

Deferred Material 的核心思想是将材质分类,找出每个材质对应的像素进行 Shading。因此需要将 Visibility Buffer 像素按材质分类处理。

其实主要就是在屏幕上渲染Material Id这种信息,然后统一的在screenspace把material data读出来用,之后可以自行做shading或者填充gbuffer。

可以来直观的看一下UE5 Nanite 生成的Material ID Buffer,它并未存储在一张UINT类型的贴图,而是将UINT类型的Material ID转为float存储在一张格式为D32S8的Depth/Stencil Target上,理论上最多支持2^32种材质(实际上只有14 bits用于存储Material ID),而Nanite Mask会被写入Stencil Buffer中。

在这里插入图片描述

由于是屏幕空间处理,最直接的想法是每个材质绘制一个全屏 Quad,找出匹配的像素进行 Shading。如果场景中材质数量为 N,屏幕像素为 M,总计需要 NxM 次的计算开销,如果场景中有大量的材质,这样的性能将无法接受。可见UE5 Demo中山谷的材质复杂的如下图:
在这里插入图片描述

另一种比较容易想到的方案是只处理每个材质对应的像素,最终每像素只会计算一次。但是现实很骨感,在实现上为了并行处理,必须用到原子操作,而原子所带来的同步开销,最终会抵消掉这个优化,另外还需要保证数据连续存储,否则无法实现一个 Indirect Dispatch 处理同材质的所有像素,中间要经过几个 Compute Pass 来回倒腾数据,最终所有这些加起来总开销更大,显然这也不是最佳的解决方案。

2.1 Material Culling

UE5 借鉴了几何处理的方法,很巧妙地来解决这个问题,称之为 Material Culling 。Visibility 像素经过 Material Culling 之后,实现了每像素只计算一次的效果,并且没有太多额外的性能和内存开销,总体上达到了很好的性能平衡。

同几何处理类似,Material Culling 分为基于 Tile 的粗剔除和基于 Pixel 的细剔除 2 种剔除粒度。从宏观流程上来看,先根据 Visibility Buffer 生成 Material Depth,接下来将屏幕划分 Tile,记录每个材质对应的 Tile 列表,最后每材质绘制对应数量的 Tile,并使用 Material Depth 剔除无效像素,只对有效像素 Shading,从而提升了整体性能。

Nanite在Base Pass绘制阶段并不是每种材质一个全屏Pass,而是将屏幕空间分成若干8x8的块,比如屏幕大小为800x600,则每种材质绘制时生成100x75个块,每块对应屏幕位置。为了能够整块地剔除,在Emit Targets之后,Nanite会启动一个CS用于统计每个块内包含的Material ID的种类。由于Material ID对应的Depth值预先是经过排序的,所以这个CS会统计每个8x8的块内Material Depth的最大最小值作为Material ID Range存储在一张R32G32_UINT的贴图中:
在这里插入图片描述
有了这张图之后,每种材质在其VS阶段,都会根据自身块的位置去采样这张贴图对应位置的Material ID Range,若当前材质的Material ID处于Range内,则继续执行材质的PS;否则表示当前块内没有像素使用该材质,则整块可以剔除,此时只需将VS的顶点位置设置为NaN,GPU就会将对应的三角形剔除。由于通常一个块内的材质种类不会太多,这种方法可以有效地减少不必要的overdraw。实际上通过分块分类减少材质分支,进而简化渲染逻辑的思路也并非第一次被提出,比如《Uncharted 4》在实现他们的延迟光照时,由于材质包含多种Shading Model,为了避免每种Shading Model启动一个单独的全屏CS,他们也将屏幕分块(16x16),并统计了块内Shading Model的种类,根据块内Shading Model的Range给每个块单独启动一个CS,取Range内对应的Lighting Shader,以此避免多遍全屏Pass或者一个包含大量分支逻辑的Uber Shader,从而大幅度提高了延迟光照的性能。

在完成了逐块的剔除后,Material Depth Buffer就派上了用场。在Base Pass PS阶段,Material Depth Buffer被设置为Depth/Stencil Target,同时Depth/Stencil Test被打开,Compare Function设置为Equal。只有当前像素的Material ID和待绘制的材质ID相同(Depth Test Pass)且该像素为Nanite Mesh(Stencil Test Pass)时才会真正执行PS,于是借助硬件的Early Z/Stencil我们完成了逐像素的材质ID剔除。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值