从前向渲染到延迟渲染:为什么3A游戏都在用Deferred?
两种算账方式两种命运从一个餐厅说起你开了一家餐厅。100桌客人10个厨师。方式一每桌每菜。服务员端着第一桌的菜单走进厨房。第一桌要红烧肉。10个厨师一起做红烧肉。做完了。第一桌还要糖醋鱼。10个厨师一起做糖醋鱼。做完了。第一桌还要宫保鸡丁。做完了。第一桌的菜全部上齐。服务员拿着第二桌的菜单走进厨房。第二桌要红烧肉。等等刚才不是做过红烧肉吗但那是第一桌的。第二桌的红烧肉要重新做。100桌客人每桌3道菜10个厨师。每道菜都要单独做。如果有50桌都点了红烧肉红烧肉就要做50次。方式二每菜每桌。服务员先收集所有桌的菜单。统计一下50桌要红烧肉30桌要糖醋鱼80桌要宫保鸡丁。厨师先做红烧肉。一次做50份。锅热着调料备着流水线作业。50份红烧肉一气呵成。然后做糖醋鱼。一次做30份。然后做宫保鸡丁。一次做80份。方式一是前向渲染。方式二是延迟渲染。桌子 物体。菜 光源。厨师 GPU。一、前向渲染每个物体算所有光1.1 工作方式前向渲染Forward Rendering的逻辑非常直觉for each 物体: for each 影响这个物体的光源: 计算这个光源对这个物体的贡献 输出最终颜色画第一个物体。这个物体受到哪些光的影响主方向光、两个点光源、一个聚光灯。在片段着色器里对每个像素计算这4个光源的贡献加在一起输出颜色。画第二个物体。这个物体受到哪些光的影响主方向光、三个点光源。计算4个光源的贡献输出颜色。画第三个物体。画第四个物体。画第一百个物体。每个物体的每个像素都要在片段着色器里计算所有影响它的光源。1.2 光源少的时候场景里只有1个方向光太阳。物体1计算1个光源 → 输出颜色 物体2计算1个光源 → 输出颜色 物体3计算1个光源 → 输出颜色 ... 物体100计算1个光源 → 输出颜色每个物体的片段着色器只做一次光照计算。很快。没问题。1.3 光源多的时候场景里有1个方向光 100个点光源室内场景到处都是灯。物体1计算101个光源 → 输出颜色 物体2计算101个光源 → 输出颜色 物体3计算101个光源 → 输出颜色 ... 物体100计算101个光源 → 输出颜色每个物体的片段着色器要做101次光照计算。但等等不是每个光源都影响每个物体。一个点光源的照射范围有限可能只影响附近的几个物体。所以实际上物体1受5个光源影响 → 计算5次 物体2受3个光源影响 → 计算3次 物体3受12个光源影响 → 计算12次 ...问题是GPU怎么知道哪些光源影响哪个物体1.4 光源剔除的困境最笨的方法每个物体都计算所有101个光源。大部分光源对大部分像素的贡献为零太远了但GPU不知道它必须算完才知道。100个物体 × 101个光源 10100次光照计算per pixel 实际有效的可能只有500次 浪费率95%稍微聪明一点CPU端做光源剔除。对每个物体判断哪些光源在它的范围内。只把有效的光源传给Shader。物体1附近有5个光源 → Shader里循环5次 物体2附近有3个光源 → Shader里循环3次但这带来了新问题每个物体的光源数量不同Shader里的循环次数不同。GPU喜欢所有线程做同样的事情SIMD。如果一个Warp里的32个像素有的要算5个光源有的要算12个光源GPU要等最慢的那个。其他像素算完了也得等着。更大的问题每个物体一个Draw Call。如果不同物体受不同光源影响它们的Shader参数不同光源列表不同不能合批。100个物体就是100个Draw Call。最大的问题Overdraw。一个像素被三个物体覆盖。前两个物体的像素最终被第三个物体遮挡了。但前向渲染不知道——它画第一个物体的时候不知道后面还有物体会覆盖它。所以前两个物体的光照计算白做了。像素(500, 300) 物体A覆盖计算8个光源 → 写入颜色 → 被覆盖了白算了 物体B覆盖计算5个光源 → 写入颜色 → 被覆盖了白算了 物体C覆盖计算3个光源 → 写入颜色 → 最终可见 总计算量8 5 3 16次光照计算 有效计算量3次 浪费率81%Overdraw 多光源 灾难性的浪费。二、延迟渲染先记录后算账2.1 核心思想延迟渲染Deferred Rendering把渲染分成两个阶段第一阶段几何阶段画所有物体但不做光照计算。只记录每个像素的几何信息位置、法线、材质颜色、金属度、粗糙度等。第二阶段光照阶段对每个像素用记录的几何信息计算所有光源的贡献。阶段一记录信息 for each 物体: for each 像素: 记录位置、法线、颜色、金属度、粗糙度 写入G-Buffer 阶段二计算光照 for each 像素: 读取G-Buffer中的信息 for each 影响这个像素的光源: 计算光照贡献 输出最终颜色光照计算被延迟到了第二阶段。这就是Deferred的含义。2.2 G-Buffer几何信息的快照G-BufferGeometry Buffer是延迟渲染的核心数据结构。它是一组纹理存储了每个像素的几何和材质信息。G-Buffer通常包含 纹理0RGBA8漫反射颜色.rgb 金属度.a ┌────────────────────────────┐ │ R: 漫反射红色分量 │ │ G: 漫反射绿色分量 │ │ B: 漫反射蓝色分量 │ │ A: 金属度 │ └────────────────────────────┘ 纹理1RGB10A2世界空间法线.rgb 粗糙度或其他 ┌────────────────────────────┐ │ R: 法线X │ │ G: 法线Y │ │ B: 法线Z │ │ A: 粗糙度2bit或存别处 │ └────────────────────────────┘ 纹理2RGBA8粗糙度 AO 自发光 ... ┌────────────────────────────┐ │ R: 粗糙度 │ │ G: 环境光遮蔽 │ │ B: 自发光强度 │ │ A: 其他标记 │ └────────────────────────────┘ 深度缓冲D32F深度值可以反推世界坐标G-Buffer就是场景的一张X光片。它记录了每个像素的所有几何和材质信息但没有任何光照信息。2.3 几何阶段画所有物体。片段着色器不做光照计算只输出几何信息到G-Buffer。// 几何阶段的片段着色器极其简单 layout(location 0) out vec4 gAlbedoMetallic; layout(location 1) out vec4 gNormalRoughness; void main() { vec3 albedo texture(albedoMap, uv).rgb; float metallic texture(metallicMap, uv).r; vec3 normal calculateNormal(); // TBN变换 float roughness texture(roughnessMap, uv).r; gAlbedoMetallic vec4(albedo, metallic); gNormalRoughness vec4(normal * 0.5 0.5, roughness); }注意没有光照计算。没有for each light。没有BRDF。没有阴影。片段着色器只做了纹理采样和法线变换。非常轻量。Overdraw的代价大幅降低。一个像素被三个物体覆盖前两个的几何信息被第三个覆盖了。但几何阶段的片段着色器很轻几次纹理采样浪费的计算量很少。前向渲染的Overdraw代价 物体A8次光照计算 → 白算了 物体B5次光照计算 → 白算了 浪费13次光照计算 延迟渲染的Overdraw代价 物体A3次纹理采样 → 白做了 物体B3次纹理采样 → 白做了 浪费6次纹理采样比光照计算便宜得多2.4 光照阶段G-Buffer填充完毕。现在每个像素的几何信息都已经确定了。画一个全屏三角形。对每个像素从G-Buffer读取几何信息计算所有光源的贡献。// 光照阶段的片段着色器 void main() { // 从G-Buffer读取信息 vec3 albedo texture(gAlbedoMetallic, uv).rgb; float metallic texture(gAlbedoMetallic, uv).a; vec3 normal texture(gNormalRoughness, uv).rgb * 2.0 - 1.0; float roughness texture(gNormalRoughness, uv).a; vec3 worldPos reconstructPosition(uv, depth); // 从深度重建位置 // 计算所有光源 vec3 finalColor vec3(0.0); for (int i 0; i numLights; i) { finalColor calculatePBR(albedo, metallic, roughness, normal, worldPos, lights[i]); } fragColor vec4(finalColor, 1.0); }关键光照计算只对最终可见的像素执行。G-Buffer里存的是深度测试之后的结果。被遮挡的像素已经被覆盖了。光照阶段处理的每一个像素都是最终可见的。零Overdraw的光照计算。前向渲染 207万像素 × Overdraw 2.5 × 平均8个光源 4140万次光照计算 延迟渲染 207万像素 × 1无Overdraw× 平均8个光源 1656万次光照计算 节省60%2.5 光源的处理方式延迟渲染对光源的处理方式非常优雅。方向光太阳影响所有像素。画一个全屏三角形对每个像素计算方向光。点光源只影响一定范围内的像素。画一个球体光源的影响范围只有球体覆盖的像素才执行光照计算。点光源A位置(100, 50, 30)半径10米 在屏幕上投影成一个圆形区域 画一个球体Mesh覆盖这个区域 只有被球体覆盖的像素才执行片段着色器 片段着色器从G-Buffer读取信息计算点光源A的贡献100个点光源 100个球体。每个球体只覆盖屏幕的一小部分。大部分像素只被少数几个球体覆盖。光源的开销跟它的屏幕覆盖面积成正比而不是跟场景中物体的数量成正比。一个远处的小灯泡在屏幕上只覆盖几十个像素。它的光照计算只对这几十个像素执行。几乎免费。这就是延迟渲染能支持几百个光源的原因。三、延迟渲染的代价延迟渲染不是免费的午餐。它有自己的代价。3.1 G-Buffer的带宽G-Buffer通常有3-4张纹理加上深度缓冲。纹理0RGBA88.3MB 纹理1RGBA88.3MB 纹理2RGBA88.3MB 深度D32F8.3MB 总计33.2MB几何阶段要写入33.2MB。光照阶段要读取33.2MB。总带宽66.4MB。前向渲染只有一个颜色缓冲8.3MB和一个深度缓冲8.3MB。总带宽16.6MB。延迟渲染的带宽是前向渲染的4倍。在PC上GDDR6X的带宽有1TB/s66.4MB不算什么。在手机上30-50GB/s的带宽66.4MB是一个沉重的负担。这就是为什么移动端很少用传统的延迟渲染。3.2 半透明物体延迟渲染的G-Buffer只能存一层几何信息。每个像素只有一个法线、一个颜色、一个深度。半透明物体怎么办半透明物体需要跟后面的物体混合。但G-Buffer里只有最前面那层的信息。后面那层的信息已经被覆盖了。解决方案半透明物体用前向渲染。阶段一延迟渲染的几何阶段不透明物体 阶段二延迟渲染的光照阶段 阶段三前向渲染半透明物体叠加在延迟渲染的结果上半透明物体是延迟渲染的阿喀琉斯之踵。每个3A游戏都要处理这个问题。通常的做法是不透明物体用延迟渲染半透明物体用前向渲染。两套管线并存。3.3 MSAA的困难前向渲染可以直接用MSAA。每个采样点独立执行片段着色器或者用更高效的方式。延迟渲染的G-Buffer如果要用MSAA每个采样点都要存一份几何信息。G-Buffer的大小翻倍MSAA 2x或翻四倍MSAA 4x。MSAA 4x的G-Buffer 33.2MB × 4 132.8MB132.8MB的G-Buffer。带宽爆炸。所以延迟渲染通常不用MSAA而用后处理抗锯齿TAA、FXAA。3.4 材质多样性的限制G-Buffer的格式是固定的。所有物体的几何信息都要塞进同样的几张纹理里。如果一个物体需要额外的材质参数比如次表面散射的厚度、各向异性的方向G-Buffer里没有地方存。解决方案在G-Buffer里预留一些通道给特殊材质用材质ID标记不同的材质类型光照阶段根据ID选择不同的光照模型用更多的G-Buffer纹理但带宽更大四、为什么3A游戏还是选择延迟渲染因为3A游戏的场景特点决定了延迟渲染的优势远大于劣势。4.1 光源数量3A游戏的室内场景可能有几十到几百个光源。走廊里的壁灯、房间里的台灯、窗外的路灯、爆炸的火光、枪口的闪光。前向渲染处理100个光源每个像素在片段着色器里循环100次。即使做了光源剔除每个像素平均也要算10-20个光源。延迟渲染处理100个光源画100个光源体积。每个光源只影响它覆盖的像素。大部分像素只被3-5个光源覆盖。延迟渲染在多光源场景下的优势是压倒性的。4.2 场景复杂度3A游戏的场景极其复杂。几百万个三角形Overdraw可能达到3-5倍。前向渲染每个被覆盖的像素都要做完整的光照计算。Overdraw 3倍意味着光照计算量翻3倍。延迟渲染几何阶段的Overdraw只浪费轻量的纹理采样。光照阶段零Overdraw。4.3 后处理的需求3A游戏需要大量后处理效果SSAO、SSR、景深、运动模糊。这些效果需要深度、法线、运动向量等信息。延迟渲染的G-Buffer天然提供了这些信息。前向渲染如果要做SSAO需要额外的Pass来输出法线和深度。相当于做了半个延迟渲染。4.4 PC/主机的硬件特点PC和主机的GPU是IMR架构显存带宽大几百GB/s到1TB/s以上。G-Buffer的带宽开销在预算之内。PC和主机的GPU算力强。光照阶段的全屏计算不是瓶颈。延迟渲染的劣势带宽大在PC/主机上不明显。延迟渲染的优势多光源、低Overdraw浪费在3A游戏的复杂场景里非常明显。五、移动端的选择移动端的情况完全不同。带宽小。功耗敏感。TBR/TBDR架构。传统延迟渲染的G-Buffer带宽在移动端是不可接受的。但移动端有一个独特的优势Subpass。在Vulkan/Metal的TBR架构上同一个RenderPass内的多个Subpass可以共享片上内存。G-Buffer不需要写回主内存。RenderPass { Subpass 1几何阶段 输出G-Buffer到片上内存 不写回主内存 Subpass 2光照阶段 从片上内存读取G-Buffer 计算光照 输出最终颜色 }G-Buffer从来没有离开片上内存。带宽开销为零。这就是**移动端延迟渲染Mobile Deferred**的核心思路。用Subpass避免G-Buffer的带宽开销。但有限制Subpass只能读取当前像素的G-Buffer值不能读取邻域像素。所以需要邻域采样的效果SSAO、SSR不能在Subpass里做。六、总结前向渲染 延迟渲染 ───────────────────────────────────────────────────── 光照计算时机 画物体的时候 画完所有物体之后 Overdraw浪费 严重光照白算 轻微只浪费纹理采样 多光源性能 差每像素算所有光源 好每光源只算覆盖的像素 带宽 低 高G-Buffer 半透明 天然支持 需要额外处理 MSAA 天然支持 困难 材质多样性 灵活 受G-Buffer格式限制 适合场景 光源少、移动端 光源多、PC/主机前向渲染像一个全能选手。什么都能做但什么都不极致。光源少的时候很好光源多的时候崩溃。延迟渲染像一个专家。在多光源、复杂场景下无可匹敌。但半透明、MSAA、带宽是它的软肋。3A游戏选择延迟渲染不是因为它完美而是因为在3A游戏的典型场景下它的优势远远大于劣势。100个光源。几百万个三角形。Overdraw 3倍。在这种场景下前向渲染的GPU在燃烧。延迟渲染的GPU在微笑。这就是为什么3A游戏都在用Deferred。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2442725.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!