前言
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
使用方法
- 创建一个Actor蓝图。
- 创建你定义的StaticMeshComponent。
- 赋予轮廓材质,并勾选NeedSecondPass。
使用这个材质的效果: