跳过正文

TBDR+Forward vs. IMR+Deferred_ 两种设备的不同权衡

·6830 字·14 分钟
杂谈 Shader Animation 渲染 材质 光照 算法 色彩
AxonSin
作者
AxonSin
梦想是复活在赛博世界,成为一名赛博垃圾人。

写在前面
#

这两套渲染方案,每一个渲染方案对应的是硬件层面(IMR、TBDR)和软件层面(Foward、Deferred),对应的是目前主流的两套方案:移动端渲染方案和电脑端渲染方案

为什么不同类型设备会采用不同方案?
#

这要从移动端的性能敏感说起。直接上图:

相机也有一句话说的是“底大一级压死人”,这句话同样也可以应用在芯片设计中。(~~光是看着面积就知道很吓人了)~~同时,桌面端直接利用电源供电,仅算上GPU就可以获得高达375W的功率,不仅能在算力上“力大飞砖”,同时同样的功能,桌面端的指标明显要优异于移动端。更别说芯片面积受限,On-Chip Memory(L1/L2缓存)容量要比桌面端更小(的多)。好的,现在我们知道了手机的算力天生就要比电脑小得多,接下来就先从硬件结构上说明这两个硬件渲染方法:TBDR/IMR。

TBDR/IMR
#

隶属于硬件层面的渲染架构,不可以进行两者间的切换。

TBDR: Tile-Based Deferred Rendering
#

TBDR大致可以分为三个模块:Binning Pass, Rendering Pass, Resolve Pass.

  1. 最上一层:Render Pipeline (渲染管线)
  2. 中间一层:On-Chip Buffer(a.k.a. 片上内存,Tiled Frame Buffer & Tiled Depth Buffer)
  3. 最下一层:系统内存,CPU和GPU共享

Binning Pass:
#

目标:将几何数据(顶点、三角形)分配到对应的屏幕小块(Tile)中,为后续渲染做准备。

流程步骤

  1. 顶点处理
    • 顶点着色器(Vertex Shader)会接收来自软件层面的顶点数据,处理顶点数据,计算顶点的屏幕坐标(投影到屏幕空间)。
    • 简化处理:部分GPU(如Adreno)可能使用简化版顶点着色器,仅计算顶点位置,忽略纹理坐标、法线等细节(这些在分块阶段不需要)。
  2. 分块分配
    • 将屏幕划分为固定大小的 Tile(如16x16像素的小方块)。
    • 遍历所有三角形,确定每个三角形覆盖了哪些 Tile。
    • 将覆盖的 Tile 记录到 Primitive List(图元列表)中,记录每个 Tile 包含哪些三角形。
  3. 数据存储
    • 将每个 Tile 的 Primitive List 和顶点数据写入 系统内存(aka. LPDDR)
    • 关键点:分块阶段仅记录几何信息,不进行光栅化或像素着色。

参与组件

  • Tiler 单元:负责将三角形分配到对应的 Tile。
  • 顶点着色器:处理顶点坐标。
  • 系统内存:存储分块后的几何数据(Primitive List)。

Rendering Pass:
#

目标:逐 Tile 处理几何数据,完成光栅化和像素着色,结果暂存到片上内存(On-Chip Memory)。

