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

16 KiB
Raw Blame History

title, date, excerpt, tags, rating
title date excerpt tags rating
基于插件的SkeletalMesh多Pass绘制方案 2022-11-04 10:25:07

本文仅供抛转引玉为大家提供一个可行思路。因为本人目前仅仅是个Ue4业余爱好者手头没有含有Lod的骨骼模型做测试所以请勿在未测试的情况下直接把本插件用于生产。

前言

在阅读了 @白昼行姜暗夜摸王 的系列文章我感觉通过改引擎的方式来实现模型外描边着实不太方便。在仔细阅读代码后发现文章中修改代码的相关函数不是虚函数就是包含在虚函数中。于是乎我就感觉可以通过插件的方式来实现多pass的绘制方案便开始尝试。

另外感谢@大钊 @YivanLee 在我尝试过程中给予的帮助。

思路说明

在重写函数的过程中因为引擎部分类是定义在cpp中以及部分函数因为没有ENGINE_API或是Private导致无法无法使用。有一个lod更新的逻辑我实在没办法绕过所以就删除了。所以这个插件对于有lod的模型可能会有问题解决思路我会说。

创建自定义SkeletalMeshComponent类

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

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

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

	virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
	//~ End UPrimitiveComponent Interface

	//virtual UMaterialInterface* GetMaterial(int32 MaterialIndex) const override;
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiplePass")
	UMaterialInterface* SecondPassMaterial = nullptr;

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

头文件

#include "StrokeSkeletalMeshComponent.h"
#include "StrokeSkeletalMeshSceneProxy.h"
#include "Engine/Public/Rendering/SkeletalMeshRenderData.h"
#include "Engine/Public/SkeletalRenderPublic.h"

GetUsedMaterials

自定义的SkeletalMeshComponent只运行OutMaterials.Add(SecondPassMaterial);会导致引擎找不到Material的ShaderMap。在尝试之处我追踪了Skeletal的MeshDraw绘制流程发现竟然是VertexFactory指针为空造成的。所以我判断应该是自定义的Component没有把Material进行注册造成的问题。

之后我尝试了手动将MaterialInterface的指针添加到SkeletalMesh的Lod数组中的material。虽然解决了问题但是遇到重复修改会导致组件栏里多出一堆材质asset显示虽然之后通过判断slotname进行数组管理解决了问题但是结果还是不完美。

不过之后在继续查看了源代码后我发现只要调用UpdateUVChannelData就可以解决问题。

void UStrokeSkeletalMeshComponent::GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials /*= false*/) const
{
	if (SkeletalMesh)
	{
		// The max number of materials used is the max of the materials on the skeletal mesh and the materials on the mesh component
		const int32 NumMaterials = FMath::Max(SkeletalMesh->Materials.Num(), OverrideMaterials.Num());
		for (int32 MatIdx = 0; MatIdx < NumMaterials; ++MatIdx)
		{
			// GetMaterial will determine the correct material to use for this index.  

			UMaterialInterface* MaterialInterface = GetMaterial(MatIdx);
			OutMaterials.Add(MaterialInterface);
		}
	    //这里是我添加的代码
		if (NeedSecondPass)
		{
			OutMaterials.Add(SecondPassMaterial);
			SkeletalMesh->UpdateUVChannelData(false);
		}
	}

	if (bGetDebugMaterials)
	{
#if WITH_EDITOR
        //这段代码无法绕过,而且因为不太重要,就直接注释掉了
		//if (UPhysicsAsset* PhysicsAssetForDebug = GetPhysicsAsset())
		//{
		//	PhysicsAssetForDebug->GetUsedMaterials(OutMaterials);
		//}
#endif
	}
}

CreateSceneProxy

CreateSceneProxy比较简单只需要创建自己的自定义Proxy就可以了

FPrimitiveSceneProxy* UStrokeSkeletalMeshComponent::CreateSceneProxy()
{
	ERHIFeatureLevel::Type SceneFeatureLevel = GetWorld()->FeatureLevel;

    //定义自定义Proxy类指针
	FStrokeSkeletalMeshSceneProxy* Result = nullptr;
	//FSkeletalMeshSceneProxy* Result = nullptr;
	FSkeletalMeshRenderData* SkelMeshRenderData = GetSkeletalMeshRenderData();

	// Only create a scene proxy for rendering if properly initialized
	if (SkelMeshRenderData &&
		SkelMeshRenderData->LODRenderData.IsValidIndex(PredictedLODLevel) &&
		!bHideSkin &&
		MeshObject)
	{
		// Only create a scene proxy if the bone count being used is supported, or if we don't have a skeleton (this is the case with destructibles)
		int32 MaxBonesPerChunk = SkelMeshRenderData->GetMaxBonesPerSection();
		int32 MaxSupportedNumBones = MeshObject->IsCPUSkinned() ? MAX_int32 : GetFeatureLevelMaxNumberOfBones(SceneFeatureLevel);
		if (MaxBonesPerChunk <= MaxSupportedNumBones)
		{
		    //使用new创建指针对象
			Result = ::new FStrokeSkeletalMeshSceneProxy(this, SkelMeshRenderData);
		}
	}

#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
	SendRenderDebugPhysics(Result);
#endif
	return Result;
}

创建自定义SkeletalMeshSceneProxy类

  1. 继承FSkeletalMeshSceneProxy,重写GetDynamicMeshElements函数。
  2. 定义USkinnedMeshComponent指针用于获取自定义SkeletalMeshComponent的变量。之后使用需要使用强制转换
#pragma once
#include "Engine/Public/SkeletalMeshTypes.h"

class FStrokeSkeletalMeshSceneProxy : public FSkeletalMeshSceneProxy
{
public:
	//在UStrokeSkeletalMeshComponent中调用这个构造函数初始化
	FStrokeSkeletalMeshSceneProxy(const USkinnedMeshComponent* Component, FSkeletalMeshRenderData* InSkelMeshRenderData);

	virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override;
private:
	UPROPERTY()
	const USkinnedMeshComponent* ComponentPtr;
};
FStrokeSkeletalMeshSceneProxy::FStrokeSkeletalMeshSceneProxy(const USkinnedMeshComponent* Component, FSkeletalMeshRenderData* InSkelMeshRenderData):
	FSkeletalMeshSceneProxy(Component,InSkelMeshRenderData),ComponentPtr(Component)
{}
  1. 实现GetDynamicMeshElements函数并包含头文件。

头文件

#include "StrokeSkeletalMeshSceneProxy.h"
#include "Engine/Public/SkeletalMeshTypes.h"
#include "Engine/Public/TessellationRendering.h"
#include "Engine/Public/SkeletalRenderPublic.h"

GetDynamicMeshElements

