BlueRose
文章97
标签28
分类7
基于插件的StaticMesh多Pass绘制方案

基于插件的StaticMesh多Pass绘制方案

前言

StaticMesh与SkeletalMesh的实现方法比较类似,不过两者的绘制方式有很大的不同。不过庆幸的是,Static的MeshBatch设置在绘制函数的外面。

思路说明

创建自定义StaticMeshComponent

  • 继承UStaticMeshComponent,重写GetUsedMaterials与CreateSceneProxy函数。
  • 添加用于多pass渲染的UMaterialInterface指针。(NeedSecondPass变量其实可以不用,因为既然你要使用这个类,那肯定要启用这个功能)
#pragma once
#include "CoreMinimal.h"
#include "Engine/Classes/Components/StaticMeshComponent.h"
#include "StrokeStaticMeshComponent.generated.h"

UCLASS(ClassGroup=(Rendering, Common), hidecategories=Object,  editinlinenew, meta=(BlueprintSpawnableComponent))
class UStrokeStaticMeshComponent : public UStaticMeshComponent
{
    GENERATED_UCLASS_BODY()

    //~ Begin UPrimitiveComponent Interface
    virtual void GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials = false) const override;

    virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
    //~ End UPrimitiveComponent Interface
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiplePass")
    UMaterialInterface* SecondPassMaterial = nullptr;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiplePass")
    bool NeedSecondPass=false;
};
  • 实现GetUsedMaterials与CreateSceneProxy函数,并包含相应头文件。

    头文件

    #include "StrokeStaticMeshSceneProxy.h"
    

    GetUsedMaterials

    void UStrokeStaticMeshComponent::GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials /*= false*/) const
    {
      if (GetStaticMesh() && GetStaticMesh()->RenderData)
      {
          TMap<int32, UMaterialInterface*> MapOfMaterials;
          for (int32 LODIndex = 0; LODIndex < GetStaticMesh()->RenderData->LODResources.Num(); LODIndex++)
          {
              FStaticMeshLODResources& LODResources = GetStaticMesh()->RenderData->LODResources[LODIndex];
              int32 MaterialNum = 0;
              for (int32 SectionIndex = 0; SectionIndex < LODResources.Sections.Num(); SectionIndex++)
              {
                  // Get the material for each element at the current lod index
                  int32 MaterialIndex = LODResources.Sections[SectionIndex].MaterialIndex;
                  if (!MapOfMaterials.Contains(MaterialIndex))
                  {
                      MapOfMaterials.Add(MaterialIndex, GetMaterial(MaterialIndex));
                      MaterialNum++;
                  }
              }
              //这里是我添加的代码
              if (NeedSecondPass)
              {
                  bool NeedAddMaterial = true;
                  for (int i = 0; i < MapOfMaterials.Num(); ++i)
                  {
                      if (MapOfMaterials[i]== SecondPassMaterial)
                      {
                          NeedAddMaterial = false;
                      }
                  }
                  if (NeedAddMaterial)
                  {
                      MapOfMaterials.Add(MaterialNum, SecondPassMaterial);
                  }
              }
          }
          if (MapOfMaterials.Num() > 0)
          {
              //We need to output the material in the correct order (follow the material index)
              //So we sort the map with the material index
              MapOfMaterials.KeySort([](int32 A, int32 B) {
                  return A < B; // sort keys in order
              });
    
              //Preadd all the material item in the array
              OutMaterials.AddZeroed(MapOfMaterials.Num());
              //Set the value in the correct order
              int32 MaterialIndex = 0;
              for (auto Kvp : MapOfMaterials)
              {
                  OutMaterials[MaterialIndex++] = Kvp.Value;
              }
          }
      }
    }
    

    CreateSceneProxy

    FPrimitiveSceneProxy* UStrokeStaticMeshComponent::CreateSceneProxy()
    {
      if (GetStaticMesh() == nullptr || GetStaticMesh()->RenderData == nullptr)
      {
          return nullptr;
      }
    
      const TIndirectArray<FStaticMeshLODResources>& LODResources = GetStaticMesh()->RenderData->LODResources;
      if (LODResources.Num() == 0 || LODResources[FMath::Clamp<int32>(GetStaticMesh()->MinLOD.Default, 0, LODResources.Num() - 1)].VertexBuffers.StaticMeshVertexBuffer.GetNumVertices() == 0)
      {
          return nullptr;
      }
      //与SkeletalMeshComponent中写的类似,直接通过new来创建
      FPrimitiveSceneProxy* Proxy = ::new FStrokeStaticMeshSceneProxy(this, false);
    #if STATICMESH_ENABLE_DEBUG_RENDERING
      SendRenderDebugPhysics(Proxy);
    #endif
      return Proxy;
    }
    

    创建自定义StaticMeshSceneProxy

    这里与SkeletalMeshSceneProxy不同,GetDynamicMeshElements没有起到绘制作用,真正起到作用的是DrawStaticElements。

  • 继承FStaticMeshSceneProxy,重写DrawStaticElements函数。
  • 定义UStaticMeshComponent指针,用于获取自定义StaticMeshComponent的变量。(之后使用需要使用强制转换)
