373 lines
14 KiB
Markdown
373 lines
14 KiB
Markdown
|
---
|
|||
|
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。
|
|||
|
|
|||
|
使用这个材质的效果:
|