
从零向 UE 5.3 渲染管线插入一个 Mesh Pass前言这次实验会在 Base Pass 之后、光照之前把选中的 Mesh 再画一遍。预期效果很简单在UStaticMeshComponent的 Rendering 高级选项中勾选bRenderCustomMeshPass运行时模型会呈现红色。上一篇的 Screen Pass 处理的是二维纹理Mesh Pass 要走的路长得多先确定哪些 Primitive 与当前 View、当前 Pass 相关取得它们的FMeshBatch选择 Shader 和渲染状态最后才轮到 RDG 安排实际绘制。总览从 UPrimitiveComponent 到 FMeshDrawCommand先约定本文的术语Primitive指渲染场景中的基本对象游戏侧对应UPrimitiveComponentProxy专指FPrimitiveSceneProxyMesh Batch指FMeshBatch描述的一批几何Draw Command指FMeshDrawCommand。Actor 可以挂载UStaticMeshComponent、USkeletalMeshComponent等组件它们都继承自UPrimitiveComponent。Game Thread 管理组件的变换、材质和渲染选项但有组件不等于一定参与渲染组件得先满足加入渲染场景的条件CreateSceneProxy()也可能返回空指针。FScene::BatchAddPrimitives在 Game Thread 调用CreateSceneProxy()创建 Proxy并创建与之关联的FPrimitiveSceneInfo。Proxy 保存渲染所需的数据提供 Mesh 和 View RelevanceFPrimitiveSceneInfo是 Renderer 内部的场景记录保存StaticMeshes、缓存信息、场景索引和八叉树 ID 等。两者随后交给 Render Thread 维护的FScene。创建完成之后Game Thread 不会再去改已注册的 Proxy组件的变化通过渲染命令传递或者靠重建 Proxy 生效。一帧可能包含多个 View例如主视口、分屏视口或场景捕获。FSceneRenderer根据FSceneViewFamily组织渲染PC 延迟路径用的是FDeferredShadingSceneRenderer。渲染侧针对每个 View 做可见性处理综合视锥、距离、遮挡和 LOD 等条件决定需要哪些 PrimitiveProxy 的GetViewRelevance()返回FPrimitiveViewRelevance说明这个 Primitive 对当前 View 有哪些渲染用途。可见性处理由 Render Thread 发起也可能被拆成并行任务分发出去——第五步会回到这里。几何则以FMeshBatch的形式提供给网格绘制流程。一个FMeshBatch关联材质和 Vertex Factory包含一个或多个FMeshBatchElementVertex Factory 决定 Shader 如何读取顶点数据。静态提交路径通常在 Proxy 加入场景时通过DrawStaticElements保存FMeshBatch动态提交路径则按需通过GetDynamicMeshElements收集它。这里的「静态动态」说的是 Mesh Batch 的提交路径既不等同于组件类名也不等同于物体是否移动。Mesh Pass 规定这些 Mesh 怎样画。例如 Depth Pass 主要处理深度Base Pass 处理基础材质绘制。每个 Pass 的FMeshPassProcessor接收FMeshBatch选择 Shader 和渲染状态再构建FMeshDrawCommand有的 Pass 可以复用缓存命令有的则按帧构建。FMeshDrawCommand描述 Shader 绑定、管线状态和绘制参数本身不发起 GPU 绘制渲染器还要在对应阶段绑定目标并把命令记录到FRHICommandList。可参考下面的流程图理解上述过程本次实验要做的事情就是让UPrimitiveComponent声明自己要参与自定义 Mesh Pass然后在可见性阶段把它的FMeshBatch送给新的FMeshPassProcessor再把得到的FMeshDrawCommand安排到一帧中的合适位置绘制。第一步给 UPrimitiveComponent 添加开关在Engine/Source/Runtime/Engine/Classes/Components/PrimitiveComponent.h的 Rendering 属性附近新增UPROPERTY(EditAnywhere,AdvancedDisplay,BlueprintReadOnly,CategoryRendering,meta(ToolTipWhen true, will rendering in custom Mesh Pass))uint8 bRenderCustomMeshPass:1;第二步将开关复制到 FPrimitiveSceneProxy在Engine/Source/Runtime/Engine/Public/PrimitiveSceneProxy.h增加inlineboolShouldRenderInCustomMeshPass()const{returnbRenderCustomMeshPass;}// private:uint8 bRenderCustomMeshPass:1;然后在Engine/Source/Runtime/Engine/Private/PrimitiveSceneProxy.cpp的构造函数初始化列表中从组件复制这个值,bRenderCustomMeshPass(InComponent-bRenderCustomMeshPass)第三步在 FPrimitiveViewRelevance 中声明 Pass 相关性Proxy 上有了开关还不够。可见性处理还得知道这个 Primitive 对当前 View是不是与自定义 Pass 相关。为此在Engine/Source/Runtime/Engine/Public/PrimitiveViewRelevance.h添加/** The primitive should render to the custom Mesh Pass. */uint32 bRenderCustomMeshPass:1;再分别修改Engine/Source/Runtime/Engine/Private/StaticMeshRender.cpp的FStaticMeshSceneProxy::GetViewRelevance以及Engine/Source/Runtime/Engine/Private/SkeletalMesh.cpp的FSkeletalMeshSceneProxy::GetViewRelevanceResult.bRenderCustomMeshPassShouldRenderInCustomMeshPass();FPrimitiveViewRelevance不是组件上的一份永久「待绘制名单」而是针对当前 View 的一组判断这个 Primitive 是否有绘制相关性、走静态还是动态路径、是否包含半透明内容以及是否与特定 Pass 相关。新增的 bitbRenderCustomMeshPass把组件开关带进可见性流程它只表示「有资格进入自定义 Pass」不会跳过视锥、距离或遮挡判断也不保证最终一定产生 Draw Call。第四步Mesh Pass 枚举在Engine/Source/Runtime/Renderer/Public/MeshPassProcessor.h的EMeshPass::Type中加入MeshDecal,MustardCustomMeshPass,同一文件的GetMeshPassName补上对应名称并把断言里的 Pass 总数加一caseEMeshPass::MustardCustomMeshPass:returnTEXT(MustardCustomMeshPass);那个断言是static_assert(EMeshPass::Num 30)WITH_EDITOR下写成30 4这里两处都要改成31和31 4。另外Engine/Source/Runtime/Engine/Public/PSOPrecache.h里的MaxPSOCollectorCount也要由34改为35。PSO Collector 的 JumpTable 是以 Pass 序号为下标的定长数组FRegisterPSOCollectorCreateFunction会把(uint32)EMeshPass::X当作 Index 写进去Pass 多了一个表容量就得跟着涨一格。第五步收集 Pass 的 MeshBatch这一步有两个很容易混在一起的问题FMeshBatch是不是每帧都要重新收集FMeshDrawCommand是不是每帧都要重新构建。两者互相独立得分开看。在Engine/Source/Runtime/Renderer/Private/SceneVisibility.cppFRelevancePacket::ComputeRelevance会对候选 Primitive 调用 Proxy 的GetViewRelevance()拿到FPrimitiveViewRelevance。其中bStaticRelevance和bDynamicRelevance指示该 Proxy 在当前 View 走哪种 Mesh 提交路径bRenderCustomMeshPass则是我们新增的 Pass 相关性。一个 Primitive 可以同时具有静态与动态相关性所以别把ComputeRelevance当成只处理静态 Mesh 的函数。静态路径不会在这里生成FMeshBatch。Proxy 加入场景时DrawStaticElements已经把它提交并保存在FPrimitiveSceneInfo::StaticMeshes里了ComputeRelevance只是遍历这些FStaticMeshBatch结合当前 View 的 LOD 和其他条件挑出需要绘制的 Batch。为我们的 Pass 增加入口如下// ComputeRelevanceif(ViewRelevance.bRenderCustomMeshPass){DrawCommandPacket.AddCommandsForMesh(PrimitiveIndex,PrimitiveSceneInfo,StaticMeshRelevance,StaticMesh,Scene,bCanCache,EMeshPass::MustardCustomMeshPass);}这里的StaticMesh是一个FStaticMeshBatchStaticMeshRelevance是与之对应的FStaticMeshBatchRelevance名字里的 Static 不特指UStaticMeshComponent更不代表 Draw Command 一定已经缓存。AddCommandsForMesh有两种结果满足缓存条件时把已经存在的FMeshDrawCommand选进当前 View 的可见命令列表不满足时只把这个 Batch 放进DynamicBuildRequests[Pass]留到本帧的 Pass 准备阶段再去构建命令。本文的 Pass 在第七步只注册了EMeshPassFlags::MainView没有EMeshPassFlags::CachedMeshCommands。所以哪怕bCanCache为真上面的调用也会走「登记构建请求」这一支。静态FMeshBatch可以复用不等于本 Pass 的FMeshDrawCommand可以复用。动态路径分两段。ComputeRelevance发现bDynamicRelevance后先把 Primitive 记进动态收集列表稍后GatherDynamicMeshElementsForPrimitive调用 Proxy 的GetDynamicMeshElements()为当前 View 收集本帧的FMeshBatch。有了这些 BatchFVisibilityTaskData::SetupMeshPasses才会逐个调用ComputeDynamicMeshRelevance依据先前的FPrimitiveViewRelevance设置FMeshPassMask。这里加的是// ComputeDynamicMeshRelevanceif(ViewRelevance.bRenderCustomMeshPass){PassMask.Set(EMeshPass::MustardCustomMeshPass);View.NumVisibleDynamicMeshElements[EMeshPass::MustardCustomMeshPass]NumElements;}PassMask.Set标记的是当前这一个动态FMeshBatch应该进入自定义 PassNumVisibleDynamicMeshElements的累加则供后面 Pass 准备阶段分配容量。注意这里没有调用AddCommandsForMesh动态 Batch 已经在View.DynamicMeshElements里了后续只要能按 Pass Mask 把它们挑出来直接交给本 Pass 的FMeshPassProcessor::AddMeshBatch就行。两条路径在FSceneRenderer::SetupMeshPass之后汇合Pass 准备阶段既读静态路径的构建请求也读动态路径的 Batch 和 Pass Mask然后调用FCustomMeshPassMeshProcessor::AddMeshBatch。到这一步才真正生成FMeshDrawCommand。拿本次实验里的不透明UStaticMeshComponent来说正常 View 下它一般是bStaticRelevance走左边的静态FMeshBatch路径但由于本 Pass 没启用命令缓存FMeshDrawCommand仍然在每帧的AddMeshBatch里生成。调试视图之类的特殊条件可能让同一个组件改走动态提交路径。决定走哪条路的是 Proxy 针对当前 View 返回的 Relevance既不是组件的 Mobility也不是bRenderCustomMeshPass这个开关。第六步Shader新建Engine/Shaders/Private/Mustard/CustomMeshPass.usf。与上一篇只写 Pixel Shader 不同这次要重新提交网格顶点Vertex Shader 必不可少。文件的核心部分如下#include ../Common.ush #include /Engine/Generated/Material.ush #include /Engine/Generated/VertexFactory.ush #if VERTEXSHADER void MainVS( FVertexFactoryInput Input, out float4 OutPosition : SV_POSITION #if INSTANCED_STEREO , out uint ViewportIndex : SV_ViewPortArrayIndex #endif ) { #if INSTANCED_STEREO ViewportIndex GetEyeIndexFromVF(Input); #endif ResolvedView ResolveViewFromVF(Input); FVertexFactoryIntermediates VFIntermediate GetVertexFactoryIntermediates(Input); float4 WorldPosition VertexFactoryGetWorldPosition(Input, VFIntermediate); float4 RasterizedWorldPosition VertexFactoryGetRasterizedWorldPosition(Input, VFIntermediate, WorldPosition); OutPosition mul(RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip); } #endif #if PIXELSHADER void MainPS( in INPUT_POSITION_QUALIFIERS float4 SvPosition : SV_Position OPTIONAL_IsFrontFace OPTIONAL_OutDepthConservative , out float4 OutColor : SV_Target0) { OutColor float4(1.0, 0.0, 0.0, 1.0); } #endifFVertexFactoryInput不是某个固定的网格格式它由当前 Vertex Factory 的生成代码提供。VS 先通过 Vertex Factory 得到世界位置再变换到当前 View 的裁剪空间RasterizedWorldPosition用的是 Vertex Factory 为真实栅格化准备的位置。INSTANCED_STEREO分支负责给立体视图提供 Viewport Index。PS 暂时不读材质颜色只输出一个常量红色。SV_Target0表示第一个颜色目标后文会把它绑到 SceneColor。和全屏后处理不同这个 Pass 只会改那些被重绘 Mesh 覆盖、并且通过深度测试的像素。接着在Engine/Source/Runtime/Renderer/Private/Mustard/CustomMeshPassRendering.h声明两个 Shader 类型classFCustomMeshPassVS:publicFMeshMaterialShader{public:DECLARE_SHADER_TYPE(FCustomMeshPassVS,MeshMaterial);staticboolShouldCompilePermutation(constFMeshMaterialShaderPermutationParametersParameters){returnIsFeatureLevelSupported(Parameters.Platform,ERHIFeatureLevel::SM5)FMeshMaterialShader::ShouldCompilePermutation(Parameters);}FCustomMeshPassVS()default;FCustomMeshPassVS(constShaderMetaType::CompiledShaderInitializerTypeInitializer):FMeshMaterialShader(Initializer){}};classFCustomMeshPassPS:publicFMeshMaterialShader{public:DECLARE_SHADER_TYPE(FCustomMeshPassPS,MeshMaterial);staticboolShouldCompilePermutation(constFMeshMaterialShaderPermutationParametersParameters){returnFCustomMeshPassVS::ShouldCompilePermutation(Parameters);}FCustomMeshPassPS()default;FCustomMeshPassPS(constShaderMetaType::CompiledShaderInitializerTypeInitializer):FMeshMaterialShader(Initializer){}};并在同名.cpp里关联 Shader 路径与入口函数IMPLEMENT_MATERIAL_SHADER_TYPE(,FCustomMeshPassVS,TEXT(/Engine/Private/Mustard/CustomMeshPass.usf),TEXT(MainVS),SF_Vertex);IMPLEMENT_MATERIAL_SHADER_TYPE(,FCustomMeshPassPS,TEXT(/Engine/Private/Mustard/CustomMeshPass.usf),TEXT(MainPS),SF_Pixel);FGlobalShader适合上一篇那种不依附 Mesh 材质的全屏工作FMeshMaterialShader则进入「材质 × Vertex Factory」的 Shader 排列体系。即使 PS 目前只写常量色VS 仍然需要正确的顶点输入FCustomMeshPassMeshProcessor也得从材质和 Vertex Factory 里找到可用的 Shader。ShouldCompilePermutation决定编译哪些变体不负责每帧筛选可见 Primitive。第七步FMeshPassProcessor仍在CustomMeshPassRendering.h中声明FCustomMeshPassMeshProcessor。它是本 Pass 的命令生成器接收FMeshBatch选择 Shader 和渲染状态再调用基类逻辑构建FMeshDrawCommand。classFCustomMeshPassMeshProcessor:publicFMeshPassProcessor{public:FCustomMeshPassMeshProcessor(constFScene*Scene,ERHIFeatureLevel::Type InFeatureLevel,constFSceneView*InViewIfDynamicMeshCommand,FMeshPassDrawListContext*InDrawListContext);virtualvoidAddMeshBatch(constFMeshBatchRESTRICT MeshBatch,uint64 BatchElementMask,constFPrimitiveSceneProxy*RESTRICT PrimitiveSceneProxy,int32 StaticMeshId-1)overridefinal;protected:FMeshPassProcessorRenderState PassDrawRenderState;};FMeshPassDrawListContext负责接收构建好的命令InViewIfDynamicMeshCommand只在需要按 View 构建命令时才有值不保证始终存在。关键实现位于同名.cpp。先在构造函数里指定 Pass 身份和固定状态FCustomMeshPassMeshProcessor::FCustomMeshPassMeshProcessor(constFScene*Scene,ERHIFeatureLevel::Type InFeatureLevel,constFSceneView*InViewIfDynamicMeshCommand,FMeshPassDrawListContext*InDrawListContext):FMeshPassProcessor(EMeshPass::MustardCustomMeshPass,Scene,InFeatureLevel,InViewIfDynamicMeshCommand,InDrawListContext){PassDrawRenderState.SetBlendState(TStaticBlendState::GetRHI());PassDrawRenderState.SetDepthStencilState(TStaticDepthStencilStatefalse,CF_Equal::GetRHI());}false表示不写深度CF_Equal表示只保留与现有 SceneDepth 相等的片元。接着在AddMeshBatch里从FMeshBatch提供的材质代理和 Vertex Factory 选择 ShaderconstFMaterialRenderProxy*MaterialRenderProxyMeshBatch.MaterialRenderProxy;constFMaterialMaterialMaterialRenderProxy-GetMaterialWithFallback(FeatureLevel,MaterialRenderProxy);constFVertexFactory*VertexFactoryMeshBatch.VertexFactory;TMeshProcessorShadersFCustomMeshPassVS,FCustomMeshPassPSPassShaders;PassShaders.VertexShaderMaterial.GetShaderFCustomMeshPassVS(VertexFactory-GetType());PassShaders.PixelShaderMaterial.GetShaderFCustomMeshPassPS(VertexFactory-GetType());GetMaterialWithFallback取的是当前 Feature Level 可用的材质及其代理必要时会退到 fallback 材质VertexFactory-GetType()决定去查哪一组网格输入对应的 Shader。接下来算 Fill Mode、Cull Mode 和 Sort Key初始化FMeshMaterialShaderElementData最后进入 UE 的通用命令构建入口constFMeshDrawingPolicyOverrideSettings OverrideSettingsComputeMeshOverrideSettings(MeshBatch);constERasterizerFillMode MeshFillModeComputeMeshFillMode(Material,OverrideSettings);constERasterizerCullMode MeshCullModeComputeMeshCullMode(Material,OverrideSettings);FMeshMaterialShaderElementData ShaderElementData;ShaderElementData.InitializeMeshMaterialData(ViewIfDynamicMeshCommand,PrimitiveSceneProxy,MeshBatch,StaticMeshId,false);FMeshDrawCommandSortKey SortKeyCalculateMeshStaticSortKey(PassShaders.VertexShader,PassShaders.PixelShader);BuildMeshDrawCommands(MeshBatch,BatchElementMask,PrimitiveSceneProxy,*MaterialRenderProxy,Material,PassDrawRenderState,PassShaders,MeshFillMode,MeshCullMode,SortKey,EMeshPassFeatures::Default,ShaderElementData);BatchElementMask指定当前FMeshBatch里需要处理哪些FMeshBatchElementFill Mode、Cull Mode 保留材质和 Mesh 的栅格化约束ShaderElementData提供 Shader 绑定所需的 per-Mesh 数据BuildMeshDrawCommands把 Shader、资源绑定、管线状态和绘制参数组织成FMeshDrawCommand。它还不会调用图形 API排序、实例化和实际提交都在后面的流程里。当前的AddMeshBatch没有显式过滤半透明材质所以本文最后用不透明 Mesh 做测试。最后注册工厂让 UE 知道这个EMeshPass该创建哪个 ProcessorstaticFMeshPassProcessor*CreateCustomMeshPassMeshProcessor(ERHIFeatureLevel::Type InFeatureLevel,constFScene*Scene,constFSceneView*InViewIfDynamicMeshCommand,FMeshPassDrawListContext*InDrawListContext){constERHIFeatureLevel::Type FeatureLevelInViewIfDynamicMeshCommand?InViewIfDynamicMeshCommand-GetFeatureLevel():InFeatureLevel;returnnewFCustomMeshPassMeshProcessor(Scene,FeatureLevel,InViewIfDynamicMeshCommand,InDrawListContext);}FRegisterPassProcessorCreateFunctionRegisterCustomMeshPass(CreateCustomMeshPassMeshProcessor,EShadingPath::Deferred,EMeshPass::MustardCustomMeshPass,EMeshPassFlags::MainView);这相当于把「Deferred 路径 MustardCustomMeshPass」映射到处理器工厂。MainView表示它供主视图的 Mesh Pass 使用——对照FSceneRenderer::SetupMeshPass就能看到它正是按这个 flag 决定要不要给某个 Pass 创建 Processor 并启动准备流程的。当前没有注册EMeshPassFlags::CachedMeshCommands所以即使静态路径保存了FMeshBatch也不代表本 Pass 的FMeshDrawCommand会被缓存。可见性处理完成后FSceneRenderer::SetupMeshPass创建 ProcessorFParallelMeshDrawCommandPass::DispatchPassSetup启动 Pass 准备再由GenerateDynamicMeshDrawCommands对静态路径的构建请求和动态路径的 Mesh 调用AddMeshBatch。到这里UE 已经能为相关 Mesh 准备好本 Pass 的FMeshDrawCommand。第八步把 Pass 接进 RDG在CustomMeshPassRendering.cpp增加 Raster Pass 的参数结构BEGIN_SHADER_PARAMETER_STRUCT(FCustomMeshPassParameters,)SHADER_PARAMETER_STRUCT_INCLUDE(FViewShaderParameters,View)SHADER_PARAMETER_STRUCT_INCLUDE(FInstanceCullingDrawParams,InstanceCullingDrawParams)RENDER_TARGET_BINDING_SLOTS()END_SHADER_PARAMETER_STRUCT()View提供当前视图的 Shader 参数InstanceCullingDrawParams给 Mesh Draw Command Pass 的实例剔除和调度使用RENDER_TARGET_BINDING_SLOTS则让 RDG 知道颜色与深度附件。上一篇为了避免同一张纹理既当 SRV 又当 RTV额外插了一次拷贝这次不需要直接把模型画进当前 SceneColor。在Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.h给FDeferredShadingSceneRenderer声明成员函数voidRenderCustomMeshPass(FRDGBuilderGraphBuilder,FRDGTextureRef SceneColorTexture,FRDGTextureRef SceneDepthTexture);定义仍写在CustomMeshPassRendering.cpp核心代码如下voidFDeferredShadingSceneRenderer::RenderCustomMeshPass(FRDGBuilderGraphBuilder,FRDGTextureRef SceneColorTexture,FRDGTextureRef SceneDepthTexture){for(int32 ViewIndex0;ViewIndexViews.Num();ViewIndex){FViewInfoViewViews[ViewIndex];FParallelMeshDrawCommandPassParallelMeshPassView.ParallelMeshDrawCommandPasses[EMeshPass::MustardCustomMeshPass];if(!View.ShouldRenderView()||!ParallelMeshPass.HasAnyDraw()){continue;}FCustomMeshPassParameters*PassParametersGraphBuilder.AllocParametersFCustomMeshPassParameters();PassParameters-ViewView.GetShaderParameters();PassParameters-RenderTargets[0]FRenderTargetBinding(SceneColorTexture,ERenderTargetLoadAction::ELoad);PassParameters-RenderTargets.DepthStencilFDepthStencilBinding(SceneDepthTexture,ERenderTargetLoadAction::ELoad,ERenderTargetLoadAction::ELoad,FExclusiveDepthStencil::DepthRead_StencilRead);ParallelMeshPass.BuildRenderingCommands(GraphBuilder,Scene-GPUScene,PassParameters-InstanceCullingDrawParams);GraphBuilder.AddPass(RDG_EVENT_NAME(CustomMeshPass),PassParameters,ERDGPassFlags::Raster,[this,View,ParallelMeshPass,PassParameters](FRHICommandListRHICmdList){SetStereoViewport(RHICmdList,View);ParallelMeshPass.DispatchDraw(nullptr,RHICmdList,PassParameters-InstanceCullingDrawParams);});}}这里有三个关键点。FParallelMeshDrawCommandPass是按 View 保存当前 Pass 的命令的所以要逐 View 检查HasAnyDraw()。颜色附件用ELoad保留已有的 SceneColor深度附件以DepthRead_StencilRead绑定正好对应前面 Processor 里「不写深度、只做 Equal 测试」的设置。BuildRenderingCommands准备 GPU Scene 和 Instance Culling 参数DispatchDraw则在 RDG Raster Pass 的执行回调里向FRHICommandList记录绘制。RDG 负责 Pass 依赖、资源生命周期和状态转换GraphBuilder.AddPass只是把工作加进帧图不表示 GPU 此时已经画完了。最后到Engine/Source/Runtime/Renderer/Private/DeferredShadingRenderer.cpp的FDeferredShadingSceneRenderer::Render。调用位于RenderBasePass及相关深度处理之后、AddSetupExposureIlluminancePass之前RenderCustomMeshPass(GraphBuilder,SceneTextures.Color.Target,SceneTextures.Depth.Target);// 随后仍由 UE 处理曝光准备、光照和后面的阶段。FRDGTextureRef ExposureIlluminanceSetupAddSetupExposureIlluminancePass(GraphBuilder,Views,SceneTextures);放在这里有两个前提Processor 用的是CF_Equal调用时必须有可比较的 SceneDepthBase Pass 跑完之后 SceneColor 也已经就绪这次重绘才有地方可写。不过还有一个坑。Deferred Base Pass 把材质信息写进了 GBuffer后续光照读的是 GBuffer我们的 Pass 只覆盖 SceneColor并没有同步改 GBuffer。所以屏幕上那块红色仍会受到后续光照、曝光与 Tonemapping 的影响最终像素不一定等于 PS 写出的(1.0, 0.0, 0.0)。这次实验只是演示自定义 Mesh Pass 的接入流程如果需要稳定的输出颜色插入时机和合成方式都得另做设计。参考剖析虚幻渲染体系03- 渲染机制 - 0向往0 - 博客园Mesh Drawing PipelineRender Dependency GraphFrameGraph|设计基于DX12实现 - 知乎