延迟渲染
Forward Rendering
先简单说下最早的前向渲染 —— 古早,但是符合直觉,因为它的核心单位是物体
用一份伪代码说明它的思路 ——
ForEach 每个可见物体 (Object):
ForEach 场景中的每个光源 (Light):
计算该光源对该物体像素的光照贡献
将最终颜色写入帧缓冲 (FrameBuffer)也就是每次取出一个物体,然后计算场景内的所有光源对他的影响,最后计算出颜色后画到屏幕
非常符合直觉,也有许多优势 ——
- 材质表现自由度高:可以针对每个物体单独编写着色器
- 完美支持硬件抗锯齿(MSAA):可以在几何体光栅化阶段利用硬件进行多重采样,得到极其锐利平滑的边缘。
- 内存带宽占用小
- 在光源较少时的极高性能
但是显然来看,它存在着
- 无效计算 —— 假设花费了大力气渲染了一些 Objects,但是它们有可能再之后被其他物体遮住,导致计算无意义
- 时间复杂度高 —— 渲染开销是
。光源越多,性能越差
为了解决 Forward Rendering 的痛点,于是提出延迟渲染
延迟渲染
一种空间换时间的方法,简单来说,把渲染过程分成两个pass
- 先将所有物体信息压入到
G-Buffers中 - 接着在计算光源着色的时候只需要逐像素进行着色即可
时间复杂度降到了
G-Buffer
Geometric Buffer 几何缓冲区。
首先遍历场景中所有可见的不透明物体,不进行着色,而是计算其几何信息 —— 计算所有之后进行光照着色所需要的信息并且存储到 G-buffer 中,比如
- Base Color
- Normal
- Depth
- Material
- 等
G-Buffer 的大小和分辨率应当一致,因为后期需要逐像素遍历进行着色。
当然,如果得把场景画好几遍才能收集齐所有的表面属性,这显然太贵了 —— 使用 MRT (多重渲染目标, Multiple Render Targets)技术做优化
光照阶段
抛弃原先遍历场景中的 3D 物体的方式,直接通过 G-Buffer 记录的信息进行光照计算
首先世界坐标重建:通过像素的屏幕 UV 和 G-Buffer 中的 Depth,乘以“视图投影逆矩阵”,利用透视除法反推出世界坐标
完整组装渲染管线
- 视锥体剔除
- 深度预处理
- 几何阶段
- 阴影
- 光照
- 正向渲染补偿(把半透明物体等叠到渲染结果上
- 后处理
缺点
1. 显存带宽爆炸!
G-Buffer 的存储是很贵的,而且在需要存储多信息的情况下,更是贵的不行
在几何计算的时候哗啦哗啦写数据,在光照阶段又必须要全部读出来
当然,通过分块延迟渲染(TBDR)或者其他什么机制,也可以缓解这个问题
2. MSAA 失效
在正向渲染里,硬件 MSAA 可以非常丝滑地处理三角形边缘的锯齿。但延迟渲染把光照推迟到了 2D 的 G-Buffer 上算。在 2D 像素层面,硬件不知道三角形的几何边缘在哪里,MSAA 就抓瞎了。 如果强行对 G-Buffer 做 MSAA,意味着 G-Buffer 的大小要翻 4 倍(4x MSAA),显存直接原地爆炸。这就是为什么现在 3A 大作全都在卷 TAA、DLSS、FSR(时间轴或 AI 抗锯齿),因为传统的 MSAA 在延迟管线下用不了了。
3. 材质
首先无法渲染半透明的物体 —— 由于 G-Buffer 无法在一个像素点记录前后重叠的多个材质状态,所以需要依赖一条额外的正向渲染 Pass 补救
其次,在延迟渲染中,所有材质的最终属性都必须塞进同一个规格的 G-Buffer 里,所以导致假设要多加一个参数,就需要一张完整的 Buffer,白白浪费内存显存空间