void FStrokeSkeletalMeshSceneProxy::GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const
{
	QUICK_SCOPE_CYCLE_COUNTER(STAT_FStrokeSkeletalMeshSceneProxy_GetMeshElements);
	
	if (!MeshObject)
	{
		return;
	}
	MeshObject->PreGDMECallback(ViewFamily.Scene->GetGPUSkinCache(), ViewFamily.FrameNumber);
	
	//这句代码无法绕过,所以只能注释掉了
	/*
	for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
	{
		if (VisibilityMap & (1 << ViewIndex))
		{
			const FSceneView* View = Views[ViewIndex];
			MeshObject->UpdateMinDesiredLODLevel(View, GetBounds(), ViewFamily.FrameNumber);
		}
	}
	*/
	const FEngineShowFlags& EngineShowFlags = ViewFamily.EngineShowFlags;

	const int32 LODIndex = MeshObject->GetLOD();
	check(LODIndex < SkeletalMeshRenderData->LODRenderData.Num());
	const FSkeletalMeshLODRenderData& LODData = SkeletalMeshRenderData->LODRenderData[LODIndex];

	if (LODSections.Num() > 0)
	{
		const FLODSectionElements& LODSection = LODSections[LODIndex];

		check(LODSection.SectionElements.Num() == LODData.RenderSections.Num());

		for (FSkeletalMeshSectionIter Iter(LODIndex, *MeshObject, LODData, LODSection); Iter; ++Iter)
		{
			const FSkelMeshRenderSection& Section = Iter.GetSection();
			const int32 SectionIndex = Iter.GetSectionElementIndex();
			const FSectionElementInfo& SectionElementInfo = Iter.GetSectionElementInfo();

			bool bSectionSelected = false;

#if WITH_EDITORONLY_DATA
			// TODO: This is not threadsafe! A render command should be used to propagate SelectedEditorSection to the scene proxy.
			if (MeshObject->SelectedEditorMaterial != INDEX_NONE)
			{
				bSectionSelected = (MeshObject->SelectedEditorMaterial == SectionElementInfo.UseMaterialIndex);
			}
			else
			{
				bSectionSelected = (MeshObject->SelectedEditorSection == SectionIndex);
			}

#endif
            //下面函数的代码
			// If hidden skip the draw
			check(MeshObject->LODInfo.IsValidIndex(LODIndex));
			bool bHide= MeshObject->LODInfo[LODIndex].HiddenMaterials.IsValidIndex(SectionElementInfo.UseMaterialIndex) && MeshObject->LODInfo[LODIndex].HiddenMaterials[SectionElementInfo.UseMaterialIndex];

			if (bHide || Section.bDisabled)
			{
				continue;
			}
            //这个函数因为没有导出,但是比较简单所以上面就直接把代码复制出来了
			/* error LNK2019
			if (MeshObject->IsMaterialHidden(LODIndex, SectionElementInfo.UseMaterialIndex) || Section.bDisabled)
			{
				continue;
			}
			*/
			const UStrokeSkeletalMeshComponent* StrokeSkeletalMeshComponent = dynamic_cast<const UStrokeSkeletalMeshComponent *>(ComponentPtr);
			if (SectionElementInfo.Material== StrokeSkeletalMeshComponent->SecondPassMaterial)
			{
				continue;
			}
			GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, SectionElementInfo, true, Collector);
			
			//进行第二个pass绘制
			//关键点在于修改FSectionElementInfo中的Material并且再次调用绘制函数GetDynamicElementsSection
			if (StrokeSkeletalMeshComponent->NeedSecondPass) {
				FSectionElementInfo Info = FSectionElementInfo(SectionElementInfo);
				if (StrokeSkeletalMeshComponent->SecondPassMaterial == nullptr) {
					continue;
				}
				Info.Material = StrokeSkeletalMeshComponent->SecondPassMaterial;
				GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, Info, true, Collector);
			}
		}
	}
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
	for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
	{
		if (VisibilityMap & (1 << ViewIndex))
		{
			if (PhysicsAssetForDebug)
			{
				DebugDrawPhysicsAsset(ViewIndex, Collector, ViewFamily.EngineShowFlags);
			}

			if (EngineShowFlags.MassProperties && DebugMassData.Num() > 0)
			{
				FPrimitiveDrawInterface* PDI = Collector.GetPDI(ViewIndex);
				if (MeshObject->GetComponentSpaceTransforms())
				{
					const TArray<FTransform>& ComponentSpaceTransforms = *MeshObject->GetComponentSpaceTransforms();

					for (const FDebugMassData& DebugMass : DebugMassData)
					{
						if (ComponentSpaceTransforms.IsValidIndex(DebugMass.BoneIndex))
						{
							const FTransform BoneToWorld = ComponentSpaceTransforms[DebugMass.BoneIndex] * FTransform(GetLocalToWorld());
							DebugMass.DrawDebugMass(PDI, BoneToWorld);
						}
					}
				}
			}

			if (ViewFamily.EngineShowFlags.SkeletalMeshes)
			{
				RenderBounds(Collector.GetPDI(ViewIndex), ViewFamily.EngineShowFlags, GetBounds(), IsSelected());
			}

			if (ViewFamily.EngineShowFlags.Bones)
			{
				DebugDrawSkeleton(ViewIndex, Collector, ViewFamily.EngineShowFlags);
			}
		}
	}
#endif
}

UpdateMinDesiredLODLevel函数处的代码被我注释掉了大致的解决方法为调用父类的GetDynamicMeshElements函数再运行一次GetDynamicElementsSection绘制自己的pass。

下面的代码我没测试过,仅为说明意思,很多地方是父类运行过的逻辑,可以删除。

QUICK_SCOPE_CYCLE_COUNTER(STAT_FStrokeSkeletalMeshSceneProxy_GetMeshElements);
//运行父类函数
FStrokeSkeletalMeshSceneProxy::GetDynamicMeshElements(Views,ViewFamily,VisibilityMap,Collector);

const FEngineShowFlags& EngineShowFlags = ViewFamily.EngineShowFlags;

const int32 LODIndex = MeshObject->GetLOD();
check(LODIndex < SkeletalMeshRenderData->LODRenderData.Num());
const FSkeletalMeshLODRenderData& LODData = SkeletalMeshRenderData->LODRenderData[LODIndex];

