本文仅供抛转引玉,为大家提供一个可行思路。因为本人目前仅仅是个Ue4业余爱好者,手头没有含有Lod的骨骼模型做测试,所以请勿在未测试的情况下直接把本插件用于生产。
前言
在阅读了 @白昼行姜暗夜摸王 的系列文章,我感觉通过改引擎的方式来实现模型外描边着实不太方便。在仔细阅读代码后,发现文章中修改代码的相关函数,不是虚函数就是包含在虚函数中。于是乎我就感觉可以通过插件的方式来实现多pass的绘制方案,便开始尝试。
另外感谢@大钊 @YivanLee 在我尝试过程中给予的帮助。
思路说明
在重写函数的过程中,因为引擎部分类是定义在cpp中,以及部分函数因为没有ENGINE_API或是Private导致无法无法使用。有一个lod更新的逻辑我实在没办法绕过,所以就删除了。所以这个插件对于有lod的模型可能会有问题,解决思路我会说。
创建自定义SkeletalMeshComponent类
- 继承USkeletalMeshComponent,重写GetUsedMaterials与CreateSceneProxy函数。
- 添加用于多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;
};
- 实现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类
- 继承FSkeletalMeshSceneProxy,重写GetDynamicMeshElements函数。
- 定义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)
{}
- 实现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);
}
}
}
- 解决FSkeletalMeshSectionIter未定义的报错问题。(原因在开头说了)
在Engine\Source\Runtime\Engine\Private\SkeletalMesh.cpp中找到FSkeletalMeshSectionIter类,并将所有代码复制到你定义的SkeletalMeshSceneProxy所在的cpp文件中。
完成以上步骤你就得到一个2个pass的SkeletalMeshComponent。
关于绘制轮廓的问题
@白昼行姜暗夜摸王 的文章采用了使用了绘制背面的方法来得到轮廓,但是很可惜,因为以下原因,我无法在不修改源码的情况下实现这个方法:
- SkeletalMeshSceneProxy的MeshBatch设置逻辑在绘制函数中。
- 因为绘制函数有很多核心函数因为没倒出或是private,所以无法重写。
- 判断剔除的IsLocalToWorldDeterminantNegative()函数不是虚函数。
- bIsLocalToWorldDeterminantNegative是父类的private变量,无法修改。
当然可以使用一些c++黑科技来突破(3、4),不过我暂时没有那种技术。
曲线救国方法
绘制背面本质上还是一种深度偏移,所以我们可以采用深度偏移的方法来解决。
材质我没有仔细调,所以在离模型较远的地方会显得有点“脏”,只需要在Mask与深度偏移添加模型距离的判断来,进行参数调整就完美。
插件地址及使用方法
插件地址
https://github.com/blueroseslol/BRPlugins
使用方法
- 创建一个Actor蓝图。
- 创建你定义的SkeletalMeshComponent。
- 赋予轮廓材质,并勾选NeedSecondPass。
延伸思考
我觉得可以通过在自定义的Component类中定义一个TArray
至少StaticMesh因为MeshBatch的设置过程,并没有在绘制函数中,所以我们可以任意修改MeshBatch设置,而可以完美实现上述猜想。下一篇文章我将介绍StaticMesh的实现方法。