虚幻引擎如何添加一个MeshPass

这一章中,我希望通过添加一个MeshPass的形式把标记了特定Tag的物体渲染成一张特定的RT作为蒙版。虽然CustomDepth之类的功能也能方便地做到这一点,但这里主要是想通过这个来学习如何添加一个MeshPass
这里参考了EMeshPass::EditorLevelInstance的实现。


首先,我们先注册我们的MeshPass以及MeshProcessor

在MeshPass的相关的内容里把我们的MeshPass加上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
namespace EMeshPass  
{
enum Type : uint8
{
...

#if WITH_EDITOR
HitProxy,
HitProxyOpaqueOnly,
EditorLevelInstance,
EditorSelection,
//Texwood Begin Add, For Add A MeshPass
SpecialTagElement,
//Texwood End Add, For Add A MeshPass
#endif
Num,
NumBits = 6,
};}
...
inline const TCHAR* GetMeshPassName(EMeshPass::Type MeshPass)
{
switch (MeshPass)
{ ...
#if WITH_EDITOR
case EMeshPass::HitProxy: return TEXT("HitProxy");
case EMeshPass::HitProxyOpaqueOnly: return TEXT("HitProxyOpaqueOnly");
case EMeshPass::EditorLevelInstance: return TEXT("EditorLevelInstance");
case EMeshPass::EditorSelection: return TEXT("EditorSelection");
//Texwood Begin Add, For Add A MeshPass
case EMeshPass::SpecialTagElement: return TEXT("SpecialTagElement");
//Texwood End Add, For Add A MeshPass
#endif
}

#if WITH_EDITOR
//Texwood Begin Add, For Add A MeshPass
//static_assert(EMeshPass::Num == 29 + 4, "Need to update switch(MeshPass) after changing EMeshPass"); // GUID to prevent incorrect auto-resolves, please change when changing the expression: {A6E82589-44B3-4DAD-AC57-8AF6BD50DF43}
//这里的数量记得要一起改
static_assert(EMeshPass::Num == 30 + 4, "Need to update switch(MeshPass) after changing EMeshPass"); // GUID to prevent incorrect auto-resolves, please change when changing the expression: {A6E82589-44B3-4DAD-AC57-8AF6BD50DF43}
//Texwood End Add, For Add A MeshPass
#else
static_assert(EMeshPass::Num == 29, "Need to update switch(MeshPass) after changing EMeshPass"); // GUID to prevent incorrect auto-resolves, please change when changing the expression: {A6E82589-44B3-4DAD-AC57-8AF6BD50DF43}
#endif

checkf(0, TEXT("Missing case for EMeshPass %u"), (uint32)MeshPass);
return nullptr;
}

class ENGINE_API FPSOCollectorCreateManager
{
public:
//Texwood0935 Begin Add ,For Add A Mesh Pass
//constexpr static uint32 MaxPSOCollectorCount = 33;
//这里也别忘了+1
constexpr static uint32 MaxPSOCollectorCount = 34;
//Texwood0935 Begin Add ,For Add A Mesh Pass

...
};

然后仿照FEditorLevelInstanceMeshProcessor写一个MeshProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Texwood0935 Begin Add, For Add A MeshPass  
class FSpecialTagElementMeshProcessor : public FSceneRenderingAllocatorObject<FSpecialTagElementMeshProcessor>, public FMeshPassProcessor
{
public:

FSpecialTagElementMeshProcessor(const FScene* Scene, const FSceneView* InViewIfDynamicMeshCommand, FMeshPassDrawListContext* InDrawListContext);

virtual void AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId = -1) override final;

FMeshPassProcessorRenderState PassDrawRenderState;

private:
bool TryAddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId, const FMaterialRenderProxy* MaterialRenderProxy, const FMaterial* Material);

bool Process(
const FMeshBatch& MeshBatch,
uint64 BatchElementMask,
int32 StaticMeshId,
const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
const FMaterial& RESTRICT MaterialResource,
ERasterizerFillMode MeshFillMode,
ERasterizerCullMode MeshCullMode);

int32 GetStencilValue(const FSceneView* View, const FPrimitiveSceneProxy* PrimitiveSceneProxy);
};
//Texwood0935 End Add, For Add A MeshPass