#pragma once
#include "Engine\Public\StaticMeshResources.h"

class FStrokeStaticMeshSceneProxy : public FStaticMeshSceneProxy
{
public:
    //在UStrokeStaticMeshComponent中调用这个构造函数初始化
    FStrokeStaticMeshSceneProxy(UStaticMeshComponent* Component, bool bForceLODsShareStaticLighting);

    virtual void DrawStaticElements(FStaticPrimitiveDrawInterface* PDI) override;

private:
    UPROPERTY()
    const UStaticMeshComponent* ComponentPtr;
};
  • 复制DrawStaticElements中所需函数的代码。
    将Engine\Source\Runtime\Engine\Private\StaticMeshRender.cpp中的AllowShadowOnlyMesh、UseLightPropagationVolumeRT2函数代码复制到你定义的StaticMeshSceneProxy类所在的cpp文件中。
  • 实现DrawStaticElements函数,并包含头文件。

与SkeletalMesh的绘制函数不同,它有3处需要调用绘制函数的地方。其中有一处因为不存在lod所以代码有些不同,所以请不要在未测试的情况下使用本插件进行生产项目。

不过一个好消息是,我们可以在绘制前设置MeshBatch,从而完美结局描边问题。

void FStrokeStaticMeshSceneProxy::DrawStaticElements(FStaticPrimitiveDrawInterface* PDI)
{
    checkSlow(IsInParallelRenderingThread());
    if (!HasViewDependentDPG())
    {
        // Determine the DPG the primitive should be drawn in.
        uint8 PrimitiveDPG = GetStaticDepthPriorityGroup();
        int32 NumLODs = RenderData->LODResources.Num();
        //Never use the dynamic path in this path, because only unselected elements will use DrawStaticElements
        bool bIsMeshElementSelected = false;
        const auto FeatureLevel = GetScene().GetFeatureLevel();

        //check if a LOD is being forced
        if (ForcedLodModel > 0)
        {
            int32 LODIndex = FMath::Clamp(ForcedLodModel, ClampedMinLOD + 1, NumLODs) - 1;
            const FStaticMeshLODResources& LODModel = RenderData->LODResources[LODIndex];
            // Draw the static mesh elements.
            for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
            {
#if WITH_EDITOR
                if (GIsEditor)
                {
                    const FLODInfo::FSectionInfo& Section = LODs[LODIndex].Sections[SectionIndex];

                    bIsMeshElementSelected = Section.bSelected;
                    PDI->SetHitProxy(Section.HitProxy);
                }
#endif // WITH_EDITOR

                const int32 NumBatches = GetNumMeshBatches();

                PDI->ReserveMemoryForMeshes(NumBatches);

                for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
                {
                    FMeshBatch MeshBatch;

                    if (GetMeshElement(LODIndex, BatchIndex, SectionIndex, PrimitiveDPG, bIsMeshElementSelected, true, MeshBatch))
                    {
                        PDI->DrawMesh(MeshBatch, FLT_MAX);
                        //以下是我添加的代码
                        const FLODInfo& ProxyLODInfo = LODs[LODIndex];
                        UMaterialInterface* MaterialInterface = ProxyLODInfo.Sections[SectionIndex].Material;

                        const UStrokeStaticMeshComponent* StrokeStaticMeshComponent = dynamic_cast<const UStrokeStaticMeshComponent *>(ComponentPtr);
                        if (MaterialInterface == StrokeStaticMeshComponent->SecondPassMaterial)
                        {
                            continue;
                        }
                        if (StrokeStaticMeshComponent->NeedSecondPass) {
                            if (StrokeStaticMeshComponent->SecondPassMaterial == nullptr) {
                                continue;
                            }
                            MeshBatch.MaterialRenderProxy = StrokeStaticMeshComponent->SecondPassMaterial->GetRenderProxy();
                            //设置反转剔除选项
                            MeshBatch.ReverseCulling = true;
                            PDI->DrawMesh(MeshBatch, FLT_MAX);
                        }
                    }
                }
            }
        }
        else //no LOD is being forced, submit them all with appropriate cull distances
        {
            for (int32 LODIndex = ClampedMinLOD; LODIndex < NumLODs; LODIndex++)
            {
                const FStaticMeshLODResources& LODModel = RenderData->LODResources[LODIndex];
                float ScreenSize = GetScreenSize(LODIndex);

                bool bUseUnifiedMeshForShadow = false;
                bool bUseUnifiedMeshForDepth = false;

                if (GUseShadowIndexBuffer && LODModel.bHasDepthOnlyIndices)
                {
                    const FLODInfo& ProxyLODInfo = LODs[LODIndex];

                    // The shadow-only mesh can be used only if all elements cast shadows and use opaque materials with no vertex modification.
                    // In some cases (e.g. LPV) we don't want the optimization
                    bool bSafeToUseUnifiedMesh = AllowShadowOnlyMesh(FeatureLevel);

                    bool bAnySectionUsesDitheredLODTransition = false;
                    bool bAllSectionsUseDitheredLODTransition = true;
                    bool bIsMovable = IsMovable();
                    bool bAllSectionsCastShadow = bCastShadow;

                    for (int32 SectionIndex = 0; bSafeToUseUnifiedMesh && SectionIndex < LODModel.Sections.Num(); SectionIndex++)
                    {
                        const FMaterial* Material = ProxyLODInfo.Sections[SectionIndex].Material->GetRenderProxy()->GetMaterial(FeatureLevel);
                        // no support for stateless dithered LOD transitions for movable meshes
                        bAnySectionUsesDitheredLODTransition = bAnySectionUsesDitheredLODTransition || (!bIsMovable && Material->IsDitheredLODTransition());
                        bAllSectionsUseDitheredLODTransition = bAllSectionsUseDitheredLODTransition && (!bIsMovable && Material->IsDitheredLODTransition());
                        const FStaticMeshSection& Section = LODModel.Sections[SectionIndex];

                        bSafeToUseUnifiedMesh =
                            !(bAnySectionUsesDitheredLODTransition && !bAllSectionsUseDitheredLODTransition) // can't use a single section if they are not homogeneous
                            && Material->WritesEveryPixel()
                            && !Material->IsTwoSided()
                            && !IsTranslucentBlendMode(Material->GetBlendMode())
                            && !Material->MaterialModifiesMeshPosition_RenderThread()
                            && Material->GetMaterialDomain() == MD_Surface;

                        bAllSectionsCastShadow &= Section.bCastShadow;
                    }

                    if (bSafeToUseUnifiedMesh)
                    {
                        bUseUnifiedMeshForShadow = bAllSectionsCastShadow;

                        // Depth pass is only used for deferred renderer. The other conditions are meant to match the logic in FDepthPassMeshProcessor::AddMeshBatch.
                        bUseUnifiedMeshForDepth = ShouldUseAsOccluder() && GetScene().GetShadingPath() == EShadingPath::Deferred && !IsMovable();

                        if (bUseUnifiedMeshForShadow || bUseUnifiedMeshForDepth)
                        {
                            const int32 NumBatches = GetNumMeshBatches();

                            PDI->ReserveMemoryForMeshes(NumBatches);

                            for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
                            {
                                FMeshBatch MeshBatch;

                                if (GetShadowMeshElement(LODIndex, BatchIndex, PrimitiveDPG, MeshBatch, bAllSectionsUseDitheredLODTransition))
                                {
                                    bUseUnifiedMeshForShadow = bAllSectionsCastShadow;

                                    MeshBatch.CastShadow = bUseUnifiedMeshForShadow;
                                    MeshBatch.bUseForDepthPass = bUseUnifiedMeshForDepth;
                                    MeshBatch.bUseAsOccluder = bUseUnifiedMeshForDepth;
                                    MeshBatch.bUseForMaterial = false;

                                    PDI->DrawMesh(MeshBatch, ScreenSize);
                                    //以下是我添加的代码
                                    const FLODInfo& ProxyLODInfo = LODs[LODIndex];
                                    UMaterialInterface* MaterialInterface = ProxyLODInfo.Sections[0].Material;

                                    const UStrokeStaticMeshComponent* StrokeStaticMeshComponent = dynamic_cast<const UStrokeStaticMeshComponent *>(ComponentPtr);
                                    if (MaterialInterface == StrokeStaticMeshComponent->SecondPassMaterial)
                                    {
                                        continue;
                                    }
                                    if (StrokeStaticMeshComponent->NeedSecondPass) {
                                        if (StrokeStaticMeshComponent->SecondPassMaterial == nullptr) {
                                            continue;
                                        }
                                        //通过MeshBatch设置Material与反转剔除选项
                                        MeshBatch.MaterialRenderProxy = StrokeStaticMeshComponent->SecondPassMaterial->GetRenderProxy();
                                        //设置反转剔除选项
                                        MeshBatch.ReverseCulling = true;
                                        PDI->DrawMesh(MeshBatch, FLT_MAX);
                                    }
                                }
                            }
                        }
                    }
                }

                // Draw the static mesh elements.
                for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
                {
#if WITH_EDITOR
                    if (GIsEditor)
                    {
                        const FLODInfo::FSectionInfo& Section = LODs[LODIndex].Sections[SectionIndex];

                        bIsMeshElementSelected = Section.bSelected;
                        PDI->SetHitProxy(Section.HitProxy);
                    }
#endif // WITH_EDITOR

                    const int32 NumBatches = GetNumMeshBatches();

                    PDI->ReserveMemoryForMeshes(NumBatches);

                    for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
                    {
                        FMeshBatch MeshBatch;

                        if (GetMeshElement(LODIndex, BatchIndex, SectionIndex, PrimitiveDPG, bIsMeshElementSelected, true, MeshBatch))
                        {
                            // If we have submitted an optimized shadow-only mesh, remaining mesh elements must not cast shadows.
                            MeshBatch.CastShadow &= !bUseUnifiedMeshForShadow;
                            MeshBatch.bUseAsOccluder &= !bUseUnifiedMeshForDepth;
                            MeshBatch.bUseForDepthPass &= !bUseUnifiedMeshForDepth;

                            PDI->DrawMesh(MeshBatch, ScreenSize);
                            //以下是我添加的代码
                            const FLODInfo& ProxyLODInfo = LODs[LODIndex];
                            UMaterialInterface* MaterialInterface = ProxyLODInfo.Sections[SectionIndex].Material;

                            const UStrokeStaticMeshComponent* StrokeStaticMeshComponent = dynamic_cast<const UStrokeStaticMeshComponent *>(ComponentPtr);
                            if (MaterialInterface == StrokeStaticMeshComponent->SecondPassMaterial)
                            {
                                continue;
                            }
                            if (StrokeStaticMeshComponent->NeedSecondPass) {
                                if (StrokeStaticMeshComponent->SecondPassMaterial == nullptr) {
                                    continue;
                                }
                                MeshBatch.MaterialRenderProxy = StrokeStaticMeshComponent->SecondPassMaterial->GetRenderProxy();
                                //
                                MeshBatch.ReverseCulling = true;
                                PDI->DrawMesh(MeshBatch, FLT_MAX);
                            }
                        }
                    }
                }
            }
        }
    }
}

插件地址及使用方法

插件地址

https://github.com/blueroseslol/BRPlugins

使用方法

  1. 创建一个Actor蓝图。
  2. 创建你定义的StaticMeshComponent。
  3. 赋予轮廓材质,并勾选NeedSecondPass。


使用这个材质的效果: