说明

本例中,使用添加自定义MeshPass的方式,在添加Shading Model的基础上,对Toon Shading Model增加描边效果。

TODO

根据UE Mesh Drawing Pipeline中对MeshPass的总结,可以知道新增自定义MeshPass需要在Pipeline的进行如下方面的修改:

  • 新增MeshPass声明
  • 创建自定义MeshPass需要使用的MeshMaterialShader
    • FMeshMaterialShader子类声明及实现
    • Shader代码
  • 创建自定义MeshPass对应的MeshPassProcessor
    • 重载AddMeshBatch函数
    • 实现Process函数(可选,仅为调用的封装,全都写在AddMeshBatch里也可以,内置MeshPassProcessor使用两种做法的都有)
    • 向引擎注册创建函数
  • 设置PassMask,将MeshBatch加入自定义MeshPass(Cached & Dynamic)
  • 修改FSceneRenderer
    • 加入自定义MeshPass对应的MeshDrawCommand的收集
    • 在合适的位置使用RDG设置绘制

总结来说,实际上需要做的就是准备好生成MeshDrawCommand所需的各种资源,并在FSceneRenderer中加入对自定义MeshPass的Dispatch,并在该Dispatch逻辑中创建对应的MeshPassProcessor,在其重载的AddMeshBatch函数(Process函数非必需,只是某些MeshPassProcessor多加一层调用的封装,并非从FMeshPassProcessor继承而来)中完成资源的收集,并送给基类FMeshPassProcessor的BuildMeshDrawCommands函数完成MeshDrawCommand的创建收集。

具体步骤

新增MeshPass声明

// Engine\Source\Runtime\Renderer\Public\MeshPassProcessor.h

namespace EMeshPass
{
    enum Type : uint8
    {
        DepthPass,
        SecondStageDepthPass,
        (...)
        WaterInfoTextureDepthPass,
        WaterInfoTexturePass,
        /********** MyDrawBorderMeshPass *****/
        MyDrawBorderMeshPass,
        /*************************************/

#if WITH_EDITOR
        HitProxy,
        HitProxyOpaqueOnly,
        EditorLevelInstance,
        EditorSelection,
#endif

        Num,
        NumBits = 6,
    };
}

inline const TCHAR* GetMeshPassName(EMeshPass::Type MeshPass)
{
    switch (MeshPass)
    {
    case EMeshPass::DepthPass: return TEXT("DepthPass");
    (...)
    case EMeshPass::WaterInfoTextureDepthPass: return TEXT("WaterInfoTextureDepthPass");
    case EMeshPass::WaterInfoTexturePass: return TEXT("WaterInfoTexturePass");
    /********** MyDrawBorderMeshPass *****/
    case EMeshPass::MyDrawBorderMeshPass: return TEXT("MyDrawBorderMeshPass");
    /*************************************/
#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");
#endif
    }

/********** MyDrawBorderMeshPass *****/
#if WITH_EDITOR
  static_assert(EMeshPass::Num == 33 + 4, "Need to update switch(MeshPass) after changing EMeshPass"); // GUID to prevent incorrect auto-resolves, please change when changing the expression: {674D7D62-CFD8-4971-9A8D-CD91E5612CD8}
#else
  static_assert(EMeshPass::Num == 33, "Need to update switch(MeshPass) after changing EMeshPass"); // GUID to prevent incorrect auto-resolves, please change when changing the expression: {674D7D62-CFD8-4971-9A8D-CD91E5612CD8}
#endif
/*************************************/
  checkf(0, TEXT("Missing case for EMeshPass %u"), (uint32)MeshPass);
  return nullptr;
}

创建自定义MeshPass需要使用的MeshMaterialShader

FMeshMaterialShader子类声明及实现

声明

// Engine\Source\Runtime\Renderer\Public\MyDrawBorderMeshPassRendering.h
// ilyaxu

#pragma once
 
#include "CoreMinimal.h"
#include "MeshMaterialShader.h"
#include "MeshMaterialShaderType.h"
#include "MeshPassProcessor.h"

