本文档详细梳理了UE5的渲染流水线,从游戏线程(Game Thread)到渲染线程(Render Thread)再到RHI执行的全过程。每个模块首先说明设计目的和功能目标,然后介绍相关实现细节,并列出关键源码类、函数和文件。
游戏线程:渲染对象的组织
目标: 在游戏线程上管理和维护所有可渲染的物体(Primitive),并为它们创建在渲染线程上使用的代理(Scene Proxy)。游戏线程负责组件状态管理、场景注册以及将必要的数据拷贝到渲染线程可访问的线程安全结构中。
在UE中,任何可渲染物体都是继承自 UPrimitiveComponent 的组件,比如 UStaticMeshComponent、USkinnedMeshComponent、UInstancedStaticMeshComponent 等。这些组件在被激活(Register)后,会调用其 CreateSceneProxy() 方法创建一个对应的渲染线程代理。该代理的基类为 FPrimitiveSceneProxy,它是游戏线程组件在渲染线程中的“镜像”表示。例如,UStaticMeshComponent::CreateSceneProxy() 返回一个 FStaticMeshSceneProxy(StaticMeshSceneProxy.h/cpp)实例;USkinnedMeshComponent::CreateSceneProxy() 返回 FSkeletalMeshSceneProxy。每个代理会将组件的几何数据、变换、材质相关信息等缓存在自身字段中,以便渲染线程使用。
创建好的场景代理并不会立即进行渲染,而是被包装在FPrimitiveSceneInfo对象中,该对象封装了组件和其代理的状态,并由全局 FScene(Scene.cpp)管理。FScene内部通常维护了一个加速结构(如八叉树)以进行视锥裁剪和碰撞检测。所有激活的可渲染组件在注册时会调用 FScene::AddPrimitive()(位于 Scene.cpp),从而将对应的 FPrimitiveSceneInfo 加入场景。这样,渲染线程在后续帧中就能访问到这些代理和相关数据。简而言之,游戏线程负责定义游戏世界的几何和材质状态,通过 FPrimitiveSceneProxy 和 FScene/FPrimitiveSceneInfo 将其安全地传递给渲染线程。
关键类与文件: UPrimitiveComponent及其派生类(UStaticMeshComponent、USkinnedMeshComponent 等);它们的 CreateSceneProxy() 实现位于 StaticMeshRender.cpp、SkinnedMeshComponent.cpp 等文件中(见文档)。在渲染模块内,FPrimitiveSceneProxy(及其子类如 FStaticMeshSceneProxy、FSkeletalMeshSceneProxy,对应头文件 StaticMeshSceneProxy.h、SkeletalMeshSceneProxy.h)、FPrimitiveSceneInfo(位于渲染模块,源码在 SceneRendering.cpp 或相关文件)和 FScene(Scene.h/cpp)等是核心结构。
游戏线程到渲染线程的通信
目标: 将游戏线程上的渲染对象及其更新传递给渲染线程,并确保在多线程并行渲染下数据一致、安全。
由于游戏线程和渲染线程并行运行,游戏线程在更新可渲染组件状态(例如变换、材质参数修改等)后,不能直接修改渲染线程用到的内存,而应使用线程安全的机制将修改传递过去。Unreal提供了多种机制,其中核心的是渲染命令队列。游戏线程通常通过 ENQUEUE_UNIQUE_RENDER_COMMAND 等宏封装一个无参或带参的Lambda,并插入到渲染线程的命令队列中,渲染线程会在合适时机执行该命令。例如,当一个组件注册到场景时,游戏线程会创建 FPrimitiveSceneProxy 并通过渲染命令将其添加到FScene中。
另一个常用机制是FRenderCommandFence。当游戏线程需要等待某些渲染线程的工作完成时,可调用 FRenderCommandFence::BeginFence() 然后 Wait(),直到渲染线程处理完毕。在资源初始化方面,诸如 FRenderResource(顶点缓冲、纹理等)只能在渲染线程初始化,所以引擎提供了 BeginInitResource() 等函数,在游戏线程上排队一个命令到渲染线程上执行其 InitResource()。
在渲染对象提交方面,当游戏线程调用 UPrimitiveComponent::RegisterComponent() 时,内部会生成对应的 FPrimitiveSceneProxy 并立即通过渲染命令将其附加到 FScene 中。之后每帧只要该代理可见,渲染线程就会调用其 DrawDynamicElements() 等方法进行渲染。
关键类与函数: ENQUEUE_UNIQUE_RENDER_COMMAND_XXX 宏实现(见引擎头文件 RenderCommandFence.h/RHICommandList.h);FRenderCommandFence(头 RenderCommandFence.h)。组件注册触发的 UActorComponent::RegisterComponent(ActorComponent.cpp)最终调用渲染线程相关代码。资源初始化通过 BeginInitResource()(RenderResource.h)调用。
渲染线程:接收与组织渲染对象
目标: 在渲染线程上遍历场景,计算每个代理在当前视图(FSceneView)中的可见性与渲染相关性,并生成最终的绘制命令。
渲染线程在每帧开始时会创建一个FSceneRenderer实例(在 SceneRendering.cpp 中实现,如 FDeferredShadingSceneRenderer 等子类),用于对一个或多个视口(View)进行渲染。每个 FSceneView(SceneView.h/cpp)代表一次视点,比如主摄像机视图或分屏的副摄像机。对应的内部表示是FViewInfo,包含渲染期间需要的临时数据。
渲染流程大致如下:首先调用 FSceneRenderer::InitViews(),该函数为每个视点构建视锥和相关信息;然后迭代场景中的所有FPrimitiveSceneInfo,调用每个代理的 GetViewRelevance(const FSceneView*) 方法,生成一个FPrimitiveViewRelevance对象。FPrimitiveViewRelevance 描述了该代理对当前视图的可见性和哪些渲染通道相关(例如它是否可绘制在基本通道、深度通道、阴影通道,是否会投射阴影等)。如果 bDrawRelevance 为真且 bStaticRelevance 为真,则引擎将该代理标记为静态可绘制对象,并调用其 DrawStaticElements() 方法(只在代理添加到场景时调用一次);如果 bDynamicRelevance 为真,则每帧都会调用其 DrawDynamicElements() 来生成动态网格批(如粒子、动画骨骼网格等)。这两个方法分别会生成若干FMeshBatch,代表最终要绘制的网格元素。
静态网格批(Cached Mesh Batches): 对于不随帧更新的部分(如静态网格几何),代理在注册时在 DrawStaticElements() 中创建并缓存 FMeshBatch(通常存放在 FPrimitiveSceneInfo::StaticMeshes 中)。这些静态批只在代理添加或属性变化时构建一次,后续每帧直接重用。
动态网格批(Dynamic Mesh Batches): 对于需要每帧更新的部分(如动画网格、粒子系统等),代理在每帧的 InitViews 阶段会执行 GetDynamicMeshElements() 方法,重新生成 FMeshBatch 并暂存到 FSceneRenderer::MeshElementCollector 中。这些批无需缓存,每帧重建。
上述批处理完成后,渲染线程会根据当前视锥和遮挡情况进行剔除,只保留可见的网格元素。这一过程通常在 FSceneRenderer::UpdatePrimitiveSceneInfo() 或 FSceneRenderer::ComputeViewVisibility() 中完成。此外,代理的 Bounds(包围盒)也用于遮挡剔除。最终,每个 FMeshBatch 连同其 FPrimitiveViewRelevance 信息被分别分配到各个渲染通道(如深度通道 BasePass、深度预通道 DepthPass、阴影通道 ShadowPass、透明通道等)的渲染列表中。
关键类与数据结构: FSceneRenderer(SceneRendering.cpp,对应每帧渲染器)、FViewInfo(SceneView.h/cpp,包含每帧渲染信息)、FPrimitiveSceneProxy(代理类)和 FPrimitiveViewRelevance(判断代理相关性)。每个代理的 GetViewRelevance、DrawDynamicElements、DrawStaticElements 等函数(如 FStaticMeshSceneProxy::DrawStaticElements 在 StaticMeshSceneProxy.cpp 中)在渲染线程中被调用。渲染通道的实现如 BasePassRendering.cpp、DepthPassRendering.cpp 等文件,分别处理各自通道的绘制。
渲染对象排序与通道划分
目标: 优化绘制顺序,减少状态切换和过度绘制,并将网格按渲染通道(Render Pass)分类以分阶段绘制。
Unreal将渲染流水线分为多个通道(如深度通道 DepthPrePass、基础通道 BasePass、阴影通道 ShadowPass、透明通道 Translucency 等),以便分别处理不同目的的绘制。在渲染器中,这些通道的调用顺序是静态定义的。例如,为了填充层次Z(Hi-Z)以加速后续渲染,DepthPass 通常在 BasePass 之前执行。此外,所有静态网格元素(来自 FPrimitiveSceneInfo::StaticMeshes)会先构建一个静态绘制列表(Static Mesh Draw List),并根据需要进行排序。这种排序通常基于几何与材质,以减少管线状态切换:比如按材质ID、顶点工厂类型甚至距离相机等做匹配/排序(类似 Matches/Compare 机制)。可见元素生成后,渲染线程会使用诸如 SortBasePassStaticPrimitives之类的函数来排序静态网格列表,以实现前向或后向排序(视具体情况而定),减小像素填充开销。
对于动态网格,它们通常无法大量合并或缓存,因此动态批尽量保持原始顺序,除非特殊优化(比如 GPU 进行硬件排序)。渲染线程在各通道中遍历相应列表时,会逐个将 FMeshDrawCommand 提交到管线。所示,渲染顺序设计为先Depth再Base,以满足Hi-Z等优化。透明通道会在所有不透明物体之后执行,以正确混合半透明内容。
关键流程与函数: 在 BasePassRendering.cpp 中的函数如 GatherBasePassMeshDrawCommands 会收集并排序基础通道的静态和动态网格,并生成绘制命令。排序函数如 SortFrontToBack、SortBasePassStaticPrimitives(源码中类似静态绘制列表的排序机制)将网格按优先级和材质状态排序。状态排序相关的比较函数位于各类 FMeshPassProcessor 和 FMeshDrawCommandSortKey 等代码中。
渲染命令的组织与封装
目标: 将每个网格渲染需求转换为最终提交给低层RHI(渲染硬件接口)的绘制命令,并尽可能缓存和合并,以提升渲染效率。
渲染线程生成的 FMeshBatch(包含顶点缓冲、索引缓冲、材质代理等)还不能直接交给GPU绘制。引擎接着使用网格通道处理器(Mesh Pass Processor)将它们转换为具体的绘制命令。每个渲染通道都有对应的 FMeshPassProcessor(如 FBasePassMeshProcessor),其 AddMeshBatch() 函数会根据当前材质、渲染状态等,决定如何生成FMeshDrawCommand。FMeshDrawCommand 是一个结构化的数据包,包含了绘制调用所需的一切信息:所用的着色器、顶点/索引缓冲绑定、常量缓冲绑定、绘制参数(基顶点、索引个数、实例数等)以及一个管线状态ID (FGraphicsMinimalPipelineStateId)。FMeshDrawCommand 完全位于渲染线程上下文,它是无状态(stateless)的,只依赖其字段来描述一次绘制调用。
引入 FMeshDrawCommand 允许引擎在RHI层之前进行缓存和合并:多个相同状态的命令可以合并以减少调用次数。静态绘制命令会存储在场景中,例如 FScene::CachedDrawLists[EMeshPass::BasePass] 的 MeshDrawCommands 列表中;动态绘制命令因为会根据视角变化,每帧生成,它们则通常存储在视图 (FViewInfo) 中的并行绘制列表中(FViewInfo::ParallelMeshDrawCommandPasses)。静态命令构建时机是代理加入场景的时刻,而动态命令构建发生在每帧的视图可见性计算阶段。
完成绘制命令生成后,渲染线程会调用诸如 SubmitMeshDrawCommands() 的函数,将每个FMeshDrawCommand转换为实际的 RHI 绘制调用,将其记录到一个 RHI 命令列表(FRHICommandList)中。此时还会创建或引用合适的管线状态对象(Graphics Pipeline State),并设置好对应的着色器和资源绑定。最终,每个 FMeshDrawCommand 通过调用其方法(如 SubmitDraw() 在 MeshDrawCommand.cpp 中)分解为若干 RHI 命令(例如绑定顶点缓冲、绑定管线状态、调用 RHIDrawIndexedPrimitive 等)。
关键类与文件: FMeshBatch(几何+材质等数据,定义在 MeshBatch.h),FMeshPassProcessor 及其派生类(如 FBasePassMeshProcessor 在 BasePassRendering.cpp),FMeshDrawCommand(定义在 MeshDrawCommand.h/cpp)和 FMeshDrawCommandStorage。管线状态ID类如 FGraphicsMinimalPipelineStateId(GraphicsPipelineState.h)用于缓存和查找PSO。RHI命令封装涉及 FRHICommandListImmediate(头 RHICommandList.h)和执行器 FRHICommandListExecutor(RHICommandList.cpp)。
渲染线程向RHI线程和GPU提交命令
目标: 将构建好的命令列表提交到底层渲染硬件接口(RHI)并最终交由GPU执行。
在渲染线程上,当所有渲染通道的 FMeshDrawCommand 都被记录到一个或多个 FRHICommandListImmediate(或其他命令列表)之后,便会调用 FRHICommandListExecutor::Submit() 将这些命令列表提交给RHI线程。RHI线程(对于支持显式命令队列的RHI,例如DX12/Metal)会最终调用底层API将命令调度给GPU。无论是否启用RHI并行,FRHICommandListImmediate 都封装了实际的渲染调用。
通常,提交过程在 FSceneRenderer::RenderFinish(RHICmdList) 之类的调用中完成。在编辑器中,游戏线程也可能使用 FlushRenderingCommands() 等函数等待渲染线程完成任务。最终,GPU驱动接收到渲染命令后,在GPU上执行绘制。此过程涉及如 RHISubmitCommandLists() 等底层调用(具体在每个平台的RHI实现中)。
关键类与文件: FRHICommandListImmediate 和 FRHICommandListExecutor(位于RHI模块,头文件 RHICommandList.h 和实现 RHICommandList.cpp),FRHIGraphicsPipelineState 等。实际上,这一步主要在引擎的渲染模块未提供源代码(私有),而是在各平台RHI代码中处理。开发者可在 RenderDependencyGraph 等现代接口中看到更多细节,但在传统渲染器中通常直接提交并调用硬件API。
特殊渲染路径和机制
静态网格和骨骼网格
常规的 静态网格 采用上述描述的基础通道流程,无特殊处理。骨骼网格(USkeletalMeshComponent),其代理类 FSkeletalMeshSceneProxy(SkeletalMeshSceneProxy.h/cpp)在渲染线程需处理骨骼动画计算结果。这包括从 CPU 端的骨骼姿势获取顶点变换并填充顶点着色器常量缓冲(或使用 GPU 骨骼变形时),然后按每个LOD和蒙皮权重生成对应的 FMeshBatch。骨骼网格在可见时也会分别调用 DrawStaticElements(针对其不随动画改变的部分)或 DrawDynamicElements(实时动画部分)。其间,FSkinnedMeshObject(SkinnedMeshObject.h/cpp)负责高效的顶点骨骼更新。重要的是,骨骼网格渲染代理在 GetViewRelevance() 中会为不同特性(是否描边、是否透明等)填充 FPrimitiveViewRelevance,从而决定走哪个通道。
Nanite 渲染
Nanite 是UE5中为超高多边形场景设计的虚拟化几何系统。其核心是预处理和聚合静态网格:在导入时,静态网格被拆分为多级层次的簇(Cluster)(一系列连续三角形)。渲染时,Nanite运行在独立的渲染通道中——一个自定义管线,不通过传统的 MeshDrawCall。它根据相机距离动态决定每个簇的详细程度,并仅流式加载可见数据。每帧,GPU在专用着色器中执行簇剔除和细节选择:只将满足细节阈值的簇发往光栅化。这意味着对开发者而言,开启Nanite后无需关心LOD,细节自动调整。支持Nanite的网格需要启用该功能,且通常对物体实例数量和动态变形有一定限制(如仅支持有限的世界位移和刚体变换,不支持蒙皮变形)。关键实现位于 NaniteSceneProxy.cpp、NaniteResources.cpp 等文件,其中 NaniteSceneProxy 会在场景中注册 Nanite 网格,并在渲染时生成Nanite管线的绘制命令(详见引擎源码)。
参考: UE官方文档总结道“在渲染时,簇会根据视角动态切换不同细节级别,并无缝拼接相邻簇,没有缝隙;数据按需流入内存,Nanite运行在自己的渲染通道,完全绕过传统绘制调用”。
Lumen 光照路径
Lumen 是UE5的全局光照(GI)和反射系统,主要依赖于软件光线追踪和多种加速结构。其重要特点是表面缓存(Surface Cache)机制:引擎为场景中的网格预计算若干个采样点(称为“卡片”),在这些位置捕获网格的材质属性和法线。这些卡片数据存储在 Surface Cache 中,用于快速查询间接光照。当渲染时,Lumen首先尝试在屏幕空间做快速追踪(Screen Tracing),如果找不到有效光照则使用全局方法(如对动态物体也支持的 Signed Distance Field 软件光线追踪,或者在支持硬件光线追踪的情况下使用GPU RTX)。Surface Cache 使得对静态场景的间接光照计算可以跨帧摊销:每帧只更新少量卡片,使系统可支持多灯光和多次反弹。Nanite可以加速Surface Cache的生成和更新,使高精度网格渲染更加高效。整体上,Lumen的追踪结构和光照缓存让动态光照具有良好的表现力,代价是较为复杂的系统实现(对应源码文件如 LumenSurfaceCache.h/cpp、LumenTracingData.h/cpp 等实现细节)。
参考: 官方文档指出Lumen会“对邻近场景表面生成自动参数化(Surface Cache),用于在光线击中时快速查找照明;对于未命中的情况,首先进行屏幕空间光线追踪,然后使用有向距离场(SDF)进行软件追踪,或在硬件RT时使用硬件追踪”。文档并提到“Lumen 用软件光追对SDF(Signed Distance Field)执行全局光照和反射”,以及Nanite可加速表面缓存捕获。