Vulkan中的环境遮挡

作者: Lawrence Green

你可能已经注意到,在现实生活中,环境光比其他光线更难进入某些表面,例如衣服的折痕或墙壁的内角与天花板相交的地方。这意味着到达表面上某一点的环境光量可以作为周围局部几何体的函数进行估计;这种估计称为环境遮挡。

环境光遮挡是物理世界中一个看似简单的照明功能,但在计算机图形学中实现起来似乎很复杂。在本篇博文中,我们将介绍桌面环境中使用的一些算法,和一种可以在移动设备上使用的算法,以及您可以采取哪些优化措施来改进该算法。我们甚至在即将发布的SDK的21.2版本中给出了一个代码示例,敬请期待。同时,本篇博文将回顾在开发上述代码示例期间探索的一些研究和概念。希望你会觉得这很有趣!


环境遮挡算法

目前3D图形中使用的主要算法有四种:

  • 屏幕空间环境遮挡 Screen space ambient occlusion
  • 水平基准环境遮挡 Horizon-basedambient occlusion
  • 体素加速环境遮挡 Voxel-accelerated ambient occlusion
  • 光线追踪环境遮挡 Ray-traced ambient occlusion

这些是按照复杂性和资源需求的顺序呈现的——屏幕空间环境遮挡(这里称为SSAO)比体素加速环境遮挡简单且轻量得多。光线追踪方法要实用,需要专门的硬件,我们希望将来能为您演示这一点。

为了简单起见,我们现在将重点关注SSAO。SSAO的工作原理是首先进行Gbuffer过程——这允许通过有关场景法线和位置数据的信息来估计场景中的几何体。对于每个片段,我们在该片段的视图空间位置周围的邻域中获取伪随机分布的样本;场景几何体内部的该邻域中的每个点都会计入该片段的遮挡量。


初始实现

SSAO天然地适合延迟渲染流水线;计算视图空间位置,使用该数据生成SSAO,然后将其中的环境照明因子与反照率(albedo)纹理合成,这样我们就可以生成完全着色的场景。

Vulkan要求用户将渲染工作划分为子过程(subpass),然后将这些子过程分组为渲染过程(renderpass)。在最简单的情况下,用户只有一个渲染过程和一个子过程。使用子过程允许用户以高效的方式将渲染工作链接在一起,只要它们以相同的帧缓冲区对象为目标。

多个子过程可以是同一渲染过程的一部分——这允许GPU使用本地像素存储将片段数据作为输入附件传递到下一个子过程。这大大降低了所需的带宽和纹理处理量。但是,在这种情况下,不可能对每个渲染过程使用子过程;这是因为子过程只允许在与当前执行的片段完全相同的片段坐标下读取输入附件。SSAO算法需要访问当前片段周围半径内的几何体数据,因此需要多个不同的渲染过程。

对于初始实现,至少需要两个渲染过程;如下所示。

Vulkan中的环境遮挡
原始实现的第一个渲染过程:反照率附件和视图空间位置附件

第一个渲染过程生成一个Gbuffer。此Gbuffer包含反照率附件和视图空间位置附件。使用视图空间位置附件会占用大量带宽;这需要优化,我们将在后面讨论。然后,这些附件将被用作下一个渲染过程的输入。

Vulkan中的环境遮挡
第二个渲染过程:环境光遮挡附件和结果显示在屏幕上

第二个渲染过程包含两个子过程;第一个生成环境光遮挡纹理,第二个通过局部像素存储接收该纹理,并将其与反照率纹理合成。最后的图像会显示在屏幕上。

生成环境光遮挡纹理

首先,我们将研究环境光遮挡的一个基本而直接的实现。这将作为以后改进版本的基准。

第一步是生成一个均匀、随机分布的样本球。这些采样被保存到uniform缓冲区对象,该对象将用于比较场景深度。之后,下一步是在空白渲染目标上绘制屏幕空间效果。然后,为每个片段生成环境遮挡。与其提交屏幕空间四边形,最简单的方法是绘制一个覆盖整个附件的巨大三角形。从三角形传递的纹理坐标将被插值,对于每次调用片段着色器,纹理坐标将表示当前片段的归一化(0,1)坐标。

对于“环境光遮挡”(ambient occlusion)纹理中的每个片段,在与当前执行的片段相同的坐标处采样“视图空间位置”(viewspace position)纹理。这为我们提供了当前片段的视图空间位置。之后,对于uniform缓冲区中的每个样本,样本的偏移量被添加到片段的视图空间位置,以生成样本的视图空间位置。然后使用投影矩阵将样本转换为屏幕空间;通过读取样本屏幕空间坐标处的视图空间位置纹理,我们可以获得与样本位于同一位置的场景的视图空间位置。这两个视图空间坐标的z值表示相机视图的深度。如果采样的深度值高于场景深度,则该采样位于场景的几何体内部,从而导致该片段被遮挡。

Vulkan中的环境遮挡
示例场景比较的横截面

在上图中,遮挡因子将被计算为8/20=0.4,但由于期望的结果是有更多遮挡因子的片段被着色为更暗,写入纹理的值实际上将为1–0.4=0.6。

初始实现中的缺陷

这个基本实现有一些问题,可能会导致一些视觉伪影,这可能使它不适合实际使用。第一个问题是,由于采样球体将一半的采样放置在表面的背面,因此平坦表面将获得不合理的大量遮挡。

除此之外,曲面上彼此相邻的点可能具有相似的几何图形。由于遮挡是作为几何体的函数计算的,因此用于每个片段的相同样本可能会导致片段与其周围片段具有相似的遮挡。在比例上,这会导致遮挡组合在一起形成带状。

对边缘周围场景深度的变化也没有得到正确处理。例如,如果一个平面直接放置在视图空间中另一个物体的前面,当尝试对靠近该平面边缘的场景片段进行采样时,该平面后面的所有采样都被视为在几何体内部,但情况可能并非如此。这样做的结果是平面边缘周围出现不必要的遮挡,从而产生一种称为光晕的效果。

Vulkan中的环境遮挡
初始实现中的视觉缺陷:平面遮挡、带状和光晕

接下来,我们可以看看解决这些问题的一些方法,并使算法更加健壮。


技术改进

法线定向半球采样

基本实现中最大的视觉缺陷之一是,即使是不应该被遮挡的平坦表面,也有一半样本落在地形内。因此,表面的颜色看起来比正常的暗,感觉不自然。这个问题的解决方案是只在半球中采集样本,然后确定这些半球的方向,使它们与曲面的视图空间法线对齐。这可以通过使用TBN矩阵把样本从切线空间转换到视图空间中来实现。

Vulkan中的环境遮挡
从切线空间到视图空间定向的采样半球:采样半球与TBN对齐

随机旋转样本

带状是因为没有采集足够的样本来区分附近曲面的遮挡量。通过随机旋转每个片段的样本,可以弥补取样不足。旋转是通过在z分量为0的单位圆上创建随机分布的向量来产生的。这样,在为旋转样本创建TBN矩阵时,切线向量作为垂直于法向量的向量,通过随机向量的施密特正交化方法计算出来。结果是,TBN矩阵在将样本变换到视图空间之前,还将围绕切线空间中的z轴旋转样本。

Vulkan中的环境遮挡
切线向量取决于随机向量的选择:随机旋转的法向量

无法在片段着色器中生成无限多个随机样本。因此,最好的结果是生成具有随机旋转半径的2D均匀缓冲区。然后,通过获取当前执行的片段坐标、uniform缓冲区大小相对于屏幕大小的取模结果,然后使用该结果作为索引,来确定选择的随机向量。

使用重复的随机向量会在环境光遮挡纹理上产生干涉图案。因此,需要添加模糊渲染过程。模糊渲染过程的内核需要与随机旋转缓冲区的大小相同。这是因为重复使用相同的随机旋转导致干扰图案,其大小与旋转缓冲器相同;通过在相同大小的半径上取平均值,可以消除这种干涉图案。因此,优选最小化随机旋转缓冲器的大小。

范围检查

减少光晕的最简单方法是停止采样超出采样半径之外的部分。使用简单的阈值判断会产生一种看起来不自然的效果,因此,最好的方法是先计算当前片段的深度和样本深度之间的距离,然后将该样本的遮挡效果乘以该距离的倒数。

该系数需要限定在0和1之间。幸运的是,使用 smoothstep 内置函数是可行的。假设检测到样本位于场景几何体内部,则样本对整体遮挡的贡献量可以计算为:

occlusion+= smoothstep(0.0, 1.0, 0.1 / abs(sceneDepth - fragPos.z))

其中 sceneDepth 是对应样本的场景深度,fragPos.z 是当前执行片段的深度。

移动优化

该应用的一个潜在瓶颈是纹理处理过载。应用可能会向纹理处理单元(TPU)发出过多请求,从而填充满队列,导致渲染器需要等待队列释放可用空间。在这里,我们将讨论一些减少这个问题的潜在方法。

可分离模糊

模糊内核的大小必须与随机旋转内核的大小相同——在当前的实现中,模糊内核的半径为n,这意味着模糊过程需要对每个片段的纹理函数调用n2次。这可以通过将模糊过程分为两个单独的渲染过程来减少。第一个过程在水平方向模糊环境光遮挡纹理,而第二个过程在垂直方向模糊。这将纹理访问的数量减少到每个片段2n。
降低离屏渲染目标的分辨率

环境光遮挡纹理的生成是程序中迄今为止最繁重的工作量,即使在优化之后也是如此。与其减少每个片段的工作量,不如通过缩小渲染目标的大小来减少被处理的片段数量。这对环境光遮挡纹理应用的延迟着色最终结果影响最小。这是因为后续的模糊过程;而对于屏幕较小的移动设备来说,细节的损失就更不明显了。

还可以将模糊过程的分离与降低分辨率结合起来。第一个模糊渲染过程可以半分辨率执行,而第二个模糊过程可以全分辨率执行。通过以全分辨率执行第二次模糊过程,它可以充当放大过程,同时还允许通过本地像素存储将最终的环境光遮挡纹理传递到合成渲染过程。

视图空间纹理重建

通过减少输入纹理中每个片段使用的数据量,可以进一步减少环境光遮挡过程中每个纹理提取的处理量。这可以通过使用深度缓冲区而不是视图空间纹理来实现。无论何时访问“视图空间”纹理,“环境光遮挡”过程都可以访问屏幕空间坐标。通过使用深度缓冲区中的视图深度,屏幕空间的(x,y)坐标和逆投影矩阵可用于重建视图空间坐标。这将输入纹理从每个片段三个浮点存储到只有一个浮点。

包含视图空间坐标的纹理也可以通过获取多个样本从深度缓冲区重建。这会进一步减少纹理处理——样本可以缓存,因为它们彼此靠近。然而,对于移动设备来说,有一个折衷办法:使用更多的片段处理来减少纹理处理。在代码示例中,由于在片段着色器中使用了投影矩阵,我们已经严重依赖片段处理。因此,对于这种特殊情况,保留了法线缓冲区。


最终确定的流水线

现在可以描述一个能够产生更好输出的扩展流水线:

Vulkan中的环境遮挡
第一个渲染过程:反照率附着、视图空间法线和深度缓冲区

首先,生成一个GBuffer。它有一个反照率,法线,深度附件。它们被用作流水线其余部分的输入。
接下来,使用改进的方法生成环境光遮挡纹理,该纹理以半分辨率渲染。该纹理将具有随机旋转产生的噪声。

Vulkan中的环境遮挡
第二个渲染过程:生成环境遮挡

从环境光遮挡渲染过程中获取半分辨率输出,并执行一部分高斯模糊;在这种情况下,纹理仅在一个轴上模糊。这仍然以半分辨率呈现。

Vulkan中的环境遮挡
第三个渲染过程:半模糊的环境遮挡附件

最后是最终渲染过程,它有两个子过程。第一个子过程完成环境遮挡纹理上高斯模糊的第二部分,方法是以相反方向模糊上一个过程中的纹理。在这种模糊的过程中,图像也会从半分辨率放大到与swapchain图像相同的大小。这样环境遮挡纹理就可以通过像素本地存储传递到最终的子过程。最后一个子过程将环境遮挡与反照率附件合成,以生成最终图像。

Vulkan中的环境遮挡
第四个渲染过程:第二次模糊和最终结果。


结束语

环境遮挡是计算机图形学中许多人已经深入探索和研究过的一个领域,我们希望这篇文章能够让那些对产生逼真效果带来的一些挑战感兴趣的人更容易理解这个概念。我们为我们的PowerVRSDK及其代码示例感到非常自豪,这只是我们如何构建它们的许多案例研究中的第一个——正如一开始提到的,环境遮挡的代码示例将作为21.2工具和SDK版本的一部分发布,我们将很快公布,请回头查看。不久之后,我们还将继续为其他代码示例发布一些研究材料。

如果您有兴趣了解更多关于各种图形技术及其实现的信息,请随时查看我们的文档网站,或者探索我们在Github上的SDK中的其他一些代码示例。

https://docs.imgtec.com/
https://github.com/powervr-graphics/Native_SDK/

英文链接:https://developer.imaginationtech.com/blog/ambient-occlusion-in-vulkan/

声明:本文为原创文章,转载需注明作者、出处及原文链接。

最新文章