class FMyDrawBorderMeshPassVS : public FMeshMaterialShader
{
public:
  DECLARE_SHADER_TYPE(FMyDrawBorderMeshPassVS, MeshMaterial);

  FMyDrawBorderMeshPassVS() {};

  FMyDrawBorderMeshPassVS(const FMeshMaterialShaderType::CompiledShaderInitializerType& Initializer)
    : FMeshMaterialShader(Initializer)
  {
    BorderThickness.Bind(Initializer.ParameterMap, TEXT("BorderThickness"));
  }
 
  static bool ShouldCompilePermutation(const FShaderPermutationParameters& Parameters)
  {
    return true;
  }

  // static void ModifyCompilationEnvironment(const FMaterialShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
  // {
  // FMeshMaterialShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
  // }
 
  void GetShaderBindings(const FScene* Scene,
    ERHIFeatureLevel::Type FeatureLevel, 
    const FPrimitiveSceneProxy* PrimitiveSceneProxy, 
    const FMaterialRenderProxy& MaterialRenderProxy, 
    const FMaterial& Material, 
    const FMeshPassProcessorRenderState& DrawRenderState, 
    const FMeshMaterialShaderElementData& ShaderElementData, 
    FMeshDrawSingleShaderBindings& ShaderBindings) const
  {
    FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
  }
 
private:
  LAYOUT_FIELD(FShaderParameter, BorderThickness);
};
 
 
class FMyDrawBorderMeshPassPS : public FMeshMaterialShader
{
public:
  DECLARE_SHADER_TYPE(FMyDrawBorderMeshPassPS, MeshMaterial);

  FMyDrawBorderMeshPassPS() {};

  FMyDrawBorderMeshPassPS(const FMeshMaterialShaderType::CompiledShaderInitializerType& Initializer)
    : FMeshMaterialShader(Initializer)
  {
    BorderColor.Bind(Initializer.ParameterMap, TEXT("BorderColor"));
  }

  static bool ShouldCompilePermutation(const FShaderPermutationParameters& Parameters)
  {
    return true;
  }

  // static void ModifyCompilationEnvironment(const FMaterialShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
  // {
  // FMeshMaterialShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
  // }
 
  void GetShaderBindings(const FScene* Scene,
    ERHIFeatureLevel::Type FeatureLevel,
    const FPrimitiveSceneProxy* PrimitiveSceneProxy,
    const FMaterialRenderProxy& MaterialRenderProxy,
    const FMaterial& Material,
    const FMeshPassProcessorRenderState& DrawRenderState,
    const FMeshMaterialShaderElementData& ShaderElementData,
    FMeshDrawSingleShaderBindings& ShaderBindings) const
  {
    FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
  }
 
private:
  LAYOUT_FIELD(FShaderParameter, BorderColor);
};

实现

// Engine\Source\Runtime\Renderer\Private\MyDrawBorderMeshPassRendering.cpp

