1. 项目概述深入UE5渲染管线的“守门员”在UE5引擎的渲染世界里每一帧画面都像是一场精密的接力赛。从场景中的成千上万个物体到最终呈现在屏幕上的像素中间要经过几何处理、光照计算、后处理等多个环节。如果让所有物体无论远近、是否在视野内都完整地跑完整个渲染流程那对GPU来说无疑是场灾难。这时就需要一个高效的“守门员”——延迟剔除。延迟剔除在UE5的语境下特指在延迟渲染管线中发生在几何通道之后、光照通道之前的一个关键优化步骤。它的核心任务非常明确在像素级别精确地判断哪些片元Fragment需要继续进行昂贵的光照计算哪些可以直接丢弃。这听起来简单但要在保证画面绝对正确的前提下实现极致的性能提升里面充满了工程智慧。我花了相当长的时间在UE5的源码海洋里追踪这个编号为“169”的提交或模块具体指代需结合引擎版本但核心逻辑相通试图理清它的脉络。这不仅仅是为了理解一段代码更是为了掌握现代游戏引擎如何在高复杂度场景下依然保持流畅帧率的核心秘诀。无论你是正在攻坚性能瓶颈的图形程序员还是对UE5底层机制充满好奇的技术美术理解延迟剔除都能让你对渲染管线的掌控力提升一个维度。2. 延迟剔除的核心价值与设计哲学2.1 为什么要“延迟”剔除在传统的正向渲染中剔除主要发生在三角形层面即视锥体剔除和遮挡剔除。这些剔除发生在顶点处理阶段之前目的是减少进入光栅化的三角形数量。然而在延迟渲染中情况发生了变化。延迟渲染将几何信息位置、法线、材质属性等先渲染到一系列缓冲区中我们称之为G-Buffer。之后光照计算作为一个独立的Pass读取G-Buffer中的信息在屏幕空间进行。这就带来了一个新问题G-Buffer中的每一个像素都代表了一个世界空间中的点但并非所有点都需要被所有光源照亮。例如一个像素位于阴影中或者它距离某个光源过远以至于该光源的影响可以忽略不计。如果在光照Pass中仍然对所有像素计算所有光源会造成巨大的浪费。因此“延迟剔除”应运而生它的剔除发生在像素级别在光照计算之前目标是减少每个像素需要处理的光源数量。2.2 设计目标在精度与性能间寻找平衡延迟剔除系统的设计始终围绕着几个核心目标展开最大化剔除率这是最直接的目标即尽可能多地识别出那些完全不需要进行光照计算的像素-光源对。剔除得越多GPU在光照Pass的工作负载就越轻。保证视觉正确性剔除必须是保守的。宁可多算不可漏算。因为漏掉一个本该计算的光源会导致画面出现黑块、光照错误这是绝对不允许的。因此所有剔除判断都需要一个“安全边界”。开销可控剔除操作本身也是有成本的。如果为了剔除而进行的计算比直接进行光照计算还要昂贵那就本末倒置了。因此剔除算法必须非常高效通常依赖于预先计算好的数据结构和简单的数学比较。适应多种光源类型UE5场景中可能包含平行光、点光源、聚光灯等多种光源它们的影响范围边界球、锥体不同剔除算法需要能统一或分别高效处理。UE5的延迟剔除系统正是基于这些目标构建了一套多层次、渐进式的剔除策略。3. 源码脉络解析从FDeferredShadingSceneRenderer开始要找到延迟剔除的代码我们的起点通常是负责延迟渲染的主要类——FDeferredShadingSceneRenderer。在Render函数调用链中我们会追踪到光照处理的部分。一个关键的函数是RenderLights或RenderLight相关的函数。在这些函数中引擎会遍历场景中的所有可见光源并为每个光源准备绘制调用。而剔除逻辑就嵌入在这个准备过程中。具体到“169”这个线索它可能指向一个特定的提交哈希、代码文件行号或是一个内部模块标识。在UE5的源码中与延迟剔除紧密相关的代码通常分布在以下文件中DeferredShadingRenderer.cpp这是延迟渲染实现的核心文件大量的渲染Pass逻辑都在这里。LightRendering.h/.cpp光源渲染的具体实现包含光源边界计算、着色器参数设置等。SceneVisibility.cpp虽然更侧重于早期的物体可见性判断但其中一些数据结构如分块也可能被延迟剔除复用。Shader目录下的各种光照着色器DeferredLightPixelShaders.usf等剔除逻辑的GPU端实现。剔除的核心思想可以概括为为每个光源计算其在屏幕空间的潜在影响区域通常是一个2D边界矩形然后利用深度缓冲区Depth Buffer和屏幕空间信息快速判断这个区域内哪些像素实际上位于该光源的有效范围之外。3.1 CPU端的粗粒度剔除在提交绘制命令到GPU之前CPU会先进行一轮粗剔除光源视锥体剔除与物体剔除类似首先判断光源的包围球或包围盒是否在摄像机视锥体内。完全在外的光源直接跳过。计算屏幕空间影响矩形对于通过视锥体测试的光源将其在世界空间中的影响范围如点光源的球体投影到屏幕空间得到一个2D的矩形区域。这个矩形是光源可能影响到的所有像素的超集。分块Tiled剔除的准备工作现代GPU渲染常用分块延迟渲染。CPU端会将屏幕划分为许多小块例如16x16像素并预先为每个块计算一些共享信息如深度范围但更精细的剔除通常在GPU着色器中完成。注意CPU端的剔除必须是保守的。计算屏幕矩形时通常会稍微扩大一些比如加上一个偏移以确保不会意外裁剪掉真正应该被照亮的像素。这个“安全边界”的大小需要根据场景尺度精心调整。3.2 GPU端的精粒度剔除着色器中的奥秘真正的性能杀手锏在GPU着色器里。在光照像素着色器开始计算辐照度之前会先执行一系列的剔除测试。以点光源为例在一个典型的DeferredLightPixelShaders中你可能会看到类似下面的逻辑以伪代码形式说明// 从G-Buffer中读取当前像素的世界位置和深度 float3 WorldPos GetWorldPositionFromGBuffer(ScreenUV); float PixelDepth SampleDepthBuffer(ScreenUV); // 获取当前光源的参数已在常量缓冲区中 float3 LightPosition; float LightRadius; float LightFalloffExponent; // 计算像素到光源的距离 float DistanceToLight distance(WorldPos, LightPosition); // 核心剔除判断1距离剔除 if (DistanceToLight LightRadius) { discard; // 或 return 0; 直接丢弃该像素的光照贡献 } // 核心剔除判断2基于深度范围的优化在分块渲染中 // 假设我们已知当前像素所在屏幕分块的最近和最远深度MinDepth, MaxDepth // 可以快速判断整个块是否完全在光源范围之外或之内进行块级别的提前退出。 // 这通常在计算着色器或更早的阶段完成。 // 如果通过所有剔除测试则进行昂贵的光照计算如Phong/BRDF float3 Lighting CalculatePhongLighting(WorldPos, Normal, LightPosition, LightColor, ...); return Lighting;这里的关键在于discard或提前返回。一旦像素被判定为不受此光源影响后续所有复杂的光照模型计算、纹理采样、复杂数学运算都将被跳过节省了大量的GPU周期。对于聚光灯还需要增加一个方向锥体的测试对于平行光虽然通常没有距离剔除但可能会有基于阴影贴图的屏幕空间遮挡剔除。4. 深入核心深度边界与屏幕分块加速4.1 利用深度缓冲区进行快速剔除这是延迟剔除中最常用且高效的技巧之一。对于每个光源的屏幕空间影响矩形我们可以获取这个矩形区域内深度缓冲区的最大值和最小值Zmax和Zmin。情况A如果计算发现该矩形内所有像素的最近深度Zmin都比光源的最大影响范围投影到深度值还要远那么整个矩形区域都可以被剔除这个光源无需对该区域进行任何渲染。情况B反之如果最远深度Zmax比光源的最近影响范围还要近说明整个矩形区域都在光源影响范围内可以跳过逐像素的距离比较直接进行光照计算。但这需要谨慎因为深度缓冲区存储的是最浅深度可能存在被遮挡的物体所以此优化通常用于不透明物体且需要特定条件。在实际实现中UE5可能会使用Hi-Z层次化深度缓冲区技术来加速这个过程。Hi-Z构建了一个深度缓冲区的金字塔链每一层是下一层的降采样。在高层级上可以快速判断一个大区域与光源范围的粗略关系从而避免对低层级高分辨率的深度缓冲区进行大量采样。4.2 分块延迟渲染Tiled Deferred Rendering这是将延迟剔除发挥到极致的架构。它将屏幕分割成许多固定大小的瓦片Tile如32x32。在光照计算之前先运行一个“光源分配”的计算着色器收集光源对每个屏幕瓦片遍历所有光源。利用瓦片的包围盒在视图空间或世界空间和该瓦片的深度范围从深度缓冲区计算得出快速剔除掉那些完全不影响该瓦片的光源。创建光源列表为每个瓦片生成一个紧凑的光源索引列表只包含真正可能影响该瓦片的光源。执行光照随后像素着色器在运行时只需根据像素所在的瓦片从对应的紧凑光源列表中读取光源数据进行计算。这样每个像素处理的光源数量从场景总光源数锐减到其所在瓦片的相关光源数性能提升巨大尤其适合拥有大量小型光源的场景如霓虹灯、粒子效果丰富的场景。在UE5的源码中你可能会在FDeferredShadingSceneRenderer::RenderTiledDeferredLighting或类似函数中找到这套逻辑。计算着色器部分会看到大量的线程组Thread Group操作、共享内存Shared Memory的使用用于高效地协作完成每个瓦片的光源剔除和列表构建。5. 实战中的注意事项与调优心得阅读源码理解了原理但要在实际项目中用好延迟剔除避免踩坑还需要一些实战经验。5.1 性能分析与调试工具GPU Profiler使用RenderDoc或Nsight等工具抓取一帧。观察RenderLights相关的Pass。如果一个光源的绘制调用覆盖了整个屏幕但像素着色器耗时却很低说明延迟剔除特别是discard效率很高。反之如果耗时高可能需要检查光源半径是否过大或者剔除判断是否有问题。可视化剔除结果可以编写一个简单的后期材质或调试着色器将不同剔除原因如距离剔除、锥体剔除的像素用不同颜色显示出来。这能直观地看到剔除的效果和边界是否准确。Stat GPU和Stat SceneRendering在UE编辑器内使用这些命令可以查看每帧处理的光源数量、绘制调用次数等宏观指标帮助判断剔除系统是否正常工作。5.2 常见性能陷阱与调优光源半径Attenuation Radius设置不当这是最常见的性能杀手。美术同学有时为了让光照效果“看起来够亮”会把衰减半径设得非常大。这会导致光源的屏幕空间影响矩形巨大剔除效率急剧下降。务必在编辑器中强制审查并优化每个光源的衰减半径确保它和视觉影响范围基本匹配。可以编写自动化检查脚本标记出半径异常的光源。半透明物体的干扰延迟渲染通常只处理不透明物体。半透明物体是在延迟渲染之后用正向渲染叠加的。这意味着延迟剔除基于的深度缓冲区不包含半透明物体。如果一个半透明物体后面有一个点光源该光源可能因为被不透明物体深度剔除而无法照亮那个半透明物体导致视觉错误。对于重要的半透明物体如窗户玻璃后的光源可能需要特殊处理或让美术调整光源位置和范围。动态阴影与剔除的交互如果一个光源开启了动态阴影如级联阴影贴图CSM即使某个像素在延迟剔除中被判定为不受该光源直接照射它仍然可能需要参与阴影深度的渲染。这意味着剔除不能完全避免该光源带来的开销。对于移动平台或性能紧张的场景需要严格控制开启动态阴影的光源数量。分块大小的选择在分块延迟渲染中瓦片大小是一个权衡。瓦片越小光源分配越精确每个像素处理的光源越少但分配阶段的开销和光源列表的管理开销会增大。UE5通常会根据平台选择默认值如PC用32x32移动端可能用16x16。除非有充分理由和性能测试数据否则不要轻易修改。5.3 针对特定场景的优化策略室内场景室内通常有大量小型点光源和聚光灯。分块延迟渲染在这里收益最高。确保墙壁、地板等大型物体有良好的光照贴图或反射探针可以减少实时光源的数量和强度。超大开放世界平行光是主角点光源/聚光灯可能集中在玩家据点。对于远处的众多小光源可以考虑使用一种称为“光源簇Light Clustering”的升级版技术它在视图空间的三维网格中进行光源分配比屏幕空间的2D分块更适合深度变化剧烈的场景。UE5的移动渲染器可能使用了变种。同时积极使用光照重要性体积Light Importance Volume来动态禁用远处光源的渲染。大量粒子特效每个发光的粒子都可能是一个小型点光源。如果数量成百上千即使是分块剔除也压力巨大。这时应考虑将粒子光照烘焙到体积纹理中或者使用更简化的光照模型如只影响漫反射的顶点光照。6. 从“169”提交看引擎演进虽然我无法直接定位到“169”这个具体标识对应的代码行但追踪UE版本间延迟渲染和剔除代码的改动是理解引擎发展的绝佳窗口。例如从UE4到UE5在延迟渲染路径上一个显著的变化是Lumen全局光照和Nanite虚拟几何体的集成。Lumen使用屏幕空间追踪和Mesh Distance Field这改变了场景的可见性信息和间接光照的计算方式。延迟剔除系统需要与Lumen协作确保被Lumen考虑到的间接光照贡献者通常是较大的表面不会被过早剔除。你可能在源码中看到新的判断条件例如检查像素是否在Mesh Distance Field的某个影响范围内。Nanite则通过其自身的超精细裁剪和细节层次在几何阶段就进行了极其高效的剔除。这实际上减轻了延迟剔除阶段的部分压力因为提交到光栅化的三角形已经是最优集合。但延迟剔除仍需处理这些超密集几何产生的像素。阅读这类提交的启示在于优化不是孤立的。延迟剔除的演进始终跟随着渲染架构的整体变革。当你修改或调试剔除相关代码时必须考虑它与其他系统阴影、GI、后处理的联动。一个看似单纯的剔除优化可能会在另一个系统引起难以察觉的视觉瑕疵。7. 自定义剔除当默认方案不够用时UE5提供的延迟剔除系统已经非常强大和通用。但在某些极端或特殊的项目需求下你可能需要实现自定义的剔除逻辑。场景案例一个需要模拟真实光线传播、存在大量微小缝隙和复杂遮挡的密室逃脱游戏。标准的光源半径剔除和屏幕分块可能效果不佳因为光线通过缝隙照亮另一侧局部区域的情况很常见标准剔除容易误杀。实现思路扩展G-Buffer可以在自定义渲染通道中向G-Buffer添加额外信息例如粗略的体素化场景表示ID或者预先计算好的“光照连通性”贴图。自定义计算着色器编写一个CS在光源分配阶段不仅考虑深度范围还读取这些额外信息。例如对于每个光源采样其位置周围小范围的“连通性贴图”如果贴图显示与当前瓦片区域不连通则即使几何距离足够也可以安全剔除。集成到渲染管线这需要修改引擎的着色器编译管线添加你自己的CS并挂钩到FDeferredShadingSceneRenderer的渲染流程中。你需要仔细研究AddRenderPass的调用、RDG渲染依赖图的构建确保你的Pass在正确的时间以正确的依赖关系被执行。警告自定义剔除逻辑的调试极其困难。你必须使用前文提到的可视化工具并准备大量的测试场景来验证剔除的正确性。一个错误的剔除判断导致的画面错误可能在复杂的场景中潜伏很久才被发现。因此除非万不得已尽量通过调整光源参数、使用光照贴图、设计关卡等上层手段来规避性能问题而非直接修改底层剔除引擎。理解UE5的延迟剔除就像拿到了一张高性能渲染管线的微观电路图。它告诉你引擎是如何在像素的洪流中聪明地省下每一分计算力。这份理解能让你在遇到渲染性能问题时不再盲目地降低分辨率或关闭特效而是能够进行精准的诊断和调优。从宏观的架构选择到微观的着色器指令性能优化之路正是由这样一个个深刻理解所铺就的。