--- title: 基于插件的StaticMesh多Pass绘制方案 date: 2022-11-04 10:26:59 excerpt: tags: rating: ⭐ --- ## 前言 StaticMesh与SkeletalMesh的实现方法比较类似,不过两者的绘制方式有很大的不同。不过庆幸的是,Static的MeshBatch设置在绘制函数的外面。 ## 思路说明 ### 创建自定义StaticMeshComponent 1. 继承UStaticMeshComponent,重写GetUsedMaterials与CreateSceneProxy函数。 2. 添加用于多pass渲染的UMaterialInterface指针。(NeedSecondPass变量其实可以不用,因为既然你要使用这个类,那肯定要启用这个功能) ```c++ #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& 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; }; ``` 3. 实现GetUsedMaterials与CreateSceneProxy函数,并包含相应头文件。 ### 头文件 ``` #include "StrokeStaticMeshSceneProxy.h" ``` ### GetUsedMaterials ```c++ void UStrokeStaticMeshComponent::GetUsedMaterials(TArray& OutMaterials, bool bGetDebugMaterials /*= false*/) const { if (GetStaticMesh() && GetStaticMesh()->RenderData) { TMap 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 ```c++ FPrimitiveSceneProxy* UStrokeStaticMeshComponent::CreateSceneProxy() { if (GetStaticMesh() == nullptr || GetStaticMesh()->RenderData == nullptr) { return nullptr; } const TIndirectArray& LODResources = GetStaticMesh()->RenderData->LODResources; if (LODResources.Num() == 0 || LODResources[FMath::Clamp(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。** 1. 继承FStaticMeshSceneProxy,重写DrawStaticElements函数。 2. 定义UStaticMeshComponent指针,用于获取自定义StaticMeshComponent的变量。(之后使用需要使用强制转换) ```c++ #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; }; ``` 3. 复制DrawStaticElements中所需函数的代码。 将Engine\Source\Runtime\Engine\Private\StaticMeshRender.cpp中的AllowShadowOnlyMesh、UseLightPropagationVolumeRT2函数代码复制到你定义的StaticMeshSceneProxy类所在的cpp文件中。 4. 实现DrawStaticElements函数,并包含头文件。 与SkeletalMesh的绘制函数不同,它有3处需要调用绘制函数的地方。其中有一处因为不存在lod所以代码有些不同,所以请不要在未测试的情况下使用本插件进行生产项目。 不过一个好消息是,我们可以在绘制前设置MeshBatch,从而完美结局描边问题。 ```c++ 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(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(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(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。 使用这个材质的效果: