第八章MRTMultiple Render Targets一句话概括MRT 让 GPU 一次 Draw Call 同时画到多张 RT 上省去多遍绘制。生活类比一支笔同时在四张复写纸上写字——写一次四份都有了。⏱ 30 秒概览MRTMultiple Render Targets让一次 Draw Call 的 Fragment Shader 同时输出到最多 8 张 Color RTSV_Target0~SV_Target7。最经典的应用是延迟渲染的 G-Buffer一次几何 Pass 同时写出 BaseColor、Normal、Roughness/Metallic、Emissive。MRT 省了计算但没省带宽——每张 RT 的写入带宽照收不误。SubpassVulkan/Metal可在 Tile-Based GPU 上让 MRT 数据留在片上 SRAM、不走显存。MRT MSAA 共存有硬性约束所有 Attachment 的 Sample Count 必须一致显存/带宽按 N×M 爆炸。延迟渲染 MSAA 有根本矛盾Lighting Pass 不走光栅化无法利用硬件 MSAA这是业界从 MSAA 转向 TAA 的核心原因。8.1 什么是 MRT在前面的章节中我们一直在讨论一次渲染画到一张 Color RT 上。但实际上现代 GPU 支持一次 Draw Call 同时输出到多张Color RT——这就是MRTMultiple Render Targets。想象一个场景你需要为每个像素同时记录颜色、法线、材质参数。不用 MRT 的方式是画三遍场景每遍输出一种信息。这意味着顶点处理做三次、光栅化做三次、片段着色器执行三次——巨大的浪费。用 MRT片段着色器只需执行一次同时输出多个值到不同的 RTstruct PSOutput { float4 color : SV_Target0; // → RT0 float4 normal : SV_Target1; // → RT1 float4 matProp : SV_Target2; // → RT2 float4 emissive: SV_Target3; // → RT3 }; PSOutput main(PSInput input) { PSOutput output; output.color baseColor; output.normal float4(worldNormal * 0.5 0.5, 1); output.matProp float4(metallic, roughness, ao, 1); output.emissive float4(emissiveColor, 1); return output; }GPU 在一次 Draw Call 中同时把四个输出写到四张不同的 RT。8.2 G-Buffer 典型布局MRT 最经典的应用就是延迟渲染中的G-BufferGeometry Buffer。延迟渲染在学术论文中最早可追溯到 1988 年但真正被 3A 游戏大规模采用是从S.T.A.L.K.E.R.: Shadow of Chernobyl2007和Killzone 22009, Guerrilla Games开始。后者首次在 GDC 上公开了完整的 G-Buffer 布局方案深刻影响了此后十多年的引擎设计。最基础的 G-Buffer 布局RT格式内容说明RT0RGBA8BaseColor.rgb Metallic基色 3 通道 金属度 1 通道RT1RGBA8Normal.xy Roughness MaterialID法线Octahedron编码 粗糙度 材质标记RT2R11G11B10FEmissive.rgb自发光需要 HDR 范围DepthD32F硬件深度深度测试 后续采样总带宽(4 4 4) bytes/pixel × 1920 × 1080 ≈ 24.9 MB/帧仅写入 ColorUE5 风格的 G-Buffer更丰富RT格式内容GBufferARGBA8WorldNormal.xyz PerObjectDataGBufferBRGBA8Metallic Specular Roughness ShadingModelIDGBufferCRGBA8BaseColor.rgb GBufferAOGBufferDRGBA8CustomData各 Shading Model 自定义GBufferERGBA8PrecomputedShadow可选SceneDepthD32F硬件深度VelocityRG16FMotion Vector可选Unreal 的 G-Buffer 可以用到 5~6 张 RT格式经过精心打包以最小化带宽。布局设计原则尽可能少的 RT——每多一张带宽就多一份数据打包到不同通道——比如把 Metallic 塞进 BaseColor 的 Alpha 通道选择最小够用的格式——法线和材质参数用 RGBA8 就够不需要 RGBA16FHDR 数据单独一张 RT——自发光需要浮点格式与其他 RGBA8 数据混一起不划算8.3 Pixel Shader 多输出SV_Target0 ~ SV_Target7HLSLstruct PSOutput { float4 target0 : SV_Target0; float4 target1 : SV_Target1; float4 target2 : SV_Target2; // ... 最多 SV_Target7 };GLSLlayout(location 0) out vec4 fragColor0; layout(location 1) out vec4 fragColor1; layout(location 2) out vec4 fragColor2; // ... 最多 8 个注意每个输出的数据类型可以不同。比如 target0 输出float4target1 输出uint4——只要对应的 RT 格式匹配。但大多数引擎为了简化统一使用float4输出。8.4 各 API 的 MRT 上限与格式限制API最大 Color RT 数量格式约束OpenGL 3.xGL_MAX_COLOR_ATTACHMENTS至少 8各 Attachment 可以不同格式OpenGL ES 3.0至少 4各 Attachment 可以不同格式DX118各 RT 格式可不同DX128各 RT 格式可不同Vulkan至少 4通常 8各 Attachment 格式可不同Metal8各 Attachment 格式可不同关键约束所有 Color RT 必须具有相同的尺寸宽×高和相同的采样数Sample Count。格式可以不同但尺寸和 MSAA 采样数必须一致。8.5 MRT 的带宽代价MRT 不是免费的。虽然 Draw Call 只执行一次但每张 RT 都需要带宽来写入。4 张 MRT vs 1 张 RT片段着色器执行次数相同都是 1 次顶点处理 / 光栅化相同RT 写入带宽4 倍如果有混合读取带宽也 4 倍所以 MRT 省了计算的代价但没省带宽的代价。在带宽受限的移动端MRT 需要格外谨慎。⚠️行业经验某移动端 3A 项目曾使用 6 张 RGBA16F MRTUE 风格 G-Buffer在 Mali G78 上测得 G-Buffer Pass 带宽 200 MB/帧——占全帧带宽的 40% 以上。最后不得不改回前向渲染 Forward 分光G-Buffer 只保留 2 张半分辨率的 SSAO/SSR 辅助 RT。移动端做延迟渲染 MRT 之前务必先算带宽预算。实际数字4 张 1080p RGBA8 MRT写入带宽 4 × 1920 × 1080 × 4 33.2 MB/帧 如果有 Alpha 混合读写 66.4 MB/帧4 张 1080p RGBA16F MRT写入带宽 4 × 1920 × 1080 × 8 66.4 MB/帧 如果有 Alpha 混合 132.7 MB/帧这只是 MRT 本身的代价。后续 Lighting Pass 还要读取这些 RT又是几十 MB。8.6 Subpass 如何缓解 MRT 带宽在 Tile-Based GPU 上Vulkan 的Subpass和 Metal 的Tile Shading可以大幅缓解 MRT 的带宽问题。核心思想如果多张 MRT 只在当前像素被读取不随机访问其他像素那它们不需要写回显存——留在 Tile Buffer 中下一个 Subpass 直接从 Tile Buffer 读取。典型的延迟渲染 Subpass 配置Subpass 0G-Buffer Pass Color Attachments: RT0, RT1, RT2, RT3 StoreOp: DontCare, DontCare, DontCare, DontCare ← 不写回显存 Subpass 1Lighting Pass Input Attachments: RT0, RT1, RT2, RT3 ← 从 Tile Buffer 直接读取 Color Attachment: FinalColor StoreOp: Store ← 只有最终颜色写回显存这样 4 张 G-Buffer 的 Store 和后续的 Load全部省掉省下的带宽约等于 G-Buffer 总大小的 2 倍。但限制很大Input Attachment 只能读取当前像素subpassLoad(inputAttachment)不能随机访问其他 UV两个 Subpass 必须在同一个 Render Pass 内桌面端 GPU 不一定能真正合并取决于 driver8.7 MRT MSAA 的共存——规则、代价与延迟渲染的抗锯齿困境MRT 和 MSAA 可以同时使用但有严格约束且代价巨大。硬性约束所有 Attachment 必须同 Sample Count你不能让 RT0 是 4xMSAA 而 RT1 是非 MSAA。所有 Color Attachment Depth Attachment 的 Sample Count 必须一致。违反这个规则的后果API后果OpenGLGL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLEDX11/12创建 PSO 时报错VulkanValidation 层报错Metal运行时报错带宽/显存的双重乘法爆炸MRT 数量 × MSAA 采样数 显存和带宽的乘数。例4 张 G-Buffer 4x MSAA每张 RGBA16F8 字节/像素1080p显存4 RT×4 samples×1920×1080×8265.4 MB\text{显存} 4 \text{ RT} \times 4 \text{ samples} \times 1920 \times 1080 \times 8 265.4 \text{ MB}显存4RT×4samples×1920×1080×8265.4MB加上 DepthD32F, 4 samples265.41×4×1920×1080×4265.433.2298.6 MB265.4 1 \times 4 \times 1920 \times 1080 \times 4 265.4 33.2 298.6 \text{ MB}265.41×4×1920×1080×4265.433.2298.6MB仅 G-Buffer 就接近 300 MB 显存。这在桌面端都很吃力移动端完全不可接受。逐 Attachment Resolve每张 MSAA RT 都需要单独 Resolve 才能作为普通纹理采样[RT0 (4x MSAA)] → Resolve → [RT0 (1x)] [RT1 (4x MSAA)] → Resolve → [RT1 (1x)] [RT2 (4x MSAA)] → Resolve → [RT2 (1x)] [RT3 (4x MSAA)] → Resolve → [RT3 (1x)]各 API 的 Resolve 方式API方式VulkanpResolveAttachments每个 Color Attachment 可指定 Resolve 目标可在 Tile 内完成Metal每个 Attachment 设resolveTexturestoreAction .multisampleResolveDX12ResolveSubresource()或 Render Pass 的 Resolve 参数DX11ResolveSubresource()逐张调用OpenGLglBlitFramebuffer逐 Attachment延迟渲染 MSAA 的三种策略延迟渲染 MSAA 有一个根本矛盾G-Buffer Pass 可以 MSAA但 Lighting Pass 是全屏 Quad / Compute不走光栅化没有硬件 MSAA。怎么办策略 ① Resolve 后再光照——简单但等于白做 MSAAG-Buffer (MSAA) → Resolve → G-Buffer (1x) → LightingResolve 时把每个像素的多个 sample 平均了边缘信息丢失。Lighting 对 Resolve 后的数据做光照——结果就是边缘的光照和非 MSAA 几乎一样。MSAA 的钱白花了。策略 ② Per-sample 光照——正确但极贵G-Buffer (MSAA, 不 Resolve) ↓ Lighting对每个像素的每个 sample 分别做光照计算 ↓ Resolve 最终颜色正确但昂贵边缘像素处着色量 ×44x MSAA而且 Lighting Pass 需要能读取 MSAA 纹理的单个 sampletexelFetchwith sample index。策略 ③ Stencil 标记自适应Adaptive MSAA——折中方案1. G-Buffer Pass 中用 SV_Coverage 或深度/法线不连续检测标记边缘像素 2. 对边缘像素执行 Per-sample Lighting 3. 对非边缘像素执行普通 Per-pixel Lighting 4. Resolve 最终颜色实际中只有约 10~20% 的像素是边缘像素所以 Per-sample 着色的代价可控。这是 CryEngine 等引擎曾采用的方案。历史性结局为什么业界从 MSAA 转向 TAA正因为 MRT MSAA 的代价如此高昂且延迟渲染 MSAA 存在根本矛盾业界从 2012 年前后逐步转向了其他抗锯齿方案FXAA / SMAA后处理抗锯齿不增加 RT 大小但质量有限TAATemporal Anti-Aliasing利用多帧累积实现超采样效果需要 Motion Vector RT 历史帧 RT但不需要 MSAA详见第 11.8 节Forward保留前向渲染天然支持 MSAA用 Tile 分光簇来处理多光源今天的主流 3A 游戏几乎都用 TAA 而非 MSAA。MRT MSAA 的组合只在特定场景下使用比如 Forward 管线 少量光源。FAQMRT MSAA 能共存吗能但所有 Attachment 的 Sample Count 必须一致且代价是 N × MN 个 RT × M 个采样点的显存和带宽。延迟渲染 MSAA 有根本矛盾业界已基本转向 TAA。本章小结MRT 让一次 Draw Call 同时写多张 RT——G-Buffer 是最典型的应用省了计算但没省带宽——每张 RT 的写入带宽不会因为 MRT 而减少SubpassVulkan/Metal可在 Tile 内传递 MRT 数据——省掉 Store/Load 的带宽MRT MSAA 代价爆炸——所有 Attachment 必须同 Sample Count显存 N × M延迟渲染 MSAA 有根本矛盾——Lighting Pass 不走光栅化无法利用硬件 MSAA设计哲学MRT——用空间换时间的经典范式 MRT 的核心取舍是用更多的显存/带宽空间换更少的计算量时间。不用 MRT你需要对每种数据画一遍场景N 遍绘制 N 倍顶点处理。用 MRT只画一遍但带宽乘以 N。这和数据库设计中的反范式化如出一辙把数据冗余存储到多张表空间膨胀换取查询时不用 JOIN时间减少。没有免费的午餐——省了一头另一头必然膨胀。优秀的引擎工程师的价值在于找到这个取舍的最佳平衡点。 思考题MRT 最多 8 个 Color Attachment如果你需要输出 10 种数据如 UE5 的复杂材质系统怎么办有几种解决方案如果 GPU 的写带宽无限大MRT 还有存在的价值吗它还能比画多遍更好吗提示考虑顶点处理和光栅化的开销Visibility Buffer 把 G-Buffer 从宽而浅变成窄而深第 14.1 节这是否意味着 MRT 的历史使命正在结束下一章讲 Compute Shader 与 RenderTarget 的关系——不走光栅化也能画。你将看到当 Shader 可以写任何地方而不再被 ROP 限制在当前像素时RT 从一个写入目的地变成了读写两用的画布——这打开的不只是后处理的大门还有 OIT、体素化、光线追踪输出等一系列远离传统管线的技术可能。