渲染路径(Rendering Path)(Forward、Deferred、Light-PrePass、Tiled、Clustered)


目录
  • Forward Rendering(前向渲染)
  • Deferred Rendering(延迟渲染)
  • Light Pre-Pass/Deferred Lighting(延迟光照)
  • Tiled Shading
    • Tiled-based Deferred Rendering
    • Forward+ Rendering
  • Clustered Shading
    • Clustered-based Deferred Rendering
    • Clustered-based Forward Rendering
  • 参考

Forward Rendering(前向渲染)

Forward Rendering/Forward Shading(前向渲染/前向着色):传统的光栅化渲染流程中,场景中每个三角形都会被光栅化成若干个片元后进行着色计算。

image-20220221160220849

但是由于无法确保这些三角形绘制的先后顺序,相当多的经过着色计算后的片元会被覆盖(由于深度测试),这就造成了计算的浪费。在大量光源的情况下,这种计算的浪费更是致命的,因为三角形光栅化后的每个片元要经过与光源数量相等次数的着色计算。

Rendering 流程

  1. 绘制所有不透明的几何体:一次 Pass,先进行着色,再进行深度测试
  2. 绘制所有半透明的物体:同上,但关闭深度写入

Forward Rendering 缺点

  • 场景复杂度与光源数量相关,假设有 n 个物体,m 个光源,则渲染复杂度为 \(O(n\cdot m)\)
  • 会导致大量的 overdraw ,一些最终会被深度测试所覆写/丢弃的片元仍需要先经过着色计算,浪费了性能

Deferred Rendering(延迟渲染)

Deferred Rendering/Deferred Shading(延迟渲染/延迟着色):相比于传统的 Forward Rendering,它的核心目标是让场景复杂度和光源数量解耦。

image-20220221160304960

它的主要思路就是将着色计算延迟到深度测试之后,这样就可以仅对最终留在屏幕上的片元进行着色计算而不是对每个三角形片元都进行着色计算(也就是说最终需要着色计算的片元仅与屏幕像素数有关,与场景有多少个三角形无关)。

而为了不丢失像素点的几何属性等信息,Deferred Rendering 需要额外借助一个名为 G-buffer 的帧缓存区(FBO)来保存之,这样在延迟后的着色计算阶段就能读取出像素点对应的信息来作为参数,然后进行着色计算。

image-20220214220145315

Rendering 流程

  1. Geometry Pass(几何处理阶段):绘制场景中所有的不透明几何体,并将每个片元对应的法线、diffuse 反射率、specular 扩散系数等属性写入到 G-buffer 中。G-buffer 其实就是一个庞大的帧缓冲区对象(FBO),其一个元素包含相当多个属性,下图是一个 G-buffer 单个像素的布局例子:

    image-20220221151450297

    G-buffer 部分属性可视化:

    image-20220222175932765

    此过程需要在片元着色器中输出多个颜色值,因此要用到图形处理器接口中的 MRT 特性

  2. Lighting Pass(光照处理阶段):绘制出一个屏幕大小的2D矩形。在对屏幕每个片元着色时,对应的 G-buffer 元素则作为光照计算的输入参数,光源对每个片元的颜色计算结果被累加到?个累积缓存(Accumulate buffer),大体公式如下:

    \(L_{o}(\mathbf{v})=\sum_{k=1}^{n}\left(\mathbf{c}_{d i f f} \otimes f_{d i f f}\left(B_{L_{k}}, \mathbf{l}_{k}, \mathbf{n}\right)+\mathbf{c}_{s p e c} \otimes f_{s p e c}\left(B_{L_{k}}, \mathbf{l}_{k}, \mathbf{n}, \mathbf{v}, m\right)\right)\)

    其中,\(n\) 为光源数量,\(c_{diff}\)\(c_{spec}\) 代表表面的 diffuse 反射率和 specular 反射率,\(f_{diff}\)\(f_{spec}\) 分别为 diffuse 和 specular 的光照贡献

    公式的由来:渲染方程为

    \(L_{o}(\mathbf{p}, \mathbf{v})=L_{e}(\mathbf{p}, \mathbf{v})+\int_{\Omega} f(\mathbf{l}, \mathbf{v}) \otimes L_{i}(\mathbf{p}, \mathbf{l}) \cos \theta_{i} d \omega_{i}\),其中 \(f\) 为 BRDF

    如果仅考虑光源的直接光照,便可以简化成如下(即多个直接光源的贡献之和)

    \(L_{o}(\mathbf{v})=\sum_{k=1}^{n} f_{\text {shade }}\left(E_{L_{k}}, \mathbf{l}_{k}, \mathbf{v}, \mathbf{n}, \mathbf{c}_{d i f f}, \mathbf{c}_{s p e c}, m\right)\)

    再具体划分一下,光照一般由 diffuse 和 specular 两种贡献组成,并通过 diffuse 反射率和 specular 反射率来控制贡献率(材质的属性),也就有了上面的公式

  3. 绘制所有半透明的物体(按传统的前向渲染管线方法)

Deferred Rendering 优点

  • 渲染性能不再与场景的复杂度像耦合(支持数量巨大的光源),渲染复杂度变为 \(O(m+n)\),从而保证稳定的帧率,在大量光源的场景优势非常明显
  • 避免了 forward rendering 的 overdraw 问题。前向着色阶段后,剩下的就是被深度测试剔除之后的片元及其着色参数,因此它只对屏幕中最终可见的片元进行着色计算,避免计算浪费

Deferred Rendering 缺陷

  • G-buffer 占用了巨大存储空间,而且其读写也需占用大量显存带宽:
    • 通常 G-Buffer 中每个纹素可以占用多达 128bits 甚至更多的显存占用,当使用多重采样时更是会占用巨大的内存
    • 为了保证多个光源累加结果的精确性,颜色累积缓存还必须使用较高精度的变量
  • 不支持半透明物体:?个包含半透明表面的像素点的颜色值是两个(或多个)表面点颜色值混合的结果,而 G-Buffer 只保存每个像素点的?个表面点的值,无法正确表现半透明表面。因此即使是 deferred rendering ,也必须对半透明物体单独采用传统的渲染管线来处理
  • 只能使用同一shader,很难自定义多种不同的shader:延迟着色阶段会对屏幕区域的像素点进行着色计算,而不是像 forward rendering 那样根据每个物体的类型来绘制物体
    • 一种解决方法:G-buffer 额外引入物体 ID 属性,从而在延迟着色阶段可以读取出像素点对应的物体 ID 来调用不同的 shader,但开销大且不能支持太多种(分支计算开销大)
  • 需硬件支持 MRT
  • 对硬件 MSAA 的支持不友好

Light Pre-Pass/Deferred Lighting(延迟光照)

Light Pre-Pass,又叫 Deferred Lighting。Light Pre-Pass 和 Deferred Rendering 是不同的概念,简单来说,Light Pre-Pass 则是 Deferred Rendering 的一种改进流程。

核心目标是,减少 G-buffer 单个元素的大小,从而使得显存读写带宽大大减轻压力甚至于无需开启 MRT 特性也可实现的 deferred rendering 流程

主要思路是,首先第一个阶段将传统 deferred rendering 中 G-buffer 的部分属性舍弃掉(也就是不写入这些属性),然后第二个阶段根据已有的部分属性计算出光照着色的中间结果,最后阶段通过一个额外的几何 Pass 来重新计算出这些被舍弃掉的属性,从而得到完整的光照参数,计算出光照着色的最终结果

image-20220214220131734

注:Deferred Rendering 是在2004年的GDC上被提出的,而 Light Pre-Pass 则是在2008年被提出。而目前主流的游戏引擎中, Light Pre-Pass 方法被大量使用。

Rendering 流程

  1. **第一个 Geometry Pass **:绘制所有不透明几何体,相比于 Deferred Rendering,它仅将法线向量 \(n\) 和高光扩展系数(specular spread factor) \(m\) 写入到 n/m-buffer(类似于 G-Buffer 的缓冲区,但包含的信息更少,更轻量)

    由于法线向量占据 3 个分量且高光扩展系数是?个很小的整数,所以它们可以被合进?个 9 分量的颜色缓存即可,可不需要 MRT 的支持

  2. Lighting Pass:绘制每个光源影响范围的包围盒,并对该包围盒覆盖的每个屏幕像素点计算漫反射和镜面着色方程(相比于 Deferred Rendering,这个计算缺少了 \(c_{diff}\)\(c_{spec}\) 的参与),并将结果写入不同的漫反射和镜面反射累积缓存区:

    \(\begin{aligned} g_{d i f f} &=\sum_{k=1}^{n} f_{d i f f}\left(B_{L_{k}}, \mathbf{l}_{k}, \mathbf{n}\right) \\ g_{s p e c} &=\sum_{k=1}^{n} f_{s p e c}\left(B_{L_{k}}, \mathbf{l}_{k}, \mathbf{n}, \mathbf{v}, m\right) \end{aligned}\)

    光源包围盒不是真实存在的场景几何体,因此该阶段需要关闭深度测试;下图是一些光源范围几何体网格示例:

    image-20220302230710717

    当摄像机位于光源包围几何体内部时,应该只绘制该几何体的背面,否则只需要绘制正面

    对于?些体积特别大的光源(如直线光源或者光源影响范围同时包含了视锥体近平面和远平面),可以直接绘制?个 2D 的全屏平面

  3. **第二个 Geometry Pass **:再次绘制所有不透明几何体,但是此时并不需要对表面进行光照计算,而仅需要从前?阶段的两个颜色累积缓存中读取值并一般执行下列运算,将最终的着色结果写入到帧缓存中:

    \[L_{o}=\mathbf{c}_{diff}\otimes g_{diff}+\mathbf{c}_{spec} \otimes g_{spec} \]

  4. 绘制所有半透明的物体

优化

可优化的地方主要集中于延迟光照计算阶段输出的两个辐射照度量的存储表述(原始方法需要两个 3 通道的颜色缓存对象来存储)

  • CryEngine3 引擎使用?个 4 通道的颜色缓存 (A16R16G16B16f 或 A8R8G8B8)来同时记录漫反射和高光反射的 irradiance ;其中前 3 个通道表?漫反射值 \(diffuse\),第 4 个通道表示高光的强度 \(strength\),所以高光颜色值可以由 \(diffuse*strength\) 计算得出。这种方式仅保留了高光的亮度丢弃了色度(chrominance),但人眼对亮度的感应比色度更为明显,因此这种效果虽然不物理但仍能接受。
image-20220214164405692
  • YCoCg 压缩帧缓存则使用一个 4 通道的颜色缓存来强行压缩存储 diffuse 和 specular 的色度和亮度,效果也尚可接受
image-20220214165046994

Light Pre-Pass 优点(与传统的 Deferred Rendering 相比):

  • 减少 G-buffer 存储空间,大大降低了显存带宽的占用,甚至可以不需要硬件支持 MRT
  • Lighting Pass 是遍历每个光源,增加了光源设置的灵活性
  • 第二个 Geometry Pass 可以在绘制每个几何体时使用不同的 shader 进行渲染(例如让某个特定模型的边缘变红变亮),有更大的灵活性
  • 第二个 Geometry Pass 使得可以很方便支持 MSAA(传统的G-buffer由于是离散的网格存储,必不能展现完整的三角形信息,而新的 Geometry Pass 可以在遍历三角形时重新得到完整的三角形信息)

