--- title: 基于插件的SkeletalMesh多Pass绘制方案 date: 2022-11-04 10:25:07 excerpt: tags: rating: ⭐ --- >本文仅供抛转引玉,为大家提供一个可行思路。因为本人目前仅仅是个Ue4业余爱好者,手头没有含有Lod的骨骼模型做测试,所以请勿在未测试的情况下直接把本插件用于生产。 ## 前言 在阅读了 @白昼行姜暗夜摸王 的系列文章,我感觉通过改引擎的方式来实现模型外描边着实不太方便。在仔细阅读代码后,发现文章中修改代码的相关函数,不是虚函数就是包含在虚函数中。于是乎我就感觉可以通过插件的方式来实现多pass的绘制方案,便开始尝试。 另外感谢@大钊 @YivanLee 在我尝试过程中给予的帮助。 ## 思路说明 >在重写函数的过程中,因为引擎部分类是定义在cpp中,以及部分函数因为没有ENGINE_API或是Private导致无法无法使用。有一个lod更新的逻辑我实在没办法绕过,所以就删除了。所以这个插件对于有lod的模型可能会有问题,解决思路我会说。 ### 创建自定义SkeletalMeshComponent类 1. 继承USkeletalMeshComponent,重写GetUsedMaterials与CreateSceneProxy函数。 2. 添加用于多pass渲染的UMaterialInterface指针。(NeedSecondPass变量其实可以不用,因为既然你要使用这个类,那肯定要启用这个功能) ```c++ #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& 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; }; ``` 3. 实现GetUsedMaterials与CreateSceneProxy函数,并包含相应头文件。 ### 头文件 ```c++ #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就可以解决问题。 ```c++ void UStrokeSkeletalMeshComponent::GetUsedMaterials(TArray& 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就可以了 ```c++ 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的变量。(之后使用需要使用强制转换) ```c++ #pragma once #include "Engine/Public/SkeletalMeshTypes.h" class FStrokeSkeletalMeshSceneProxy : public FSkeletalMeshSceneProxy { public: //在UStrokeSkeletalMeshComponent中调用这个构造函数初始化 FStrokeSkeletalMeshSceneProxy(const USkinnedMeshComponent* Component, FSkeletalMeshRenderData* InSkelMeshRenderData); virtual void GetDynamicMeshElements(const TArray& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override; private: UPROPERTY() const USkinnedMeshComponent* ComponentPtr; }; ``` ```c++ FStrokeSkeletalMeshSceneProxy::FStrokeSkeletalMeshSceneProxy(const USkinnedMeshComponent* Component, FSkeletalMeshRenderData* InSkelMeshRenderData): FSkeletalMeshSceneProxy(Component,InSkelMeshRenderData),ComponentPtr(Component) {} ``` 3. 实现GetDynamicMeshElements函数,并包含头文件。 ### 头文件 ```c++ #include "StrokeSkeletalMeshSceneProxy.h" #include "Engine/Public/SkeletalMeshTypes.h" #include "Engine/Public/TessellationRendering.h" #include "Engine/Public/SkeletalRenderPublic.h" ``` ### GetDynamicMeshElements ```c++ void FStrokeSkeletalMeshSceneProxy::GetDynamicMeshElements(const TArray& 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(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& 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。 下面的代码我没测试过,仅为说明意思,很多地方是父类运行过的逻辑,可以删除。 ```c++ 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(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); } } } ``` 4. 解决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,用于存储各个Pass的材质,之后再自己定义的SceneProxy中进行for循环绘制,不就实现类似unity的多pass材质系统了。 至少StaticMesh因为MeshBatch的设置过程,并没有在绘制函数中,所以我们可以任意修改MeshBatch设置,而可以完美实现上述猜想。下一篇文章我将介绍StaticMesh的实现方法。