BlueRoseNote/03-UnrealEngine/Plugins/基于插件的StaticMesh多Pass绘制方案.md
2023-06-29 11:55:02 +08:00

373 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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<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;
};
```
3. 实现GetUsedMaterials与CreateSceneProxy函数并包含相应头文件。
### 头文件
```
#include "StrokeStaticMeshSceneProxy.h"
```
### GetUsedMaterials
```c++
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
```c++
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。**
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<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。
使用这个材质的效果: