关于延迟渲染的一些瞎bb
笔者受到这里的启发, 试图对不同种类的绘制剔除技术进行区分和总结, 故作本文.
前向(Forward)渲染
. 即, 几何体的顶点经过VS/GS/FS着色器, 变成片元着色器输出的颜色值和深度. 为了正确地表示片元间的遮挡关系, 片元的深度与深度缓冲中的深度比较, 若比较的结果符合预设的大小关系, 则新产生的片元将覆盖帧缓重中的颜色值和深度值; 最终还留在帧缓冲中的颜色值就是未被遮挡从而应显示在屏幕上的像素的值. 这种渲染方式是我们讨论的基础, 而各种剔除技术都是对其进行改进的尝试.
然而, 随着片元计算成本的增加和场景复杂度的提升, 过度绘制(overdraw)的影响也变得越来越重要. 一方面, 为了产生更逼真的色彩, 准确地计算片元上的照度变得越来越重要, 这使得片元着色器中往往包含大量采样和计算, 每次执行都变得非常昂贵; 另一方面, 越来越复杂的场景意味着越来越多的遮挡, 场景中更大比例的片元将没有显示到屏幕上的机会. 这一现状提示我们, 如果能有效地避免绘制任何最终不会被绘制到屏幕上的片元, 我们将得到巨大的优化可能性. 我们可以感性地对这一问题加以认识: 设场景中物体的数量级为 \(N\) , 屏幕上像素数量为 \(M\) , 而假设计算片元照度和颜色产生了主要的开销, 则前向渲染将具有 \(O(N)\) 的时间复杂度; 如果能实现 zero overdraw, 即所有计算都能最终显示在屏幕上, 屏幕上的像素不会被片元着色器计算多次(这就是 overdraw 的意思), 则时间复杂度将为 \(O(M)\). 随着时代的进步, \(N\) 增长了几个数量级(在开放世界游戏中更是吓人), 而由于视网膜视细胞密度的限制, \(M\) 的增长远远赶不上 \(N\) 的增长(甚至赶不上显卡计算单元数目的增长).
Early-Z 和 Z-culling
事实上, 大部分情况下, gpu 可以在早于片元着色器执行的时机知道一个片元是否已经被遮挡了. 因此gpu制造商在光栅化后片元着色器执行之前加入了 early-z 阶段, 如果片元着色器不手动写入深度值, 则在 early-z 阶段 gpu 就可以知道片元的深度(通过重心坐标插值顶点着色器的输出得到), 从而根据已经部分绘制的深度缓冲将片元进行相对保守的剔除. 考虑一个理想情况, 即所有图元是根据离摄像机从近到远的顺序绘制的, 这种情况在启用 early-z 阶段时可以完全消除所有 overdraw; 但在最不理想的情况下, 即图元按由远到近的顺序绘制, early-z 完全不能提供任何改善. 为了利用这种特性, 往往需要先在 cpu 上对图元进行排序, 然后再提交给 gpu 绘制, 这无疑增加了 cpu 的占用.
Z-culling 是另一种想法相似的技术, 不过常常出现在 Tile Based Rendering 架构的 gpu 中. gpu 首先取得当前 tile(16*16 pixels) 对应的深度缓冲区中最大的深度值 z_max 和最小的深度值 z_min; 如果当前 tile 的深度最大值大于 z_max, 就说明当前 tile 应该被整个丢弃. 由于此技术不直接读取深度缓冲, 就节约了部分读写显存的带宽.
延迟渲染(Deferred Rendering)
上一节的基于硬件的片元剔除方式虽然有所改善, 但远不足以达到 zero overdraw 的目标. 一个自然而然的想法是更充分地利用深度测试, 只对最终通过深度测试的片元执行有意义的计算; 相比之下, 通过 early-z 测试的片元仍然可能最终被遮挡. 考虑到计算片元上的光照往往需要法线/切线/世界坐标/表面颜色/粗糙度等参数, 我们可以把这些信息编码进前向渲染中片元着色器输出的颜色值中, 然后像进行任何其它后处理pass那样, 从得到的帧缓冲(称为 g-buffer)的颜色值中还原出这些参数, 进行实际的片元计算, 例如依次对所有光源, 计算光源对 g-buffer 中片元颜色的贡献:
1 | For each object: |
这一算法看起来非常美好, 但实际上却占用了大量的显存带宽. 如此多的信息必须编码在高位宽度的 g-buffer 中, 必然导致片上缓存无法完全容纳 g-buffer, 而必须大量读取显存. 不妨做一个计算, 4k 分辨率下, 128 bit per pixel 的 g-buffer, 有 8 个光源, 在 60fps 的帧率下将消耗 140GB/s 的显存带宽(读写各8次). 此外, 编码了复杂信息的颜色值也不能进行任何混合操作, 这限制了 MSAA 等反走样方法的使用.
延迟光照(deferred lighting)则是对延迟渲染的一个改进. 延迟光照渲染场景中不透明的几何体, 将法线 n 和镜面扩展因子(specular spread factor)m 写入缓冲区. 这个缓冲区可以看作一个更轻量的 g-buffer, 因此节约了一部分显存带宽. 随后计算漫反射项和高光项(即照度和高光的强度), 并将结果分别写入漫反射和高光缓冲区(将这两项写入不同的缓冲区是考虑到纹理对漫反射项的调制和高光项的调制颜色往往是不同的). 最终, 对场景中的不透明几何体再执行一次前向渲染. 由于此次渲染时所有光源的贡献都已经储存在缓冲区中了, 只需从纹理中读取漫反射颜色和高光颜色, 对漫反射和高光缓冲区的值进行调制,就能得到最终的颜色.