可可西

UE4两种静态绘制路径详解

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

image

注:如果VF与view相关,就需要为每帧为每个view重新创建MeshDrawCommand

 

在 UE 的静态物体可见性计算(FRelevancePacketAddCommandsForMesh,位于 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 的可见性阶段动态构建(DynamicBuildRequestsGenerateDynamicMeshDrawCommands),因为它依赖单帧/单个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=truebSupportsPrimitiveIdStream=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);

FMeshParticleVertexFactoryFParticleSpriteVertexFactoryFGPUSpriteVertexFactoryFGeometryCacheVertexVertexFactory 等都是非 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 viewVF need view
命令构建时机 加入场景时一次性缓存(CacheMeshDrawCommands 每 View、每帧构建(DynamicBuildRequests
存储位置 Scene->CachedDrawLists / StateBuckets 临时 FDynamicMeshDrawCommandStorage
多视图复用 多 View 共享同一命令 每个 View 各自构建
典型对象 StaticMesh、ISM、HISM(植被) SkeletalMesh、Particle
CPU 开销 极低(命令复用 + 合批) 较高(每个 View 都需要重建)

posted on 2026-06-25 20:27  可可西  阅读(8)  评论(0)    收藏  举报

导航