IMPLEMENT_MATERIAL_SHADER_TYPE(, FMyDrawBorderMeshPassVS, TEXT("/Engine/Private/FMyDrawBorderMeshPassShader.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_MATERIAL_SHADER_TYPE(, FMyDrawBorderMeshPassPS, TEXT("/Engine/Private/FMyDrawBorderMeshPassShader.usf"), TEXT("MainPS"), SF_Pixel);

HLSL代码

// Engine\Shaders\Private\FMyDrawBorderMeshPassShader.usf
// ilyaxu

#include "Common.ush"
#include "MobileBasePassCommon.ush"
#include "/Engine/Generated/Material.ush"
#include "/Engine/Generated/VertexFactory.ush"


struct FMyDrawBorderMeshPassShaderVStoPS
{
  FVertexFactoryInterpolantsVSToPS FactoryInterpolants;
  FMobileBasePassInterpolantsVSToPS BasePassInterpolants;
  float4 Position : SV_POSITION;
};

void MainVS(
  FVertexFactoryInput Input,
  out FMyDrawBorderMeshPassShaderVStoPS Output)
{
  ResolvedView = ResolveView();
  FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input);
  float4 WorldPositionExcludingWPO = VertexFactoryGetWorldPosition(Input, VFIntermediates);
  float4 WorldPosition = WorldPositionExcludingWPO;
  half3x3 TangentToLocal = VertexFactoryGetTangentToLocal(Input, VFIntermediates);
  FMaterialVertexParameters VertexParameters = GetMaterialVertexParameters(Input, VFIntermediates, WorldPosition.xyz, TangentToLocal);
#if MATERIAL_SHADINGMODEL_TOON
  half3 WorldPositionOffset = GetMaterialWorldPositionOffset(VertexParameters);
#else
  half3 WorldPositionOffset = 0;
#endif
  WorldPosition.xyz += WorldPositionOffset;
  float4 RasterizedWorldPosition = VertexFactoryGetRasterizedWorldPosition(Input, VFIntermediates, WorldPosition);
  Output.Position = mul(RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip);
}

void MainPS(
  FVertexFactoryInterpolantsVSToPS Interpolants
  , FMobileBasePassInterpolantsVSToPS BasePassInterpolants
  , in float4 SvPosition : SV_Position
  , out half4 OutColor : SV_Target0)
{
  const bool bIsFrontFace = false;
  FMaterialPixelParameters MaterialParameters = GetMaterialPixelParameters(Interpolants, SvPosition);
  FPixelMaterialInputs PixelMaterialInputs;
  {
    float4 ScreenPosition = SvPositionToResolvedScreenPosition(SvPosition);
    float3 WorldPosition = BasePassInterpolants.PixelPosition.xyz;
    float3 WorldPositionExcludingWPO = BasePassInterpolants.PixelPosition.xyz;
#if USE_WORLD_POSITION_EXCLUDING_SHADER_OFFSETS
    WorldPositionExcludingWPO = BasePassInterpolants.PixelPositionExcludingWPO;
#endif
    CalcMaterialParametersEx(MaterialParameters, PixelMaterialInputs, SvPosition, ScreenPosition, bIsFrontFace, WorldPosition, WorldPositionExcludingWPO);

#if FORCE_VERTEX_NORMAL
    // Quality level override of material's normal calculation, can be used to avoid normal map reads etc.
    MaterialParameters.WorldNormal = MaterialParameters.TangentToWorld[2];
    MaterialParameters.ReflectionVector = ReflectionAboutCustomWorldNormal(MaterialParameters, MaterialParameters.WorldNormal, false);
#endif
  }
 
#if !EARLY_Z_PASS_ONLY_MATERIAL_MASKING
  //Clip if the blend mode requires it.
  GetMaterialCoverageAndClipping(MaterialParameters, PixelMaterialInputs);
#endif

  half3 OutlineColor = half3(0.0, 0.0, 0.0);

// #if NUM_MATERIAL_OUTPUTS_GETOUTLINECOLOR > 0 && MATERIAL_SHADINGMODEL_TOON
// OutlineColor = GetBorderColor0(MaterialParameters);
// #endif

#if MATERIAL_SHADINGMODEL_TOON
  OutlineColor = half3(1.0, 1.0, 1.0);
#endif

  OutColor = half4(OutlineColor, 1.0);
}
// Engine\Shaders\Private\MobileBasePassVertexShader.usf

void Main(
  FVertexFactoryInput Input
  , out FMobileShadingBasePassVSOutput Output
#if INSTANCED_STEREO
  , out uint LayerIndex : SV_RenderTargetArrayIndex
#elif MOBILE_MULTI_VIEW
  , in uint ViewId : SV_ViewID
#endif
  )
{
  (...)
  // float3 WorldPositionOffset = GetMaterialWorldPositionOffset(VertexParameters);
#if MATERIAL_SHADINGMODEL_TOON
  float3 WorldPositionOffset = float3(0.0, 0.0, 0.0);
#else
  float3 WorldPositionOffset = GetMaterialWorldPositionOffset(VertexParameters);
#endif
  (...)
}

创建MeshPassProcessor

MeshPassProcessor声明

// Engine\Source\Runtime\Renderer\Public\MyDrawBorderMeshPassRendering.h
// ilyaxu

#pragma once
 
#include "CoreMinimal.h"
#include "MeshMaterialShader.h"
#include "MeshMaterialShaderType.h"
#include "MeshPassProcessor.h"
 
class FMyDrawBorderMeshPassProcessor : public FMeshPassProcessor
{
public:

  // UE5.4建议使用如下形式的Ctor,增加一个参数EMeshPass::Type
  FMyDrawBorderMeshPassProcessor(
    EMeshPass::Type InMeshPassType,
    const FScene* Scene,
    const FSceneView* InViewIfDynamicMeshCommand,
    const FMeshPassProcessorRenderState& InPassDrawRenderState,
    FMeshPassDrawListContext* InDrawListContext);

  virtual void AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId = -1) override final;
 
public:
  FMeshPassProcessorRenderState PassDrawRenderState;
 
private:
  void Process(
    const FMeshBatch& MeshBatch,
    uint64 BatchElementMask,
    const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy,
    int32 StaticMeshId,
    const FMaterialRenderProxy& RESTRICT MaterialRenderProxy,
    const FMaterial& RESTRICT MaterialResource,
    ERasterizerFillMode MeshFillMode,
    ERasterizerCullMode MeshCullMode);
};

// Engine\Source\Runtime\Renderer\Private\MyDrawBorderMeshPassRendering.cpp

FMyDrawBorderMeshPassProcessor::FMyDrawBorderMeshPassProcessor(
  EMeshPass::Type InMeshPassType,
  const FScene* Scene, 
  const FSceneView* InViewIfDynamicMeshCommand, 
  const FMeshPassProcessorRenderState& InPassDrawRenderState, 
  FMeshPassDrawListContext* InDrawListContext)
  : FMeshPassProcessor(InMeshPassType, Scene, Scene->GetFeatureLevel(), InViewIfDynamicMeshCommand, InDrawListContext)
  , PassDrawRenderState(InPassDrawRenderState)
{
}

重载AddMeshBatch函数

在AddMeshBatch函数中对当前Material所用的ShadingModel进行判断,仅对自定义的Toon Shading Model进行描边

// Engine\Source\Runtime\Renderer\Private\MyDrawBorderMeshPassRendering.cpp

void FMyDrawBorderMeshPassProcessor::AddMeshBatch(const FMeshBatch& MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* PrimitiveSceneProxy, int32 StaticMeshId)
{
  const FMaterialRenderProxy* FallbackMaterialRenderProxyPtr = nullptr;
  const FMaterial& Material = MeshBatch.MaterialRenderProxy->GetMaterialWithFallback(FeatureLevel, FallbackMaterialRenderProxyPtr);

  if (Material.GetShadingModels().HasShadingModel(MSM_Toon))
  {
    const FMeshDrawingPolicyOverrideSettings OverrideSettings = ComputeMeshOverrideSettings(MeshBatch);
    const ERasterizerFillMode MeshFillMode = ComputeMeshFillMode(Material, OverrideSettings);
    const ERasterizerCullMode MeshCullMode = ComputeMeshCullMode(Material, OverrideSettings);
    const FMaterialRenderProxy& MaterialRenderProxy = FallbackMaterialRenderProxyPtr ? *FallbackMaterialRenderProxyPtr : *MeshBatch.MaterialRenderProxy;
    Process(MeshBatch, BatchElementMask, PrimitiveSceneProxy, StaticMeshId, MaterialRenderProxy, Material, MeshFillMode, MeshCullMode);
  }
}

实现Process函数(可选)

// Engine\Source\Runtime\Renderer\Private\MyDrawBorderMeshPassRendering.cpp

void FMyDrawBorderMeshPassProcessor::Process(const FMeshBatch& MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* PrimitiveSceneProxy, int32 StaticMeshId, const FMaterialRenderProxy& MaterialRenderProxy, const FMaterial& MaterialResource, ERasterizerFillMode MeshFillMode, ERasterizerCullMode MeshCullMode)
{
  const FVertexFactory* VertexFactory = MeshBatch.VertexFactory;

  TMeshProcessorShaders<
    FMyDrawBorderMeshPassVS,
    FMyDrawBorderMeshPassPS> BorderGlowPassShaders;

  if (!GetMyDrawBorderMeshPassShaders(MaterialResource, VertexFactory->GetType(), BorderGlowPassShaders.VertexShader, BorderGlowPassShaders.PixelShader))
    return;

  FMeshMaterialShaderElementData ShaderElementData;
  ShaderElementData.InitializeMeshMaterialData(ViewIfDynamicMeshCommand, PrimitiveSceneProxy, MeshBatch, StaticMeshId, false);

  const FMeshDrawCommandSortKey SortKey = CalculateMeshStaticSortKey(BorderGlowPassShaders.VertexShader, BorderGlowPassShaders.PixelShader);
  MeshCullMode = ERasterizerCullMode::CM_CCW;
 
  BuildMeshDrawCommands(
    MeshBatch,
    BatchElementMask,
    PrimitiveSceneProxy,
    MaterialRenderProxy,
    MaterialResource,
    PassDrawRenderState,
    BorderGlowPassShaders,
    MeshFillMode,
    MeshCullMode,
    SortKey,
    EMeshPassFeatures::Default,
    ShaderElementData);
}

向引擎注册创建函数

定义MeshProcessor创建函数,并向引擎注册,以便后续可通过FPassProcessorManager::CreateMeshPassProcessor来创建

// Engine\Source\Runtime\Renderer\Public\MyDrawBorderMeshPassRendering.h

FMeshPassProcessor* CreateMyDrawBorderMeshPassProcessor(ERHIFeatureLevel::Type FeatureLevel, const FScene* Scene, const FSceneView* InViewIfDynamicMeshCommand, FMeshPassDrawListContext* InDrawListContext)
{
  FMeshPassProcessorRenderState PassDrawRenderState;
  PassDrawRenderState.SetBlendState(TStaticBlendStateWriteMask<CW_RGBA>::GetRHI());
  PassDrawRenderState.SetDepthStencilAccess(Scene->DefaultBasePassDepthStencilAccess);
  PassDrawRenderState.SetDepthStencilState(TStaticDepthStencilState<true, CF_DepthNearOrEqual>::GetRHI());

  return new FMyDrawBorderMeshPassProcessor(EMeshPass::MyDrawBorderMeshPass, Scene, InViewIfDynamicMeshCommand, PassDrawRenderState, InDrawListContext);
}

// UE5 使用下列宏来将MeshPassProcessor注册进FPassProcessorManager和FPSOCollectorCreateManager
REGISTER_MESHPASSPROCESSOR_AND_PSOCOLLECTOR(MyDrawBorderMeshPass, CreateMyDrawBorderMeshPassProcessor, EShadingPath::Mobile, EMeshPass::MyDrawBorderMeshPass, EMeshPassFlags::CachedMeshCommands | EMeshPassFlags::MainView);

设置PassMask

Dynamic

// Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp

static void ComputeDynamicMeshRelevance(
  EShadingPath ShadingPath,
  bool bAddLightmapDensityCommands,
  const FPrimitiveViewRelevance& ViewRelevance,
  const FMeshBatchAndRelevance& MeshBatch,
  FViewInfo& View,
  FMeshPassMask& PassMask,
  FPrimitiveSceneInfo* PrimitiveSceneInfo,
  const FPrimitiveBounds& Bounds)
{
  const int32 NumElements = MeshBatch.Mesh->Elements.Num();
  if (ViewRelevance.bDrawRelevance && (ViewRelevance.bRenderInMainPass || ViewRelevance.bRenderCustomDepth || ViewRelevance.bRenderInDepthPass))
  {
    (...)
    if (ViewRelevance.bRenderInMainPass || ViewRelevance.bRenderCustomDepth)
    {
      (...)
      if (ShadingPath == EShadingPath::Mobile)
      {
        PassMask.Set(EMeshPass::MobileBasePassCSM);
        View.NumVisibleDynamicMeshElements[EMeshPass::MobileBasePassCSM] += NumElements;

        PassMask.Set(EMeshPass::MyDrawBorderMeshPass);
        View.NumVisibleDynamicMeshElements[EMeshPass::MyDrawBorderMeshPass] += NumElements;
      }
      (...)
    }
  }
  (...)
}

