BlueRose
文章97
标签28
分类7
基于插件的SkeletalMesh多Pass绘制方案

基于插件的SkeletalMesh多Pass绘制方案

本文仅供抛转引玉,为大家提供一个可行思路。因为本人目前仅仅是个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。

关于绘制轮廓的问题

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

  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的实现方法。