UE4两种静态绘制路径详解
在UE静态绘制路径中,针对VF是否需要view,会决定能否复用已创建好的MeshDrawCommand来做优化

注:如果VF与view相关,就需要为每帧为每个view重新创建MeshDrawCommand
在 UE 的静态物体可见性计算(FRelevancePacket → AddCommandsForMesh,位于 SceneVisibility.cpp)中,对每个静态网格批次(FStaticMeshBatch)都要决定它的 FMeshDrawCommand 走哪条路径
// UnrealEngine\Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp struct FDrawCommandRelevancePacket { // ... ... void AddCommandsForMesh( int32 PrimitiveIndex, const FPrimitiveSceneInfo* InPrimitiveSceneInfo, const FStaticMeshBatchRelevance& RESTRICT StaticMeshRelevance, const FStaticMeshBatch& RESTRICT StaticMesh, const FScene* RESTRICT Scene, bool bCanCache, EMeshPass::Type PassType) { // ... ... const bool bUseCachedMeshCommand = bUseCachedMeshDrawCommands && !!(FPassProcessorManager::GetPassFlags(ShadingPath, CommandSourcePass) & EMeshPassFlags::CachedMeshCommands) && StaticMeshRelevance.bSupportsCachingMeshDrawCommands && bCanCache; if (bUseCachedMeshCommand) { // ... 直接复用 Scene 级缓存好的 MeshDrawCommand VisibleCachedDrawCommands[(uint32)PassType].AddUninitialized(); // 从 Scene->CachedDrawLists / CachedMeshDrawCommandStateBuckets 取出已构建好的命令 } else { // ... 本帧、按 View 动态构建 DynamicBuildRequests[PassType].Add(&StaticMesh); } // ... ... } };
其中StaticMeshRelevance.bSupportsCachingMeshDrawCommands变量的赋值来自于如下逻辑:
// UnrealEngine\Engine\Source\Runtime\Renderer\Private\PrimitiveSceneInfo.cpp class FBatchingSPDI : public FStaticPrimitiveDrawInterface { public: // ... ... virtual void DrawMesh(const FMeshBatch& Mesh, float ScreenSize) final override { if (Mesh.HasAnyDrawCalls()) { checkSlow(IsInParallelRenderingThread()); FPrimitiveSceneProxy* PrimitiveSceneProxy = PrimitiveSceneInfo->Proxy; const ERHIFeatureLevel::Type FeatureLevel = PrimitiveSceneInfo->Scene->GetFeatureLevel(); if (!Mesh.Validate(PrimitiveSceneProxy, FeatureLevel)) { return; } FStaticMeshBatch* StaticMesh = new(PrimitiveSceneInfo->StaticMeshes) FStaticMeshBatch( PrimitiveSceneInfo, Mesh, CurrentHitProxy ? CurrentHitProxy->Id : FHitProxyId() ); StaticMesh->PreparePrimitiveUniformBuffer(PrimitiveSceneProxy, FeatureLevel); // Volumetric self shadow mesh commands need to be generated every frame, as they depend on single frame uniform buffers with self shadow data. const bool bSupportsCachingMeshDrawCommands = SupportsCachingMeshDrawCommands(*StaticMesh, FeatureLevel) && !PrimitiveSceneProxy->CastsVolumetricTranslucentShadow(); const FMaterial* Material = Mesh.MaterialRenderProxy->GetMaterial(FeatureLevel); bool bUseSkyMaterial = Mesh.MaterialRenderProxy->GetMaterial(FeatureLevel)->IsSky(); bool bUseSingleLayerWaterMaterial = Material->GetShadingModels().HasShadingModel(MSM_SingleLayerWater); bool bUseAnisotropy = Material->GetShadingModels().HasAnyShadingModel({MSM_DefaultLit, MSM_ClearCoat}) && Material->MaterialUsesAnisotropy_RenderThread(); FStaticMeshBatchRelevance* StaticMeshRelevance = new(PrimitiveSceneInfo->StaticMeshRelevances) FStaticMeshBatchRelevance( *StaticMesh, ScreenSize, bSupportsCachingMeshDrawCommands, bUseSkyMaterial, bUseSingleLayerWaterMaterial, bUseAnisotropy, FeatureLevel ); } } }; // UnrealEngine\Engine\Source\Runtime\Engine\Private\PrimitiveSceneProxy.cpp bool SupportsCachingMeshDrawCommands(const FMeshBatch& MeshBatch) { return // Cached mesh commands only allow for a single mesh element per batch. (MeshBatch.Elements.Num() == 1) && // Vertex factory needs to support caching. MeshBatch.VertexFactory->GetType()->SupportsCachingMeshDrawCommands(); }
调用堆栈如下:
libUE4.so FDrawCommandRelevancePacket::AddCommandsForMesh(int, FPrimitiveSceneInfo const*, FStaticMeshBatchRelevance const&, FStaticMeshBatch const&, FScene const*, bool, EMeshPass::Type) (Runtime/Renderer/Private/SceneVisibility.cpp:2813)([Inline]TArray<FVisibleMeshDrawCommand, TSizedDefaultAllocator<32>>::AddUninitialized(int) (Runtime\Core\Public\Containers/Array.h:1323:4)) [arm64-v8a] libUE4.so FRelevancePacket::MarkRelevant() (Runtime\Renderer\Private/SceneVisibility.cpp:3591:29) [arm64-v8a] libUE4.so ParallelForImpl::TParallelForData<TFunctionRef<void (int)>>::Process(int, TSharedRef<ParallelForImpl::TParallelForData<TFunctionRef<void (int)>>, (ESPMode)1>&, ENamedThreads::Type, bool) (Runtime/Core/Public/Async/ParallelFor.h:179)([Inline]UE4Function_Private::TFunctionRefBase<UE4Function_Private::FFunctionRefStoragePolicy, void (int)>::operator()(int) const (Runtime\Core\Public\Templates/Function.h:676:11)) [arm64-v8a] libUE4.so void ParallelForImpl::ParallelForInternal<TFunctionRef<void (int)>>(int, TFunctionRef<void (int)>, EParallelForFlags) (Runtime\Core\Public\Async/ParallelFor.h:232:14) [arm64-v8a] libUE4.so ComputeAndMarkRelevanceForViewParallel(FRHICommandListImmediate&, FScene const*, FViewInfo&, FViewCommands&, unsigned char, TArray<unsigned char, TMemStackAllocator<0u>>&, TArray<unsigned char, TMemStackAllocator<0u>>&) (Runtime/Renderer/Private/SceneVisibility.cpp:4063)([Inline]ParallelFor(int, TFunctionRef<void (int)>, bool, bool) (Runtime\Core\Public\Async/ParallelFor.h:332:2)) [arm64-v8a] libUE4.so FSceneRenderer::ComputeViewVisibility(FRHICommandListImmediate&, FExclusiveDepthStencil::Type, TArray<FViewCommands, TInlineAllocator<4u, TSizedDefaultAllocator<32>>>&, FGlobalDynamicIndexBuffer&, FGlobalDynamicVertexBuffer&, FGlobalDynamicReadBuffer&) (Runtime\Renderer\Private/SceneVisibility.cpp:6373:4) [arm64-v8a] libUE4.so FMobileSceneRenderer::InitViews(FRHICommandListImmediate&, FILCUpdatePrimTaskData&) (Runtime\Renderer\Private/MobileShadingRenderer.cpp:1003:2) [arm64-v8a] libUE4.so FMobileSceneRenderer::Render(FRHICommandListImmediate&) (Runtime\Renderer\Private/MobileShadingRenderer.cpp:2617:2) [arm64-v8a] libUE4.so FRendererModule::BeginRenderingViewFamily(FCanvas*, FSceneViewFamily*)::$_32::operator()(FRHICommandListImmediate&) const (Runtime/Renderer/Private/SceneRendering.cpp:5004)([Inline]RenderViewFamily_RenderThread(FRHICommandListImmediate&, FSceneRenderer*) (Runtime\Renderer\Private/SceneRendering.cpp:0:5)) [arm64-v8a] libUE4.so TEnqueueUniqueRenderCommandType<FRendererModule::BeginRenderingViewFamily(FCanvas*, FSceneViewFamily*)::FDrawSceneCommandName, FRendererModule::BeginRenderingViewFamily(FCanvas*, FSceneViewFamily*)::$_32>::DoTask(ENamedThreads::Type, TRefCountPtr<FGraphEvent> const&) (Runtime\RenderCore\Public/RenderingThread.h:194:3) [arm64-v8a] libUE4.so TGraphTask<TEnqueueUniqueRenderCommandType<FRendererModule::BeginRenderingViewFamily(FCanvas*, FSceneViewFamily*)::FDrawSceneCommandName, FRendererModule::BeginRenderingViewFamily(FCanvas*, FSceneViewFamily*)::$_32>>::ExecuteTask(TArray<FBaseGraphTask*, TSizedDefaultAllocator<32>>&, ENamedThreads::Type) (Runtime\Core\Public\Async/TaskGraphInterfaces.h:901:9) [arm64-v8a] libUE4.so FNamedTaskThread::ProcessTasksNamedThread(int, bool) (Runtime/Core/Private/Async/TaskGraph.cpp:742)([Inline]FBaseGraphTask::Execute(TArray<FBaseGraphTask*, TSizedDefaultAllocator<32>>&, ENamedThreads::Type) (Runtime\Core\Public\Async/TaskGraphInterfaces.h:539:3)) [arm64-v8a] libUE4.so FNamedTaskThread::ProcessTasksUntilQuit(int) (Runtime\Core\Private\Async/TaskGraph.cpp:619:4) [arm64-v8a] libUE4.so RenderingThreadMain(FEvent*) (Runtime\RenderCore\Private/RenderingThread.cpp:426:29) [arm64-v8a] libUE4.so FRenderingThread::Run() (Runtime\RenderCore\Private/RenderingThread.cpp:584:4) [arm64-v8a] libUE4.so FRunnableThreadPThread::Run() (Runtime\Core\Private\HAL/PThreadRunnableThread.cpp:25:24) [arm64-v8a] libUE4.so FRunnableThreadPThread::_ThreadProc(void*) (Runtime\Core\Private\HAL/PThreadRunnableThread.h:185:15) [arm64-v8a] /apex/com.android.runtime/lib64/bionic/libc.so (__p_cdname+64) [arm64-v8a] /apex/com.android.runtime/lib64/bionic/libc.so ((null)+0) [arm64-v8a]
这里的 StaticMeshRelevance.bSupportsCachingMeshDrawCommands 来自顶点工厂类型 FVertexFactoryType::bSupportsCachingMeshDrawCommands,在 PrimitiveSceneInfo.cpp 缓存阶段就被拷贝下来(避免 InitViews 里再去解引用 VF)
- VF doesn't need view(不需要 view)= 走
bUseCachedMeshCommand分支:FMeshDrawCommand在场景层(Scene->CachedDrawLists[Pass])一次性构建好,所有 View 共享同一份缓存命令,命令本身与具体 View 无关 → 进入VisibleCachedDrawCommands - VF need view(需要 view)= 走
else分支:命令无法在场景层缓存,必须在每个 View 的可见性阶段动态构建(DynamicBuildRequests→GenerateDynamicMeshDrawCommands),因为它依赖单帧/单个View 的 uniform buffer
判定该 VF 是否支持缓存的两个关键标志(VertexFactory.h):
// UnrealEngine\Engine\Source\Runtime\RenderCore\Public\VertexFactory.h bool SupportsCachingMeshDrawCommands() const { return bSupportsCachingMeshDrawCommands; } bool SupportsPrimitiveIdStream() const { return bSupportsPrimitiveIdStream; }
注意:非 EX 的 IMPLEMENT_VERTEX_FACTORY_TYPE 宏会把这两个标志默认填 false:
// UnrealEngine\Engine\Source\Runtime\RenderCore\Public\VertexFactory.h bSupportsPositionOnly, \ false, \ false, \ IMPLEMENT_VERTEX_FACTORY_VTABLE(FactoryClass) \
IMPLEMENT_VERTEX_FACTORY_TYPE_EX 并显式传 bSupportsCachingMeshDrawCommands=true 的 VF 才属于"不需要 view"Static Relevance — VF doesn't need view(可缓存MeshDrawCommand)
这类 VF 的绘制命令与 View 无关,可全局缓存。
例1:普通静态网格 FLocalVertexFactory
// UnrealEngine\Engine\Source\Runtime\Engine\Private\LocalVertexFactory.cpp IMPLEMENT_VERTEX_FACTORY_TYPE_EX(FLocalVertexFactory,"/Engine/Private/LocalVertexFactory.ush",true,true,true,true,true,true,true);
最后两个参数 (…,true,true) 即 bSupportsCachingMeshDrawCommands=true、bSupportsPrimitiveIdStream=true
一个摆在场景里的 StaticMeshActor(如建筑、石头)走的就是这条路:BasePass / DepthPass 的 FMeshDrawCommand 在它被加入场景时(FPrimitiveSceneInfo::CacheMeshDrawCommands)就构建并缓存进 Scene->CachedDrawLists
之后无论主视图、阴影视图、还是多个分屏视图,relevance 阶段都直接把这份缓存命令塞进 VisibleCachedDrawCommands,配合 Dynamic Instancing 合批,CPU 几乎零构建开销
例 2:实例化静态网格 FInstancedStaticMeshVertexFactory(ISM/HISM)
// UnrealEngine\Engine\Source\Runtime\Engine\Private\InstancedStaticMesh.cpp IMPLEMENT_VERTEX_FACTORY_TYPE_EX(FInstancedStaticMeshVertexFactory,"/Engine/Private/LocalVertexFactory.ush",true,true,true,true,true,true,false);
bSupportsCachingMeshDrawCommands=true(倒数第二个参数)。大片草地/树木的绘制命令同样可缓存复用,与具体 View 无关
Static Relevance — VF need view(不可缓存MeshDrawCommand)
这类 VF 用非 EX 宏注册(caching 默认 false),或本身依赖每帧/每 View 的动态数据,必须按 View 重新构建命令,relevance 时落入 DynamicBuildRequests,每个 View 都要在 GenerateDynamicMeshDrawCommands 里重新构建MeshDrawCommand命令
例 1:骨骼网格 FGPUSkinPassthroughVertexFactory
// UnrealEngine\Engine\Source\Runtime\Engine\Private\GPUSkinVertexFactory.cpp IMPLEMENT_VERTEX_FACTORY_TYPE(FGPUSkinPassthroughVertexFactory, "/Engine/Private/LocalVertexFactory.ush", true, false, true, false, false);
例 2:各类粒子特效 VF
// UnrealEngine\Engine\Source\Runtime\Engine\Private\MeshParticleVertexFactory.cpp IMPLEMENT_VERTEX_FACTORY_TYPE(FMeshParticleVertexFactory,"/Engine/Private/MeshParticleVertexFactory.ush",true,false,true,false,false);
FMeshParticleVertexFactory、FParticleSpriteVertexFactory、FGPUSpriteVertexFactory、FGeometryCacheVertexVertexFactory 等都是非 EX → caching=false
粒子数据每帧 CPU/GPU 模拟生成,绘制命令本帧构建、与 View 绑定
移动端 Landscape和桌面Landscape
移动端Landscape(FLandscapeVertexFactoryMobile)
移动端走的是Static Relevance — VF need view(不可缓存MeshDrawCommand)
1. 它被显式注册为不可缓存
// UnrealEngine\Engine\Source\Runtime\Landscape\Private\LandscapeRenderMobile.cpp IMPLEMENT_VERTEX_FACTORY_TYPE(FLandscapeVertexFactoryMobile, "/Engine/Private/LandscapeVertexFactory.ush", true, true, true, false, false);
注:非 EX 宏 → bSupportsCachingMeshDrawCommands=false
2. 它从 View 获取 相机世界坐标(ViewOrigin)
// UnrealEngine\Engine\Source\Runtime\Landscape\Private\LandscapeRenderMobile.cpp if (TexCoordOffsetParameter.IsBound()) { FVector CameraLocalPos3D = SceneProxy->WorldToLocal.TransformPosition(InView->ViewMatrices.GetViewOrigin()); FVector2D TexCoordOffset( CameraLocalPos3D.X + SceneProxy->SectionBase.X, CameraLocalPos3D.Y + SceneProxy->SectionBase.Y ); ShaderBindings.Add(TexCoordOffsetParameter, TexCoordOffset); }
这段逻辑:
InView->ViewMatrices.GetViewOrigin()—— 取当前 View 的相机世界位置- 用
WorldToLocal把相机位置变换到地形局部空间,得到CameraLocalPos3D - 加上该地形 section 的基准坐标,算出
TexCoordOffset - 把这个值绑进 vertex shader
3. 这个 view 信息拿来干嘛——连续 LOD 地形形变(geomorphing)
地形要做无缝 LOD 过渡:离相机近的网格用高细分、远的用低细分,并且在 LOD 之间平滑插值(顶点高度按"到相机的距离"在相邻 LOD 间渐变),否则会看到 LOD 突变跳变
TexCoordOffset 本质就是"相机相对该 section 的位置",shader 用它在 LOD 高度图之间做距离相关的混合。相机一移动,每个顶点的 LOD 混合系数就变——这正是它必须读 View 的根本原因
draw command 内容逐 View 不同 → 不能在场景层缓存复用 → 只能每个 View 在 GenerateDynamicMeshDrawCommands 里重新生成
桌面Landscape(FLandscapeVertexFactory)
桌面走的是Static Relevance — VF doesn't need view(可缓存MeshDrawCommand)
// UnrealEngine\Engine\Source\Runtime\Landscape\Private\LandscapeRender.cpp IMPLEMENT_VERTEX_FACTORY_TYPE_EX(FLandscapeVertexFactory, "/Engine/Private/LandscapeVertexFactory.ush", true, true, true, false, false, true, false);
桌面端在同样要做 per-view LOD,但它没把相机相关值塞进 per-draw 绑定,而是绑了一个GPU 侧统一更新的 uniform buffer的SRV buffer中
// UnrealEngine\Engine\Source\Runtime\Landscape\Private\LandscapeRender.cpp ShaderBindings.Add(Shader->GetUniformBufferParameter<FLandscapeUniformShaderParameters>(), *BatchElementParams->LandscapeUniformShaderParametersResource); if (SceneProxy && SceneProxy->bRegistered) { ShaderBindings.Add(Shader->GetUniformBufferParameter<FLandscapeSectionLODUniformParameters>(), LandscapeRenderSystems.FindChecked(SceneProxy->LandscapeKey)->UniformBuffer);
桌面版的 GetElementShaderBindings 没有读 InView 去算 per-draw 的值,per-view 的 section LOD 被收敛进 FLandscapeSectionLODUniformParameters 这个 uniform buffer的SRV buffer中(由 LandscapeRenderSystem 统一管理)
// UnrealEngine\Engine\Source\Runtime\Landscape\Private\LandscapeRender.cpp FLandscapeSectionLODUniformParameters Parameters; Parameters.Min = Min; Parameters.Size = Size; Parameters.SectionLOD = SectionLODSRV; Parameters.SectionLODBias = SectionLODBiasSRV; Parameters.SectionTessellationFalloffC = SectionTessellationFalloffCSRV; Parameters.SectionTessellationFalloffK = SectionTessellationFalloffKSRV; if (UniformBuffer.IsValid()) { UniformBuffer.UpdateUniformBufferImmediate(Parameters); // 每帧只更新 buffer 内容 }
关键在于:顶点着色器通过 SRV 自己去 buffer 里按 section 索引取 LOD(即 manual vertex fetch)。于是:
- draw command 里绑的是"指向这些 SRV 的 uniform buffer",结构固定
- per-view 的 LOD 变化体现在 buffer 内容上(
UpdateUniformBufferImmediate每帧/每 view 刷),命令本身不变 - 所以同一份缓存命令能跨 View 复用 →
caching=true
per-view 计算是在 GPU 取数侧完成的(ComputeSectionPerViewParameters → 写 SRV),没有把相机相关的标量塞进 per-draw 绑定
于是 draw command 本身与 View 无关 → 可缓存复用,只是绑定的 uniform buffer的SRV buffer 每帧/每 view 更新
移动端 GPU 不支持桌面Landscape方案的原因
移动端 GPU 不支持桌面方案所依赖的核心能力 —— Manual Vertex Fetch(在顶点着色器里读 SRV buffer)
强行照搬不仅做不到,即使用别的方式绕过,在移动端也是 GPU 负收益
// UnrealEngine\Engine\Source\Runtime\RHI\Public\RHI.h /** Whether Manual Vertex Fetch is supported for the specified shader platform. Shader Platform must not use the mobile renderer, and for Metal, the shader language must be at least 2. */ inline bool RHISupportsManualVertexFetch(const FStaticShaderPlatform InShaderPlatform) { bool bIsMetalMobilePlatform = IsMetalPlatform(InShaderPlatform) && !IsPCPlatform(InShaderPlatform); bool bIsUnsupportedGL = IsOpenGLPlatform(InShaderPlatform) && !IsSwitchPlatform(InShaderPlatform); return !bIsUnsupportedGL && !IsMobilePlatform(InShaderPlatform) && !bIsMetalMobilePlatform; }
桌面方案要求 VS 里 SRV fetch / VTF,移动 GPU 要么不支持、要么极慢(顶点阶段随机访存延迟高、带宽紧张)
在 GPU 算力、带宽、发热,而非 draw command 构建。用"顶点流 + 一个 CPU 算的 uniform"对移动 GPU 是比较好的选择
移动端只能改用的替代方案 → 必然引入 per-view 依赖
既然 VS 里读不了 SRV,移动端 LOD 信息只能采用如下做法:
1. 把 LOD 高度烘焙进顶点流(逐顶点属性,不需要在 VS 里 fetch buffer)
// UnrealEngine\Engine\Source\Runtime\Landscape\Private\LandscapeRenderMobile.cpp if (MobileData.LODHeightsComponent.Num()) { const int32 BaseAttribute = 1; for(int32 Index = 0;Index < MobileData.LODHeightsComponent.Num();Index++) { Elements.Add(AccessStreamComponent(MobileData.LODHeightsComponent[Index], BaseAttribute + Index)); } }
2. per-draw 在 CPU 用相机位置算一个 TexCoordOffset 的ub传进去做 LOD morph
VF 通常会从 view 获取这些信息
| View 信息 | 典型用途 | 举例 VF |
|---|---|---|
相机位置 ViewMatrices.GetViewOrigin() |
相机相对 LOD / 地形 geomorph / 朝向相机 | 移动端 FLandscapeVertexFactoryMobile |
| View Uniform Buffer(视图/投影矩阵等) | camera-relative 顶点变换、屏幕空间尺寸 | 粒子/sprite 类 VF |
| 每 View 计算的 LOD / 实例数据 | 连续 LOD 混合、per-view 实例裁剪 | 各类带动态 LOD 的 VF |
两种静态对比方案对比小结
| 维度 | VF doesn't need view | VF need view |
|---|---|---|
| 命令构建时机 | 加入场景时一次性缓存(CacheMeshDrawCommands) |
每 View、每帧构建(DynamicBuildRequests) |
| 存储位置 | Scene->CachedDrawLists / StateBuckets |
临时 FDynamicMeshDrawCommandStorage |
| 多视图复用 | 多 View 共享同一命令 | 每个 View 各自构建 |
| 典型对象 | StaticMesh、ISM、HISM(植被) | SkeletalMesh、Particle |
| CPU 开销 | 极低(命令复用 + 合批) | 较高(每个 View 都需要重建) |
浙公网安备 33010602011771号