Static

// Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp

void FRelevancePacket::ComputeRelevance(FDynamicPrimitiveIndexList& DynamicPrimitiveIndexList)
{
    (...)    
    
            if ((StaticMeshRelevance.bUseForMaterial || StaticMeshRelevance.bUseAsOccluder)
              && (ViewRelevance.bRenderInMainPass || ViewRelevance.bRenderCustomDepth || ViewRelevance.bRenderInDepthPass)
              && !bHiddenByHLODFade)
            {
              (...)

              // Mark static mesh as visible for rendering
              if (StaticMeshRelevance.bUseForMaterial && (ViewRelevance.bRenderInMainPass || ViewRelevance.bRenderCustomDepth))
              {
                // Specific logic for mobile packets
                if (ShadingPath == EShadingPath::Mobile)
                {
                  // Skydome must not be added to base pass bucket
                  if (!StaticMeshRelevance.bUseSkyMaterial)
                  {
                    DrawCommandPacket.AddCommandsForMesh(PrimitiveIndex, PrimitiveSceneInfo, StaticMeshRelevance, StaticMesh, CullingPayloadFlags, Scene, bCanCache, EMeshPass::BasePass);
                    if (!bMobileBasePassAlwaysUsesCSM)
                    {
                      DrawCommandPacket.AddCommandsForMesh(PrimitiveIndex, PrimitiveSceneInfo, StaticMeshRelevance, StaticMesh, CullingPayloadFlags, Scene, bCanCache, EMeshPass::MobileBasePassCSM);
                      DrawCommandPacket.AddCommandsForMesh(PrimitiveIndex, PrimitiveSceneInfo, StaticMeshRelevance, StaticMesh, CullingPayloadFlags, Scene, bCanCache, EMeshPass::MyDrawBorderMeshPass);
                    }
                  }

                  (...)
                }

                (...)
              }
              (...)
            }

    (...)
}

修改FSceneRenderer

描边的MeshPass需要加载BasePass之后,需要进行的主要有两项:收集MeshDrawCommand,以及在BasePass之后进行绘制。

收集MeshDrawCommand

在BasePass入队生成MeshDrawCommand任务之后添加

// Engine\Source\Runtime\Renderer\Private\MobileShadingRenderer.cpp

void FMobileSceneRenderer::SetupMobileBasePassAfterShadowInit(FExclusiveDepthStencil::Type BasePassDepthStencilAccess, TArrayView<FViewCommands> ViewCommandsPerView, FInstanceCullingManager& InstanceCullingManager)
{
  for (int32 ViewIndex = 0; ViewIndex < AllViews.Num(); ++ViewIndex)
  {
    (...)

    FName PassName(GetMeshPassName(EMeshPass::BasePass));
    Pass.DispatchPassSetup(
      Scene,
      View,
      FInstanceCullingContext(PassName, ShaderPlatform, &InstanceCullingManager, ViewIds, nullptr, InstanceCullingMode),
      EMeshPass::BasePass,
      BasePassDepthStencilAccess,
      MeshPassProcessor,
      View.DynamicMeshElements,
      &View.DynamicMeshElementsPassRelevance,
      View.NumVisibleDynamicMeshElements[EMeshPass::BasePass],
      ViewCommands.DynamicMeshCommandBuildRequests[EMeshPass::BasePass],
      ViewCommands.DynamicMeshCommandBuildFlags[EMeshPass::BasePass],
      ViewCommands.NumDynamicMeshCommandBuildRequestElements[EMeshPass::BasePass],
      ViewCommands.MeshCommands[EMeshPass::BasePass],
      BasePassCSMMeshPassProcessor,
      &ViewCommands.MeshCommands[EMeshPass::MobileBasePassCSM]);

    /********** MyDrawBorderMeshPass *****/
    FMeshPassProcessor* MyDrawBorderMeshPassProcessor = FPassProcessorManager::CreateMeshPassProcessor(EShadingPath::Mobile, EMeshPass::MyDrawBorderMeshPass, Scene->GetFeatureLevel(), Scene, &View, nullptr);
    FParallelMeshDrawCommandPass& MyDrawBorderPass = View.ParallelMeshDrawCommandPasses[EMeshPass::MyDrawBorderMeshPass];
    
    FName MyDrawBorderPassName(GetMeshPassName(EMeshPass::MyDrawBorderMeshPass));

    MyDrawBorderPass.DispatchPassSetup(
      Scene,
      View,
      FInstanceCullingContext(MyDrawBorderPassName, ShaderPlatform, &InstanceCullingManager, ViewIds, nullptr, InstanceCullingMode),
      EMeshPass::MyDrawBorderMeshPass,
      BasePassDepthStencilAccess,
      MeshPassProcessor,
      View.DynamicMeshElements,
      &View.DynamicMeshElementsPassRelevance,
      View.NumVisibleDynamicMeshElements[EMeshPass::MyDrawBorderMeshPass],
      ViewCommands.DynamicMeshCommandBuildRequests[EMeshPass::MyDrawBorderMeshPass],
      ViewCommands.DynamicMeshCommandBuildFlags[EMeshPass::MyDrawBorderMeshPass],
      ViewCommands.NumDynamicMeshCommandBuildRequestElements[EMeshPass::MyDrawBorderMeshPass],
      ViewCommands.MeshCommands[EMeshPass::MyDrawBorderMeshPass]);
    /*************************************/
  }
}

// Engine\Source\Runtime\Renderer\Private\SceneRendering.cpp

void FSceneRenderer::SetupMeshPass(FViewInfo& View, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, FViewCommands& ViewCommands, FInstanceCullingManager& InstanceCullingManager)
{
  SCOPE_CYCLE_COUNTER(STAT_SetupMeshPass);

  const EShadingPath ShadingPath = GetFeatureLevelShadingPath(Scene->GetFeatureLevel());

  for (int32 PassIndex = 0; PassIndex < EMeshPass::Num; PassIndex++)
  {
    const EMeshPass::Type PassType = (EMeshPass::Type)PassIndex;

    if ((FPassProcessorManager::GetPassFlags(ShadingPath, PassType) & EMeshPassFlags::MainView) != EMeshPassFlags::None)
    {
      // Mobile: BasePass and MobileBasePassCSM lists need to be merged and sorted after shadow pass.
      if (ShadingPath == EShadingPath::Mobile && (PassType == EMeshPass::BasePass || PassType == EMeshPass::MobileBasePassCSM || PassType == EMeshPass::MyDrawBorderMeshPass))
      {
        continue;
      }

      (...)
    }
  }
}

在BasePass后设置绘制

// Engine\Source\Runtime\Renderer\Private\MobileShadingRenderer.cpp

void FMobileSceneRenderer::RenderMyDrawBorderMeshPass(FRHICommandListImmediate& RHICmdList, const TArrayView<const FViewInfo*> PassViews)
{
  for (int32 ViewIndex = 0; ViewIndex < PassViews.Num(); ViewIndex++)
  {
    SCOPED_CONDITIONAL_DRAW_EVENTF(RHICmdList, EventView, Views.Num() > 1, TEXT("View%d"), ViewIndex);
    const FViewInfo& View = *PassViews[ViewIndex];
    if (!View.ShouldRenderView())
    {
      continue;
    }

    RHICmdList.SetViewport(View.ViewRect.Min.X, View.ViewRect.Min.Y, 0, View.ViewRect.Max.X, View.ViewRect.Max.Y, 1);
    View.ParallelMeshDrawCommandPasses[EMeshPass::MyDrawBorderMeshPass].DispatchDraw(nullptr, RHICmdList);
  }
}