大部分内容直接copyFEditorLevelInstanceMeshProcessor的实现就行了,注意我们不需要判断物体是否是LevelInstance以及我们的stencil值不同(这里我们先暂时都返回1)。
流程重点就是我们通过MeshPassProcessor的AddMeshBatch接口传来的MeshBatch以及其他信息,构建一个我们想要的MeshDrawCommands。
(直接用FEditorLevelInstanceMeshProcessor实现的好处就是shader啥的都不用我们自己写了,简而言之就是偷个懒×)
参考FEditorLevelInstanceMeshProcessor的实现时,我们还会发现一行很重要的代码:

1
FRegisterPassProcessorCreateFunction RegisterEditorLevelInstancePass(&CreateEditorLevelInstancePassProcessor, EShadingPath::Deferred, EMeshPass::EditorLevelInstance, EMeshPassFlags::MainView);

这个代码声明了一个全局变量,这个全局变量被声明的时候,通过构造函数把MeshPass和相应的MeshPassProcessor绑定到一起(一点吐槽:看UE代码的时候发现,UE还挺喜欢像这样在构造函数里面搞点东西)


然后,让标记了特定Tag的物体渲染成一张特定的蒙版

在不修改任何东西的前提下,到我们的MeshPassProcessor的AddMeshBatch是无法知道对应物体的Tag的,因此我们首先把Tag的值传进来,我们可以通过PrimitiveSceneProxy来完成这个目的。
在PrimitiveSceneProxy增加一个变量,并且在构造PrimitiveSceneProxy的时候把Component的Tags赋值给那个变量。
然后在FSpecialTagElementMeshProcessor::GetStencilValue中把有特殊Tag的物体的stencil值设置为1。

做完上述工作之后,让标记了特定Tag的物体渲染成一张特定的蒙版的任务还差一步:把Pass加到我们的渲染流程里。
我们把这个Pass加到后处理Pass里。同样仿照添加EMeshPass::EditorLevelInstanceAddVisualizeLevelInstancePass
在两者流程基本相同,这里暂时不考虑nanite物体(nanite物体因为和普通物体的渲染流程不同,要做额外处理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//Texwood0935 Begin Add, For Add A MeshPass  
BEGIN_SHADER_PARAMETER_STRUCT(FVisualizeSpecialTagElementPassParameters, )
SHADER_PARAMETER_STRUCT_INCLUDE(FSceneTextureShaderParameters, SceneTextures)
SHADER_PARAMETER_STRUCT_INCLUDE(FInstanceCullingDrawParams, InstanceCullingDrawParams)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()

void AddVisualizeSpecialTagElementPass(FRDGBuilder& GraphBuilder, const FViewInfo& View,
const FVisualizeLevelInstanceInputs& Inputs)
{
check(Inputs.SceneColor.IsValid());
check(Inputs.SceneDepth.IsValid());

RDG_EVENT_SCOPE(GraphBuilder, "Editor Visualize Special Tag Element");

const uint32 NumSamples = View.GetSceneTexturesConfig().NumSamples;

// Patch uniform buffers with updated state for rendering the outline mesh draw commands.
const FViewInfo* EditorView = CreateEditorPrimitiveView(View, Inputs.SceneColor.ViewRect, NumSamples);

FRDGTextureRef DepthStencilTexture = nullptr;

// Generate custom depth / stencil for outline shapes.
{
{ FRDGTextureDesc DepthStencilDesc = Inputs.SceneColor.Texture->Desc;
DepthStencilDesc.Reset();
DepthStencilDesc.Format = PF_DepthStencil;
// This is a reversed Z depth surface, so 0.0f is the far plane.
DepthStencilDesc.ClearValue = FClearValueBinding((float)ERHIZBuffer::FarPlane, 0);
DepthStencilDesc.Flags = TexCreate_DepthStencilTargetable | TexCreate_ShaderResource;
DepthStencilDesc.NumSamples = NumSamples;

DepthStencilTexture = GraphBuilder.CreateTexture(DepthStencilDesc, TEXT("LevelInstanceDepth"));
}
FScene* Scene = View.Family->Scene->GetRenderScene();

const FScreenPassTextureViewport SceneColorViewport(Inputs.SceneColor);

auto* PassParameters = GraphBuilder.AllocParameters<FVisualizeSpecialTagElementPassParameters>();
const_cast<FViewInfo&>(View).ParallelMeshDrawCommandPasses[EMeshPass::SpecialTagElement].BuildRenderingCommands(GraphBuilder, Scene->GPUScene, PassParameters->InstanceCullingDrawParams);

PassParameters->SceneTextures = Inputs.SceneTextures;
PassParameters->RenderTargets.DepthStencil = FDepthStencilBinding(
DepthStencilTexture,
ERenderTargetLoadAction::EClear,
ERenderTargetLoadAction::EClear,
FExclusiveDepthStencil::DepthWrite_StencilWrite);

GraphBuilder.AddPass(
RDG_EVENT_NAME("LevelInstanceDepth %dx%d", SceneColorViewport.Rect.Width(), SceneColorViewport.Rect.Height()),
PassParameters, ERDGPassFlags::Raster,
[&View, SceneColorViewport, DepthStencilTexture, PassParameters](FRHICommandListImmediate& RHICmdList)
{ RHICmdList.SetViewport(SceneColorViewport.Rect.Min.X, SceneColorViewport.Rect.Min.Y, 0.0f, SceneColorViewport.Rect.Max.X, SceneColorViewport.Rect.Max.Y, 1.0f);

{ SCOPED_DRAW_EVENT(RHICmdList, EditorLevelInstance);

// Run LevelInstance pass on static elements
View.ParallelMeshDrawCommandPasses[EMeshPass::SpecialTagElement].DispatchDraw(nullptr, RHICmdList, &PassParameters->InstanceCullingDrawParams);
}
}); }}
//Texwood0935 End Add, For Add A MeshPass

然后在后处理流程里调用我们的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void AddPostProcessingPasses(  
FRDGBuilder& GraphBuilder,
const FViewInfo& View,
int32 ViewIndex,
bool bAnyLumenActive,
EReflectionsMethod ReflectionsMethod,
const FPostProcessingInputs& Inputs,
const Nanite::FRasterResults* NaniteRasterResults,
FInstanceCullingManager& InstanceCullingManager,
FVirtualShadowMapArray* VirtualShadowMapArray,
FLumenSceneFrameTemporaries& LumenFrameTemporaries,
const FSceneWithoutWaterTextures& SceneWithoutWaterTextures,
FScreenPassTexture TSRMoireInput)
{
...
if (PassSequence.IsEnabled(EPass::VisualizeSpecialTagElement))
{
FVisualizeSpecialTagElement PassInputs;
PassSequence.AcceptOverrideIfLastPass(EPass::VisualizeSpecialTagElement, PassInputs.OverrideOutput);
PassInputs.SceneColor = SceneColor;
PassInputs.SceneDepth = SceneDepth;
PassInputs.SceneTextures.SceneTextures = Inputs.SceneTextures;
SceneColor = AddVisualizeSpecialTagElementPass(GraphBuilder, View, PassInputs);
}
...
}

同时别忘了在计算可见性的地方让Mesh能够加入我们的MeshPass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void MarkRelevant(){
...
{
DrawCommandPacket.AddCommandsForMesh(PrimitiveIndex, PrimitiveSceneInfo, StaticMeshRelevance, StaticMesh, Scene, bCanCache, EMeshPass::SpecialTagElement);
}
...
}

void ComputeDynamicMeshRelevance(EShadingPath ShadingPath, bool bAddLightmapDensityCommands, const FPrimitiveViewRelevance& ViewRelevance, const FMeshBatchAndRelevance& MeshBatch, FViewInfo& View, FMeshPassMask& PassMask, FPrimitiveSceneInfo* PrimitiveSceneInfo, const FPrimitiveBounds& Bounds)
{
if (ViewRelevance.bDrawRelevance)
{
...
//Texwood0935 Begin Add, For Add A MeshPass
PassMask.Set(EMeshPass::SpecialTagElement);
View.NumVisibleDynamicMeshElements[EMeshPass::SpecialTagElement] += NumElements;
//Texwood0935 End Add, For Add A MeshPass
}
}

编译运行,看看我们的结果

备注:调试的时候遇到一个很麻烦的bug,VS输出的SV_Position值有问题,是NAN导致结果出错。目前排查到的原因是LocalToWorld矩阵的值有问题,具体是哪步的值开始传出问题的还没继续排查。因此把Pass改成在BasePass的时候调用了(这个时候的值没问题)。
找到原因了,和后处理的顺序有关,把相应的Pass的顺序提前就好了

最后,代码添加一个MeshPass学习笔记代码记录