Light Pre-Pass 缺陷(与传统的 Deferred Rendering 相比):

  • Light Pre-Pass 重复读写多次 buffer,可能造成另一种显存带宽压力

    传统 Deferred Rendering 的 Lighting Pass 直接对屏幕每个像素着色,保证 G-buffer 的元素只被读一次,frame buffer 只写一次:

    for each pixel
        read G-buffer
        for each affecting light
            compute shading
        write frame buffer
    

    而 Light Pre-Pass 由于需要遍历光源范围包围盒,导致同一像素可能要读写多次 buffer :

    for each affecting light
    	for each pixel
        	read n/m-buffer
            compute shading
        	write frame buffer
    

    实际上无论是传统的 Deferred Rendering 还是 Light Pre-Pass,它们的 Lighting Pass 都可以采用先遍历屏幕像素的策略或者先遍历光源范围包围盒的策略:

    • 先遍历屏幕像素的策略,一个浪费性能的点在于:一些光源可能由于距离太远,本就不可能影响到某个像素,但对该像素着色时却又不得不遍历所有的光源并进行光照计算,造成了计算浪费
    • 而先遍历光源范围包围盒的策略可以避免这种计算浪费但读写带宽不友好

    我们可以想办法尽可能优化先遍历屏幕像素的策略,着色某个像素时可以尽量剔除掉无意义的光源,这样就能尽量减少计算浪费还能对读写带宽友好。

Tiled Shading

Tiled Shading 将屏幕区分划分为多个块(tile),每个 tile 覆盖的像素区域为32×32(也可以是别的分辨率),且拥有?个光源列表(包含所有在该 tile 内有影响的光源 ID )。这样就可以在对某个片元着色时,得到该片元所在的 tile,然后根据 tile 光源列表中的所有 ID 调用对应光源的着色计算,而不必调用全部光源的着色计算

image-20220221154108717

Tiled Shading 用到的具体数据结构如下:

  • 全局光源列表:存储着各光源的属性(如 radiant intensty 等)
  • 块光源索引列表:存储着各 tile 对应的光源 ID 列表,以数组的形式拼接在一起
  • 光源表格:用于查询某个 tile 的光源 ID 列表位于 tile 光源索引列表的哪里到哪里(数组中下标多少到多少)
image-20220223112021440

Tiled Shading 的 Light Culling 流程

实际上就是填充上述数据结构,一般用 GPU compute shader 来实现,有很多种方法,如:

  • 光源包围盒(光源影响范围包围盒)相交检测:一个 compute shader pass,对每个 tile,做以下步骤

    1. 根据 depth range 确定出该 tile 的包围盒

      tile 的 depth range:只需要求在 tile 中各个像素的 max depth(往往借助 Z-buffer)

    2. 让这个 tile 包围盒和各个光源包围盒进行相交检测,得到对这个 tile 有贡献的光源 ID 并插入到块光源索引列表尾部(数组尾部)

    3. 更新光源表格中对应位置的 offset 和 size

Tiled Shading 优点

  • 减少了相当部分的无关光源的计算

Tiled Shading 缺陷

  • tile 的光源列表信息与摄像机的变换、光源的变换相关,也就是说当摄像机(或者光源)位置和方向发生改变,tile 光源索引列表和光源表格都需要重新计算,带来一定性能开销

  • 基于屏幕空间的 tile 划分仍然粗糙,没有考虑到深度值(离摄像机远近的距离)划分

    如下图黑色线段物体,每个 shading point 本应该最多同时受到一个光源的影响,由于 tile 划分没有考虑 z 值,从而使得实际每个 shading point 都有三个光源的着色计算:

image-20220223155008338

Tiled-based Deferred Rendering

将 Tiled Shading 应用到 Deferred Rendering 时,只需要在 Lighting Pass 做稍微的改动(在填充好 Tiled Shading 数据结构的情况下)就可以了。

rendering 流程

  1. Geometry Pass
  2. Light Culling Pass
  3. Lighting Pass :片元着色时根据该片元所在 tile 的光源列表来累加计算着色(其实就是根据光源列表中的所有 ID 调用对应光源的着色计算)
  4. (Light Pre-Pass 可选)Geometry Pass

Forward+ Rendering

所谓 Forward+ Rendering,其实就是将 Tiled Shading 应用到 Forward Rendering

rendering 流程

  1. Z-prepass(很多 forward rendering 都会用这个作为优化,而在 forward+ 中变成了必然步骤)

  2. Light Culling Pass

  3. Forward Rendering Pass:绘制所有几何体,片元着色时根据该片元所在 tile 的光源列表来累加计算着色

Forward+ 优点(相比 Deferred Rendering):

  • 着色时可以通过 tile 方法过滤相当多不必要计算的光源
  • 由于增加了 Z-prepass,可以过滤掉相当多不必要计算着色的片元
  • forward rendering 天然支持各类不同复杂的材质
  • 不需要占用较高的带宽利用率
  • 可以支持透明物体
  • 可以很方便支持硬件 MSAA
  • 几乎所有主流移动平台的 GPU 是 tile-based 的,可以减少 tile-based 部分的算法代价

Forward+ 缺陷

  • 相比传统 forward rendering,增加了 Z-prepass + Lighting Culling Pass 的额外开销
  • 需要支持 compute shader 等高级特性,部分手机GPU并不支持

在一些PC性能实验中,凭借过滤了相当的光源及片元计算量和低带宽开销,Forward+ 性能 > Deferred Rendering 性能(甚至是 > 带宽需求最小的 deferred lighting 方法):

Clustered Shading

为了进?步剔除光源数量,clustered shading 在 tiled shading 的基础上,将像素分组的划分从 2D 的屏幕空间扩展到 3D 的观察空间。

每个 3D 的块称为?个 cluster,从而使每个光源真正做到仅影响其局部区域:

image-20220221154021172

空间划分

由于摄像机的透视投影,对于同样大小的物体,较远物体在屏幕上所占的空间更小;因此每个 cluster 不应该是均匀划分的,而是可以以指数形式划分深度方向(即更远的 cluster 体积更大)

image-20220301112638098

那么为了索引到一个 cluster,就必须有一个三维坐标来表示 \((i,j,k)\)\(i,j\) 其实和 tiled shading 的索引映射没有区别,而重点在于,给定一个深度值 \(z\),其在深度方向的索引值(即 \(k\) )如何计算。

\(near_k\)\(Z\) 方向上第 \(k\) 层 cluster 的近平面在 \(Z\) 方向上的值:

\(near_{k}= near_{k-1}+h_{k-1}\)

\(h_k\) 为在 \(Z\) 方向上第 \(k\) 层其中一个 cluster 的近平面长度

其中第 0 层 cluster(即朝摄像机的那面位于近平面的 cluster)有:

\(near_0 = near\)\(h_{0}=\frac{2 \text { near } \tan \theta}{S_{y}}\)

其中,视锥体的 \(Y\) 方向上的张角为 \(2\theta\)\(S_y\) 为屏幕空间 \(Y\) 方向划分的数量

\(h_k = h_{k-1}*2*tan\theta/S_y+d_{k-1}\)

\(d_{k-1}=h_{k-1}\)

\(\operatorname{near}_{k}=\operatorname{near}\left(1+\frac{2 \tan \theta}{S_{y}}\right)^{k}\)

此时,索引值 \(k\) 可被解出来:

\(k=\left\lfloor\frac{\log \left(-z\ / \text { near }\right)}{\log \left(1+\frac{2 \tan \theta}{S_{y}}\right)}\right\rfloor\)

法线分布划分(可选)

为了让 cluster 进一步剔除 light,还可以从背面剔除的角度出发,例如 cluster 内的各像素法线均背朝着 light ,那么就可以剔除之。

为此可以为 cluster 指定一个法线分布,索引键值额外增加 6 位(\(2^6=64\) 种法线分布)用来表示法线分布,这样 cluster 的索引就刚好凑成 32 位 \((i,j,k,normal)\)

可以假定一个类似于魔方(一面均分成3*3个格子)的立方体包围住 cluster 中心,然后该中心往各个格子的方向投射出一个锥形,一个锥形的范围便作为一种法线分布的范围(共3*3*6=54种锥形范围的法线分布,而索引键值的6位足以满足表示):

可能有些 cluster 的像素法线相差很大,一个锥形范围不能容纳所有的法线,此时可以调整锥形的半角来让锥形范围变得更大,但这也会让法线分布剔除光源的效率有所降低。在 Battlefield 中称这种技术为法线剔除(normal culling)

image-20220301164920296

如果入射光方向与法线锥形中心轴的夹角 \(\omega > \frac{\pi}{2}+\alpha+\delta\) ,则应该在该 cluster 剔除掉该光源

\(\alpha\) 是法线锥形的半角;\(\delta\) 时从光源发出的锥形半角,且这个锥形恰好包围住了 cluster 的AABB包围盒

Clustered Shading 的 Light Culling 流程

1. 找出所有参与计算的 cluster

因为屏幕上的所有像素(或片元)所涉及到的 cluster 数量一般远远小于所有 cluster 的总数量(换句话说实际上能用上场的 cluster 是稀疏的),我们只需要对会参与着色计算的 cluster 进行光源分配

方法一:

  • 每个像素点计算出 cluster index(cluster 索引值)后插入到一个数组
  • 对该数组进行 GPU 排序

GPU 排序是非常影响性能的,因此可以先对2D屏幕空间的单个 tile 内像素进行排序,而不是对整个屏幕所有像素进行排序。这样每个 tile 可以各自独立进行局部排序,刚好就可以充分利用 GPU 并行计算,且每个 tile 内的数据可以写入到本地共享缓存,而不是全局内存进行计算。

  • 对该有序数组去重,得到唯一 cluster index 列表(即包含所有参与着色计算的 cluster index)

image-20220301211011020

方法二:

  • 使用 virtual texture 来存储巨大内存的稀疏数据(通过动态分配页表来映射索引到稠密的物理内存去)

2. 对每个 cluster 分配光源

得到 cluster index 列表后,就可以遍历 cluster 了。

方法一(基于相交测试):

  • 对每个 cluster ,与所有光源包围盒进行相交测试,然后填充光源列表相关数据结构,其实方法与 tiled shading 差不多

方法二(基于DX12的保守光栅化技术):

所谓保守光栅化(conservative rasterization),就是光栅化时只要片元存在图元面积,哪怕只有一点点面积,也视为有效片元

image-20220302225313437

DirectX12 中通过创建一个管线状态对象时设置 ConservativeRaster 的标识为 D3D12_CONSERVATIVE_RASTERIZATION_MODE_ON 来开启保守光栅化

  • Shell Pass:绘制所有光源范围几何体,利用保守光栅化技术,绘制到 tile 分辨率的 buffer 上,并记录该光源在每个 tile 上的最大深度值和最小深度值
image-20220303212824699
  • Fill Pass:是一个 compute shader Pass,利用 shell pass 产生的最大最小深度值来填充范围内 cluster 的光源列表(tile和最大最小深度值组成了一段空间,这段空间范围里包含的所有 cluster 都会被该光源填充)
image-20220303212849699

3. 着色计算

这部分基本和 tiled shading 的着色计算差不多,取决于 forward rendering 还是 deferred rendering。只不过现在拥有了更加具有局部性的 cluster 光源列表

Clustered Shading 优点:

  • 进一步剔除了更多无关光源(相比 tiled shading 多考虑了深度乃至于法线分布的划分)

Clustered Shading 缺点:

  • light culling 流程变得更加复杂,可能需要多个 pass 才能完成 clustered shading 的数据结构填充,会相当占据性能

Clustered-based Deferred Rendering

rendering 流程

  1. Geometry Pass

  2. Light Culling:

    • 计算出所有参与运算的 cluster 集合

    • 对集合中每个 cluster 进行光源分配

  3. Lighting Pass :绘制整个屏幕的2D矩形,片元着色时根据该片元所在 cluster 的光源列表来累加计算着色

  4. (Light Pre-Pass 可选)Geometry Pass

Clustered-based Forward Rendering

rendering 流程

  1. Z-prepass

  2. Light Culling:

    1. 计算出所有参与运算的 cluster 集合
    2. 对集合中每个 cluster 进行光源分配
  3. Forward Rendering Pass:绘制所有几何体,片元着色时根据该片元所在 cluster 的光源列表来累加计算着色

参考

  • [1] 《Real-time Rendering 4th》
  • [2] 《全局光照技术》
  • [3] forward框架的逆袭:解析forward渲染 | KlayGE
  • [4] 《GPU Pro7》