void FMobileSceneRenderer::RenderForwardSinglePass(FRDGBuilder& GraphBuilder, FMobileRenderPassParameters* PassParameters, FRenderViewContext& ViewContext, FSceneTextures& SceneTextures)
{
  (...)

  GraphBuilder.AddPass(
    RDG_EVENT_NAME("SceneColorRendering"),
    PassParameters,
    // the second view pass should not be merged with the first view pass on mobile since the subpass would not work properly.
    ERDGPassFlags::Raster | ERDGPassFlags::NeverMerge,
    [this, PassParameters, ViewContext, bDoOcclusionQueries, &SceneTextures](FRHICommandList& RHICmdList)
  {
    (...)

    // Depth pre-pass
    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLM_MobilePrePass));
    RenderMaskedPrePass(RHICmdList, View);
    // Opaque and masked
    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Opaque));
    RenderMobileBasePass(RHICmdList, View, &PassParameters->InstanceCullingDrawParams);
    /********** MyDrawBorderMeshPass *****/
    RenderMyDrawBorderMeshPass(RHICmdList, View);
    /*************************************/

    (...)
  });

  (...)
}

效果

ToonShadingModel描边效果

result

RenderDoc截帧

使用宏添加统计数据以使自定义MeshPass能被RenderDoc捕获

// Engine\Source\Runtime\RenderCore\Public\RenderCore.h

(...)

DECLARE_CYCLE_STAT_EXTERN(TEXT("Depth drawing"),STAT_DepthDrawTime,STATGROUP_SceneRendering, RENDERCORE_API);
DECLARE_CYCLE_STAT_EXTERN(TEXT("Base pass drawing"),STAT_BasePassDrawTime,STATGROUP_SceneRendering, RENDERCORE_API);
/********************/
DECLARE_CYCLE_STAT_EXTERN(TEXT("My draw border pass drawing"),STAT_MyDrawBorderPassDrawTime,STATGROUP_SceneRendering, RENDERCORE_API);
/********************/
DECLARE_CYCLE_STAT_EXTERN(TEXT("Anisotropy pass drawing"), STAT_AnisotropyPassDrawTime, STATGROUP_SceneRendering, RENDERCORE_API);
DECLARE_CYCLE_STAT_EXTERN(TEXT("Water pass drawing"), STAT_WaterPassDrawTime, STATGROUP_SceneRendering, RENDERCORE_API);

(...)

// Engine\Source\Runtime\RenderCore\Private\RenderCore.cpp

(...)

DEFINE_STAT(STAT_StaticDrawListDrawTime);
DEFINE_STAT(STAT_BasePassDrawTime);
/********************/
DEFINE_STAT(STAT_MyDrawBorderPassDrawTime);
/********************/
DEFINE_STAT(STAT_AnisotropyPassDrawTime);
DEFINE_STAT(STAT_DepthDrawTime);

(...)

// Engine\Source\Runtime\Renderer\Private\MobileBasePassRendering.cpp

void FMobileSceneRenderer::RenderMyDrawBorderMeshPass(FRHICommandList& RHICmdList, const FViewInfo& View)
{
  CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderMyDrawBorderMeshPass);
  SCOPED_DRAW_EVENT(RHICmdList, MyDrawBorderMeshPass);
  SCOPE_CYCLE_COUNTER(STAT_MyDrawBorderPassDrawTime);

  RHICmdList.SetViewport(View.ViewRect.Min.X, View.ViewRect.Min.Y, 0, View.ViewRect.Max.X, View.ViewRect.Max.Y, 1);
  View.ParallelMeshDrawCommandPasses[EMeshPass::MyDrawBorderMeshPass].DispatchDraw(nullptr, RHICmdList);
}

renderdoc

Reference

Unreal Engine Documentation: Render Dependency Graph

(UE4 4.27)UE4添加一次自定义的MeshPass实现移动端边缘发光

虚幻引擎之自定义MeshPass

UE5 添加自定义MeshPass

剖析虚幻渲染体系(03)- 渲染机制