深入解析延迟渲染(Deferred Shading)
延迟渲染(Deferred Shading)是现代图形渲染引擎(如 Unreal Engine、Unity HDRP 及自定义 Vulkan/DirectX12 引擎)中处理多光源场景的核心渲染范式。本文将从基本原理、G-Buffer 设计、Vulkan Subpass 工程实践、混合渲染工作流到进阶光照优化进行全面解构与扩展。
一、 渲染范式变革:从正向到延迟
1. 正向渲染(Forward Rendering)的痛点
在传统的正向渲染管线中,光照计算直接在几何体的片元着色器(Fragment Shader)阶段执行。渲染复杂度通常表示为: \[\text{Complexity} = O(N_{\text{objects}} \times M_{\text{lights}} \times \text{Overdraw})\]
- 光照计算冗余 (Overdraw Overhead):即便启用了 Early-Z(早期深度测试),在存在复杂遮挡、Alpha 测试或无序绘制的场景中,许多片元在执行完极耗性能的光照算法后,仍可能在后续测试中被丢弃,造成大量的算力浪费。
- 多光源性能骤降:每增加一个光源,每个受影响的几何体都需要重新绘制一次(或在单个 Shader 内用循环遍历所有光源),导致着色器指令膨胀、变体激增,性能随光源数量呈线性甚至指数级下降。
2. 延迟渲染的核心解耦思想
延迟渲染的核心在于“将几何处理与光照计算彻底解耦”。整个渲染过程分为两个主阶段:
- 几何阶段(Geometry Pass):渲染场景几何体,但不计算任何光照,仅将光照计算所需的表面属性(位置、法线、材质参数等)写入一组称为 G-Buffer(G-缓冲区) 的帧缓冲附件中。
- 光照阶段(Lighting Pass):以全屏矩形(Screen Quad)或光源体积为驱动,逐像素读取 G-Buffer 中的信息,统一执行光照计算并输出最终颜色。
解耦后的计算复杂度降为: \[\text{Complexity} = O(N_{\text{objects}}) + O(\text{Screen Pixels} \times M_{\text{visible\_lights}})\]
关键优势:在此流程下,光照计算仅对最终在屏幕上可见的像素执行一次,彻底消除了 Lighting 阶段的 Overdraw 浪费。
二、 G-Buffer 架构设计与存储优化
G-Buffer(Geometry Buffer)是连接几何阶段与光照阶段的数据纽带。合理的 G-Buffer 布局不仅能节约显存空间,还能大幅降低带宽占用。
1. 经典 G-Buffer 布局
以计算 Blinn-Phong 或基础 PBR 光照为例,标准 G-Buffer 包含以下物理量:
| 附件名称 | 记录数据 | 推荐格式 | 占用内存/像素 |
|---|---|---|---|
| G-Position | 观察空间/世界空间位置 \((X, Y, Z)\) | VK_FORMAT_R16G16B16A16_SFLOAT |
8 Bytes |
| G-Normal | 表面法线向量 \((N_x, N_y, N_z)\) | VK_FORMAT_R16G16B16A16_SFLOAT |
8 Bytes |
| G-AlbedoSpec | 漫反射颜色 \((R, G, B)\) + 镜面强度/粗糙度 | VK_FORMAT_R8G8B8A8_UNORM |
4 Bytes |
| G-Depth | 硬件深度缓冲区 (Depth Buffer) | VK_FORMAT_D32_SFLOAT |
4 Bytes |
2. 工业级 G-Buffer 带宽优化扩展
在实际工程中,8 字节/像素的 G-Position
会带来巨大的显存带宽开销。现代引擎通常采用以下优化手段:
- 深度重建位置 (Position Reconstruction from Depth):
- 原理:取消独立的
G-Position附件。在光照阶段,利用G-Depth中的深度值 \(d\),结合像素的屏幕坐标 \((u, v)\) 和相机的逆投影矩阵(Inverse Projection Matrix),实时反算出视空间或世界空间坐标。 - 收益:直接将 G-Buffer 的写出带宽降低约 30%~40%。
- 原理:取消独立的
- 法线编码压缩 (Normal Encoding):
- 利用八面体编码(Octahedral Normal Encoding)将单位法线向量 \((N_x, N_y, N_z)\) 压缩存储至 2 个 8-bit 或 16-bit 通道,进一步节省显存。
三、 延迟渲染的权衡利弊
| 维度 | 延迟渲染(Deferred Shading) | 正向渲染(Forward Rendering) |
|---|---|---|
| 多光源扩展性 | 极其优秀,可轻松支持上千动态光源 | 较差,受限于 Shader 遍历或多 Pass 绘制 |
| Overdraw 消耗 | 光照计算无 Overdraw | 光照计算存在严重 Overdraw |
| 半透明渲染 | 原生不支持(只能存储单层片元) | 原生支持(从后往前排序混合) |
| 显存与带宽 | 极高(G-Buffer 频繁读写) | 极低(直接写入 Render Target) |
| 抗锯齿 (AA) | 无法原生使用硬件 MSAA | 原生完美支持硬件 MSAA |
| 材质多样性 | 受限(所有物体倾向共享同一种 Shader) | 灵活(每个物体可独立编写复杂 Shader) |
局限性的主流解决方案
- 半透明物体:使用混合渲染流,即先用延迟渲染处理不透明物体,再用正向渲染 Pass 叠加透明物体。
- 硬件 MSAA 缺失:使用后处理抗锯齿技术(如 TAA、SMAA 或 FXAA)。
- 材质单一:在 G-Buffer 中增加 Material ID 通道,光照阶段根据 ID 进行着色分支处理(Branching)。
四、 工程实现:基于 Vulkan Subpass 的现代管线实践
在移动端 Tile-Based GPU(TBR/TBDR)及现代 PC 显卡架构下,传统的 Multi-Pass 延迟渲染(将 G-Buffer 写入 VRAM,再从 VRAM 读取)会导致极高的显存带宽消耗与发热。
Vulkan 引入了 Subpass 和 Input Attachment 机制,允许 G-Buffer 数据保存在 Tile Memory(片上 SRAM 高速缓存) 中,甚至不需要写回主显存,从而实现极其高效的延迟光照。
1 | +-------------------------------------------------------------------+ |
1. Subpass 0:几何阶段 (Geometry Pass)
几何 Pass 的使命是将场景物体的表面属性渲染并填入 G-Buffer 附件中。
GLSL 几何阶段着色器示例 (Geometry Shader)
1 |
|
2. Subpass 1:延迟光照阶段 (Lighting Pass)
在第二个 Subpass 中,G-Buffer 被配置为
Input Attachments。着色器通过 subpassInput
内置类型和 subpassLoad()
函数直接读取当前像素在片上缓存中的数据。
依赖同步 (VkSubpassDependency) 配置
为了确保几何阶段写操作完成后再进行光照读取,必须配置 Subpass 依赖:
1 | VkSubpassDependency dependency = {}; |
GLSL 光照阶段着色器示例 (Lighting Shader)
1 |
|
五、 混合渲染工作流:处理半透明与调试几何体
延迟渲染无法直接渲染半透明物体(如玻璃、烟雾)或需要特殊着色器的小控件(如光源代理 3D 立方体)。通常的解决方案是将管线分为延迟阶段与正向阶段。
深度复用与状态配置 (Depth Buffer Reuse)
传统 OpenGL 做法可能需要调用 glBlitFramebuffer
拷贝深度缓冲,但在 Vulkan 中,可通过合理的 RenderPass
规划避免拷贝性能损失:
- 共享深度附件:在随后执行的正向渲染 Pass
中,直接绑定几何 Pass 所填充的
VkImageView深度附件。 - 深度加载指令:将该深度附件的加载操作设置为
VK_ATTACHMENT_LOAD_OP_LOAD,这会完整保留几何阶段写入的场景深度信息。 - 管线测试状态:在正向渲染管线中开启深度测试、关闭深度写入:
1
2
3
4VkPipelineDepthStencilStateCreateInfo depthStencil = {};
depthStencil.depthTestEnable = VK_TRUE; // 开启深度测试(保证正确遮挡关系)
depthStencil.depthWriteEnable = VK_FALSE; // 关闭深度写入(避免半透明物体遮挡后续绘制)
depthStencil.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
六、 进阶光照优化策略
全屏绘制光照阶段(Screen-space Quad Lighting)会导致场景中的每一个像素都遍历计算所有光源,导致 \(O(\text{Pixels} \times \text{Lights})\) 的复杂度。在成千上万个动态光源的场景下,性能依然会遭遇瓶颈。
1. 光体积截断 (Light Volumes & Attenuation Bounds)
由于点光源的照射强度随距离衰减,距离光源较远的片元受到的光照影响微乎其微。我们可以通过数学公式推导出一个极限受光半径 \(R_{\text{light}}\),并仅在该半径封装的几何体积(如简易球体或网格)覆盖的屏幕区域计算光照。
点光源衰减公式与半径推导
常用的点光源衰减方程如下:
\[ F_{\text{attenuation}} = \frac{1}{K_c + K_l d + K_q d^2} \]
其中: * \(d\) 为片元到光源的距离。 * \(K_c\) 为常数项(Constant),\(K_l\) 为一次线性项(Linear),\(K_q\) 为二次项(Quadratic)。
假设定义当光照贡献低于极限色彩阈值 \(I_{\text{threshold}}\)(例如在 8-bit 帧缓冲下取值 \(I_{\text{threshold}} = \frac{5}{256} \approx 0.0195\))时,光照视作不可见。令强度边界方程成立:
\[ I_{\text{max}} \cdot \frac{1}{K_c + K_l d + K_q d^2} = I_{\text{threshold}} \]
将其整理为关于距离 \(d\) 的标准一元二次方程:
\[ K_q d^2 + K_l d + \left(K_c - \frac{I_{\text{max}}}{I_{\text{threshold}}}\right) = 0 \]
根据一元二次方程求根公式,解得光源的最大影响半径 \(R_{\text{light}}\) 为:
\[ R_{\text{light}} = \frac{-K_l + \sqrt{K_l^2 - 4 K_q \left(K_c - \frac{I_{\text{max}}}{I_{\text{threshold}}}\right)}}{2 K_q} \]
实施方式
在光照 Pass 中,不再绘制全屏矩形,而是为每个点光源绘制一个半径为 \(R_{\text{light}}\) 的低模球体。球体覆盖的像素即为受光影响的像素,大幅缩减了无效片段的 Lighting Shader 执行次数。
2. 现代架构演进:Tiled & Clustered Deferred Shading
随着场景光源数量进一步增加(如上万个光源),频繁提交光照几何体(Light Volume Proxy)会导致 Draw Call 和几何顶点过高。现代 3D 引擎采用了更高效的空间分块策略:
1 | +------------------------+---------------------------------------+ |
- 分块延迟渲染 (Tiled Deferred Shading):
- 将屏幕切割为 \(16 \times 16\) 像素的小块(Tiles)。
- 使用 Compute Shader 在视锥体空间内求交,计算每个 Tile 覆盖的光源列表,将光源索引存入 Buffer。
- 光照 Pass 逐像素读取当前 Tile 的光源列表,避免全屏光源遍历。
- 集群延迟渲染 (Clustered Deferred / Forward+):
- 在 Tiled 的基础上的,将 View Frustum 在 Z 轴深度方向上进一步切片,形成 3D 空间栅格(Clusters/Voxels)。
- 彻底解决了 2D Tile 在深度跨度较大时(例如景深极深处与近处物体重叠在同一个 Tile)导致的光源误判与计算浪费。同时,这种集群裁剪思想还能无缝扩展至 Forward+ 管线,实现透明物体与不透明物体的统一光照管理。
七、 总结
延迟渲染通过“空间换时间”与“计算解耦”的思想,为现代 3D 引擎提供了强大的多光源渲染能力。在工程实践中: * 理解 G-Buffer 布局设计是降低渲染带宽的关键; * 结合 Vulkan Subpass 与 Tile Memory 是移动端与现代图形 API 发挥硬件极致性能的必然选择; * 配合 深度重建位置、光体积裁剪 及 Clustered 方案,使得千万级光照渲染在实时应用中成为可能。