if (LODSections.Num() > 0)
{
	const FLODSectionElements& LODSection = LODSections[LODIndex];

	check(LODSection.SectionElements.Num() == LODData.RenderSections.Num());

	for (FSkeletalMeshSectionIter Iter(LODIndex, *MeshObject, LODData, LODSection); Iter; ++Iter)
	{
		const FSkelMeshRenderSection& Section = Iter.GetSection();
		const int32 SectionIndex = Iter.GetSectionElementIndex();
		const FSectionElementInfo& SectionElementInfo = Iter.GetSectionElementInfo();

		bool bSectionSelected = false;

#if WITH_EDITORONLY_DATA
		// TODO: This is not threadsafe! A render command should be used to propagate SelectedEditorSection to the scene proxy.
		if (MeshObject->SelectedEditorMaterial != INDEX_NONE)
		{
			bSectionSelected = (MeshObject->SelectedEditorMaterial == SectionElementInfo.UseMaterialIndex);
		}
		else
		{
			bSectionSelected = (MeshObject->SelectedEditorSection == SectionIndex);
		}

#endif
        //下面函数的代码
		// If hidden skip the draw
		check(MeshObject->LODInfo.IsValidIndex(LODIndex));
		bool bHide= MeshObject->LODInfo[LODIndex].HiddenMaterials.IsValidIndex(SectionElementInfo.UseMaterialIndex) && MeshObject->LODInfo[LODIndex].HiddenMaterials[SectionElementInfo.UseMaterialIndex];

		if (bHide || Section.bDisabled)
		{
			continue;
		}
		const UStrokeSkeletalMeshComponent* StrokeSkeletalMeshComponent = dynamic_cast<const UStrokeSkeletalMeshComponent *>(ComponentPtr);
		if (SectionElementInfo.Material== StrokeSkeletalMeshComponent->SecondPassMaterial)
		{
			continue;
		}
		
		//进行第二个pass绘制
		//关键点在于修改FSectionElementInfo中的Material并且再次调用绘制函数GetDynamicElementsSection
		if (StrokeSkeletalMeshComponent->NeedSecondPass) {
			FSectionElementInfo Info = FSectionElementInfo(SectionElementInfo);
			if (StrokeSkeletalMeshComponent->SecondPassMaterial == nullptr) {
				continue;
			}
			Info.Material = StrokeSkeletalMeshComponent->SecondPassMaterial;
			GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, Info, true, Collector);
		}
	}
}
  1. 解决FSkeletalMeshSectionIter未定义的报错问题。(原因在开头说了)

在Engine\Source\Runtime\Engine\Private\SkeletalMesh.cpp中找到FSkeletalMeshSectionIter类并将所有代码复制到你定义的SkeletalMeshSceneProxy所在的cpp文件中。

完成以上步骤你就得到一个2个pass的SkeletalMeshComponent。

关于绘制轮廓的问题

@白昼行姜暗夜摸王 的文章采用了使用了绘制背面的方法来得到轮廓,但是很可惜,因为以下原因,我无法在不修改源码的情况下实现这个方法:

  1. SkeletalMeshSceneProxy的MeshBatch设置逻辑在绘制函数中。
  2. 因为绘制函数有很多核心函数因为没倒出或是private所以无法重写。
  3. 判断剔除的IsLocalToWorldDeterminantNegative()函数不是虚函数。
  4. bIsLocalToWorldDeterminantNegative是父类的private变量无法修改。

当然可以使用一些c++黑科技来突破3、4不过我暂时没有那种技术。

曲线救国方法

绘制背面本质上还是一种深度偏移,所以我们可以采用深度偏移的方法来解决。

材质我没有仔细调所以在离模型较远的地方会显得有点“脏”只需要在Mask与深度偏移添加模型距离的判断来进行参数调整就完美。

插件地址及使用方法

插件地址

https://github.com/blueroseslol/BRPlugins

使用方法

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

延伸思考

我觉得可以通过在自定义的Component类中定义一个TArray<MaterialInterface*>用于存储各个Pass的材质之后再自己定义的SceneProxy中进行for循环绘制不就实现类似unity的多pass材质系统了。

至少StaticMesh因为MeshBatch的设置过程并没有在绘制函数中所以我们可以任意修改MeshBatch设置而可以完美实现上述猜想。下一篇文章我将介绍StaticMesh的实现方法。