流程步骤

  1. Tile 遍历
    • 按顺序处理每个 Tile(如从左到右、从上到下)。
  2. 几何处理
    • 重新处理顶点:读取该 Tile 的 Primitive List,重新执行顶点着色器(这次会完整计算顶点属性,如纹理坐标、法线等)。
    • 裁剪与剔除:进行背面剔除、视锥体剔除等,减少无效三角形。
  3. 光栅化(****Rasterization)
    • 将三角形转换为该 Tile 内的像素(片元)。
    • 深度测试(Z-Test):利用 HSR(Hidden Surface Removal,隐藏面消除)LRZ(Low Resolution Z) 技术,快速剔除被遮挡的片元,减少不必要的像素着色计算。(需要注意的是,虽然我们认为IMR可以“不切实际的进行高强度运算”,但是它同样可以采用Early-Z/Pre-Z来进行剔除并减少overdraw
  4. 像素着色
    • 开始执行像素着色器(Fragment Shader),计算每个片元的颜色、光照等。
    • 结果暂存:将处理后的像素数据写入 片上内存(Tile Buffer)。

参与组件

  • 光栅化引擎:将三角形转换为像素。
  • 像素着色器:处理像素颜色和光照。
  • HSR/LRZ 单元:隐藏面消除,减少过绘制(Overdraw)。
  • 片上内存:临时存储 Tile 的渲染结果(颜色、深度缓冲等)。

Resolve Pass:
#

目标:将所有 Tile 的渲染结果合并到最终的帧缓冲区(Frame Buffer)。

流程步骤

  1. 合并 Tile 数据
    • 将每个 Tile 的 Tile Buffer 内容(颜色、深度等)从片上内存写入系统内存的帧缓冲区Framebuffer。
    • 多采样抗锯齿(MSAA):如果启用了 MSAA,此时会直接将多个样本点合并为最终像素颜色。这里由于Tile的像素数据直接按块存在于片上内存,极大提高了MSAA的缓存命中率(直接采样点即可而不需要从片外内存读取Framebuffer)。这也是为什么MSAA在低性能移动端的性能损耗会小于TAA。但是,MSAA的每个像素存储多个样本点(如4x MSAA占用4倍显存),在移动端设备中,显存容量有限(如低端设备仅1-2GB),可能导致显存不足或CPU和GPU间的左右脑互搏,带宽竞争。
  2. 清理与提交
    • 清除片上内存中的临时数据(如深度缓冲)。
    • 将完成的帧提交给显示屏。

参与组件

  • ROP(光栅操作单元):合并 Tile 数据并执行最终的写入操作。
  • 系统内存DDR:存储最终的帧缓冲区。

IMR: Immediate Mode Rendering
#

IMR的渲染流程分为以下4个核心阶段:Vertex Process, Rasterizer, Pixel Shading, Merge.

Vertex Process
#

目标:将顶点数据转换为屏幕空间坐标,并执行顶点着色器计算。

参与组件

  • 顶点缓冲区(Vertex Buffer):存储顶点的原始数据(坐标、法线、纹理坐标等)。
  • 顶点着色器(Vertex Shader, VS):GPU的可编程单元,负责计算顶点的最终位置、颜色、纹理坐标等。
  • 固定功能单元(Fixed-Function Units)
    • 顶点属性组装(Vertex Assembly):将顶点数据从内存加载到GPU内部缓存。
    • 投影变换(Projection Transform):将顶点坐标从模型空间转换为屏幕空间。
  • 缓存(L1/L2 Cache):减少顶点数据从主存重复读取的延迟。

流程

  1. 顶点数据从系统内存加载到GPU的顶点缓冲区。
  2. 顶点着色器对每个顶点执行计算(如光照、动画变形)。
  3. 处理后的顶点数据通过固定功能单元完成投影变换,得到屏幕坐标。

Rasterization
#

目标:将三角形转换为像素(片元),并进行深度测试(Early-Z)。

参与组件

  • 光栅化单元(Rasterizer):负责将三角形分解为像素级别的片元。
  • 深度缓冲区(Depth Buffer):存储每个像素的深度值,用于隐藏面消除(Z-Test)。
  • 裁剪单元(Clipping Unit):剔除超出视口的三角形。

流程

  1. 光栅化单元将三角形的顶点坐标扩展为覆盖的像素区域。
  2. 每个像素生成一个片元(Fragment),并计算其深度值。
  3. 片元通过Early-Z测试:若深度值小于Depth Buffer中的值,则保留;否则丢弃。
  4. 未被丢弃的片元传递给像素着色器处理。

Pixel Shadeing
#

目标:计算每个片元的最终颜色值。

参与组件

  • 像素着色器(Pixel Shader, PS aka. Fragment Shader, FS):对通过Early-Z的片元执行光照、纹理采样等计算。
  • 纹理单元(Texture Units):从纹理内存加载纹理数据(如法线贴图、颜色贴图)。
  • 固定功能插值单元:对顶点属性进行插值(如颜色、纹理坐标)。

流程

  1. 片元通过插值单元获取顶点属性的插值结果(如像素颜色、纹理坐标)。
  2. 像素着色器根据插值后的数据、纹理采样结果及光照模型计算最终颜色。
  3. 若启用了Alpha测试(Alpha Test),部分片元可能被丢弃。

Merger
#

目标:将片元颜色写入Frame Buffer,并执行混合(Blending)和深度更新。

参与组件

  • 混合单元(Blending Unit):根据混合方程(如透明度)合并新颜色与已有颜色。
  • 深度Stencil单元:执行Late-Z测试和Stencil Buffer操作。
  • Frame Buffer(颜色缓冲区):存储最终像素颜色。
  • Depth Buffer:存储深度值,用于后续帧的深度测试。

流程

  1. 通过像素着色的片元进入Late-Z测试(若未通过Early-Z)。
  2. 混合单元将新颜色与Frame Buffer中的颜色混合(如透明物体叠加)。
  3. 更新Frame Buffer和Depth Buffer,完成像素输出。

电脑端参与的组件:
#

硬件组件:
#
  1. GPU核心模块
    • 计算单元(Shader Cores):执行顶点着色器、像素着色器。
    • 光栅化引擎:处理光栅化和深度测试。
    • 内存控制器:管理与系统内存的交互。
  2. 缓存结构(片上内存)
    • L1/L2 Cache:缓存顶点数据和中间结果。
    • Texture Cache:加速纹理采样。
  3. 显存(VRAM)
    • 存储Frame Buffer、Depth Buffer、纹理等数据。
软件组件:
#
  1. 图形API(如DirectX、OpenGL):
    • 管理顶点缓冲区、渲染状态(如混合模式、深度测试)。
  2. 驱动程序
    • 将API命令转换为GPU可执行的指令流。
  3. 着色器代码
    • 顶点着色器、像素着色器的可编程逻辑。

Forward搭配TBDR,而Deferred则会搭配IMR?
#

这就要说到缺点了。虽然Forward在目标实现接近于IMR(两者的目的都是需要及时渲染),按理来说应该是Forward+IMR。我们来简要过一下两个渲染管线:

Forward管线流程
#

  1. 顶点处理:顶点着色器计算顶点坐标、法线等。
  2. 光栅化:生成片元并进行Early-Z测试。
  3. 片段处理
    • ForwardBase Pass:计算主光源(如Directional Light)。
    • ForwardAdd Pass:为每个附加光源(点光源、聚光灯)单独渲染,叠加光照。
    • 需要注意的是,在URP14中,ForwardAdd Pass被取消,为了简洁化,可以直接调用GetAdditionalLightsCount()。但是底层实现逻辑仍然一致。
  • LightAdd in URP14:
#ifdef _ADDITIONAL_LIGHTS
                int additionalLightsCount = GetAdditionalLightsCount();
                for (int i = 0; i < additionalLightsCount; ++i)
                {
                    Light light = GetAdditionalLight(i, input.positionWS);
                    
                    // 简单的Lambert漫反射
                    half3 additionalLambert = CalculateLambert(N, light.direction, light.color, light.distanceAttenuation * light.shadowAttenuation);
                    
                    additionalLightsColor += baseColor.rgb * additionalLambert * _AdditionalLightsIntensity;
                }
#endif
  1. 混合与输出:将结果写入Frame Buffer。

Deferred管线流程
#

  1. 顶点处理:仅计算顶点坐标,不处理光照。
  2. 光栅化:开始采集并生成G-Buffer(存储几何数据:位置、法线、颜色、材质属性等)。
  3. 光照Pass:遍历屏幕像素,根据G-Buffer数据计算所有光源对像素的影响。在这一阶段, 会在屏幕上逐像素执行光照,不再访问场景几何
  4. 混合与输出:将结果写入Frame Buffer。

发现了没有?Deferred管线这里多了一个Gbuffer。这个Gbuffer是什么呢,这是一个会储存在**片外内存(对应桌面端的VRAM,移动端的LPDDR)**的Buffer。

延迟渲染需要将几何属性(如位置、法线、材质参数、颜色等)存储到 Gbuffer的多张渲染目标(Render Targets, RT) 中,例如:

  • Position RT:存储像素的三维位置。
  • Normal RT:存储法线向量。
  • Albedo RT:存储基础颜色。
  • Depth RT:存储深度信息。

虽然deferred在多光源计算的性能消耗明显优于forward管线,那么代价是什么呢?代价是deferred虽然可以一次性解决屏幕的所有物体渲染,不需要逐物体渲染;但是这个条件的前提是你的Gbuffer可以迅速地从系统内存读取到计算单元中并作计算。更别说你还没有开启MSAA(4xMSAA=从Gbuffer采样四次,带宽要求直接x4),TAA(从Framebuffer的时域采样,直接*n帧带宽)。

另外,由于Gbuffer不会存储透明物体数据,因为Deferred渲染的核心是几何处理阶段(Geometry Pass),将所有不透明物体的几何信息(如位置、法线、材质属性、深度)写入G-Buffer(多个渲染目标)。

也就是说,如果还要加一个透明处理,就意味着:

  1. 可以用加一个Forward管线单独处理,但是这会脱离Gbuffer,导致Deferred的屏幕空间效果不起作用
  2. 单独开一个Gbuffer接着渲染。再次从片外内存读写一遍。

对比复杂度
#

最后对比一下算法(由通义生成):在分析延迟渲染(Gbuffer + IMR)与前向渲染(+ TBDR)的带宽压力差异时,需要从渲染流程、数据存储方式、架构特性 三个维度进行对比。


1. 延迟渲染(Gbuffer + IMR)的带宽压力来源
#

(1) Gbuffer的存储与传输
#
  • Gbuffer的定义
    • 延迟渲染需要将几何属性(如位置、法线、材质参数、颜色等)存储到 多张渲染目标(Render Targets, RT) 中,例如:
      • Position RT:存储像素的三维位置。
      • Normal RT:存储法线向量。
      • Albedo RT:存储基础颜色。
      • Depth RT:存储深度信息。
  • 带宽消耗点
    • 初始化阶段:渲染所有几何体时,必须将所有可见像素的几何属性写入Gbuffer,这需要 多次系统内存写入
    • 光照阶段:每个光源的计算需要读取Gbuffer中的所有RT数据(如法线、位置、材质),导致 多次系统内存读取
    • 屏幕分辨率越高,Gbuffer的带宽需求呈线性增长(如1080p屏幕需要约200万像素 × 多个RT通道)。
(2) IMR架构的劣势
#
  • 直接写入系统内存
    • 在IMR架构下,Gbuffer的写入和读取均直接通过 系统内存(VRAM) 进行,无法利用片上内存暂存中间结果。
    • 频繁的读写操作:例如,每个光源的计算需读取所有RT数据,带宽压力随光源数量线性增长。
  • 抗锯齿(MSAA)的额外开销
    • 若启用MSAA,Gbuffer的每个像素需存储多个样本点的数据(如4x MSAA需4倍存储空间),进一步增加带宽需求。
  • 对于读写速度的刚需:
    • Gbuffer的显存带宽需求在移动端是致命的(如1080p屏幕下,四张Gbuffer的带宽消耗可达 2GB/s,而移动端总带宽仅30GB/s)。
    • 延迟渲染需要 多张RT,导致显存带宽成为瓶颈。

2. 前向渲染(+ TBDR)的带宽优化
#

(1) TBDR架构的优势
#
  • 分块处理与片上内存暂存
    • 分块(Tiling):将屏幕分割为小Tile,每个Tile的渲染数据(颜色、深度、模板)暂存在 片上内存(On-Chip Memory)
    • 中间结果暂存:仅在 解析阶段(Resolve) 将Tile结果合并到系统内存的Framebuffer,减少频繁写入。
  • 隐藏面消除(HSR/LRZ)
    • 提前剔除被遮挡的像素,仅对 可见像素 进行纹理采样和光照计算,减少无效带宽消耗。
  • 纹理读取的局部性优化
    • 同一Tile内的像素共享纹理数据,片上缓存(如L2缓存)可高效复用,减少系统内存访问。
(2) 前向渲染的流程优化
#
  • 逐Tile处理
    • 每个Tile的渲染在片上内存完成,仅需读取 当前Tile相关的纹理和几何数据
    • 无需存储Gbuffer:颜色和深度数据直接暂存于Tile Buffer,最终合并到Framebuffer。
  • 过绘制(Overdraw)的减少
    • HSR/LRZ技术剔除被遮挡像素,减少纹理和着色计算的带宽需求。
(3) 知识库依据
#
  • 知识库[2][5][7] 强调:
    • TBDR通过 片上内存 存储中间数据,仅最终合并时写入系统内存,显著降低带宽。
    • HSR/LRZ技术可将Overdraw降至接近1,减少无效像素的纹理读取。

3. 带宽压力对比:延迟渲染 vs 前向渲染
#

维度 延迟渲染(Gbuffer + IMR) 前向渲染(+ TBDR)
Gbuffer存储需求 需存储多张RT(如Position、Normal、Albedo等),带宽随RT数量线性增长。 无需Gbuffer,仅存储颜色和深度数据,带宽需求更低。
系统内存访问频率 每个Pass需读取所有RT数据(如每个光源需读取Gbuffer),导致 高频系统内存访问 仅最终合并时写入系统内存,中间数据在片上内存处理, 系统内存访问频率低
纹理读取优化 纹理数据需多次读取(如每个光源读取Gbuffer),无片上缓存优化。 纹理数据通过片上缓存复用,且仅处理可见像素, 带宽消耗显著降低
抗锯齿开销 MSAA需存储多倍样本数据,Gbuffer带宽压力进一步增加。 TBDR的 片上MSAA 可暂存多样本数据,减少系统内存带宽需求。
移动设备适用性 移动端显存带宽有限,Gbuffer的高带宽需求导致性能瓶颈(如知识库[4]提到的2GB/s vs 30GB/s总带宽)。 TBDR的优化使其在移动端成为主流,前向渲染与TBDR协同降低带宽压力。

4. 具体场景示例
#

场景:1080p屏幕,4光源
#
  • 延迟渲染(Gbuffer + IMR)
    • Gbuffer带宽:假设每像素存储4个通道(如Position、Normal、Albedo、Depth),8bit/通道:
      • 单帧带宽 = 1920×1080 × 4通道 × 8bit × 60fps ≈ 2.5GB/s
    • 光照阶段带宽:每个光源需读取Gbuffer数据,4光源总带宽 ≈ 10GB/s
  • 前向渲染(+ TBDR)
    • 纹理读取:仅对可见像素进行光照计算,假设Overdraw减少为1,带宽 ≈ 1920×1080 × 4光源 × 0.2(缓存命中) ≈ 0.8GB/s
    • 系统内存写入:仅最终合并时写入Framebuffer,带宽 ≈ 0.5GB/s

5. 总结:带宽压力差异的核心原因
#

  • 延迟渲染(Gbuffer + IMR)
    • 带宽杀手:多张RT的存储与高频读写、无片上内存优化导致带宽需求激增。
    • 移动端不友好:显存带宽有限,Gbuffer的高开销易触发带宽瓶颈。
  • 前向渲染(+ TBDR)
    • 带宽优化:利用片上内存暂存数据、HSR剔除无效像素、纹理缓存复用,显著降低系统内存访问。
    • 移动端首选:与TBDR架构协同,平衡性能与功耗,适合移动设备的典型场景(少光源、透明物体)。

类比理解
#

  • 延迟渲染(Gbuffer + IMR):像快递公司每次配送都要先建一个庞大的中央仓库(Gbuffer),再多次往返取货送货,导致交通拥堵(带宽不足)。
  • 前向渲染(+ TBDR):快递员分片区配送(分块),每个片区的货物暂存在本地仓库(片上内存),仅最终合并到中央仓库,减少重复运输。
Reply by Email

相关文章

基本渲染管线
·2770 字·6 分钟
杂谈 Unity Shader Animation 渲染 光照 色彩
渲染技术解析
无厚度_低厚度的zfighting问题
·3192 字·7 分钟
杂谈 Unity Blender Shader 渲染 材质 光照 物理 色彩 故障排除 教程 技巧
Blender导入Unity指南
自定义效果的大杀器——RenderFeature
·2758 字·6 分钟
杂谈 Unity Shader 渲染 材质 光照 色彩 故障排除 配置
问题解决方案