UE – ProceduralMeshComponent 分析

引擎: UE4.26.2 构建版本

ProceduralMeshComponent 简称 PMC ,提供一个程序化网格组件,可以通过自定义的三角形制作可渲染网格,使用动态渲染路径,在每帧收集渲染数据,没有任何数据缓存。

类结构

游戏 类图

渲染器 类图

简介

  • UActorComponent – 所有组件的基类。
  • USceneComponent – 带有 变换 属性的组件。
  • UPrimitiveComponent – 图元组件的基类,表示组件带有可渲染的图元。
  • FPrimitiveSceneProxy – 图元组件的 渲染器 代表,镜像组件渲染数据以进行多线程渲染。
  • UMeshComponent – 网格组件的基类,表示组件带有可渲染的网格图元。
  • Interface_CollisionDataProvider – 碰撞数据接口,表示对象具有碰撞数据。
  • UProceduralMeshComponent – PMC 组件,整个系统的核心,提供自定义三角面集的接口。
  • FProceduralMeshSceneProxy – PMC 的 渲染器 代表,镜像 PMC 数据。
  • FProcMeshSection – PMC 网格分段,分段可独立设置 材质 碰撞 可见性 。
  • FProcMeshProxySection – PMC 网格分段的 渲染器 代表,镜像网格分段数据。
  • FProcMeshVertex – PMC 顶点,一个顶点集和三角形索引构成 PMC 网格分段。
  • FProcMeshTangent – PMC 切线,每个顶点都有独立的切线,其中 Z-切线 为法线, Y-切线 动态计算。

流程

分段创建流程

PMC 最常用的接口,也是第一个学习的接口是 分段创建 接口,功能是在网格上 创建/更新 一个分段的数据,主要流程是通过参数独立获取分段的 顶点 三角索引 颜色 UV 法线 切线 等,组装为 PMC 内部数据,开放具有以下签名的函数:

// Engine\Plugins\Runtime\ProceduralMeshComponent\Source\ProceduralMeshComponent\Public\ProceduralMeshComponent.h

void CreateMeshSection(int32 SectionIndex, const TArray<FVector>& Vertices, const TArray<int32>& Triangles, const TArray<FVector>& Normals, const TArray<FVector2D>& UV0, const TArray<FColor>& VertexColors, const TArray<FProcMeshTangent>& Tangents, bool bCreateCollision);
void CreateMeshSection(int32 SectionIndex, const TArray<FVector>& Vertices, const TArray<int32>& Triangles, const TArray<FVector>& Normals, const TArray<FVector2D>& UV0, const TArray<FVector2D>& UV1, const TArray<FVector2D>& UV2, const TArray<FVector2D>& UV3, const TArray<FColor>& VertexColors, const TArray<FProcMeshTangent>& Tangents, bool bCreateCollision);
void CreateMeshSection_LinearColor(int32 SectionIndex, const TArray<FVector>& Vertices, const TArray<int32>& Triangles, const TArray<FVector>& Normals, const TArray<FVector2D>& UV0, const TArray<FVector2D>& UV1, const TArray<FVector2D>& UV2, const TArray<FVector2D>& UV3, const TArray<FLinearColor>& VertexColors, const TArray<FProcMeshTangent>& Tangents, bool bCreateCollision);
void CreateMeshSection_LinearColor(int32 SectionIndex, const TArray<FVector>& Vertices, const TArray<int32>& Triangles, const TArray<FVector>& Normals, const TArray<FVector2D>& UV0, const TArray<FLinearColor>& VertexColors, const TArray<FProcMeshTangent>& Tangents, bool bCreateCollision)

但是最终都会被转发到第二个函数,其实现为。

// Engine\Plugins\Runtime\ProceduralMeshComponent\Source\ProceduralMeshComponent\Private\ProceduralMeshComponent.cpp

void UProceduralMeshComponent::CreateMeshSection(int32 SectionIndex, const TArray<FVector>& Vertices, const TArray<int32>& Triangles, const TArray<FVector>& Normals, const TArray<FVector2D>& UV0, const TArray<FVector2D>& UV1, const TArray<FVector2D>& UV2, const TArray<FVector2D>& UV3, const TArray<FColor>& VertexColors, const TArray<FProcMeshTangent>& Tangents, bool bCreateCollision)
{
    // 声明作用域时钟计数器 用于性能分析
    SCOPE_CYCLE_COUNTER(STAT_ProcMesh_CreateMeshSection);

    // 确保 ProcMeshSections 数组有足够的空间
    if (SectionIndex >= ProcMeshSections.Num())
    {
        ProcMeshSections.SetNum(SectionIndex + 1, false);
    }

    // 如果分段已经被创建 则清空以前的数据
    FProcMeshSection& NewSection = ProcMeshSections[SectionIndex];
    NewSection.Reset();

    // 复制顶点数据 无效数据用默认值填充
    const int32 NumVerts = Vertices.Num();
    NewSection.ProcVertexBuffer.Reset();
    NewSection.ProcVertexBuffer.AddUninitialized(NumVerts);
    for (int32 VertIdx = 0; VertIdx < NumVerts; VertIdx++)
    {
        FProcMeshVertex& Vertex = NewSection.ProcVertexBuffer[VertIdx];

        Vertex.Position = Vertices[VertIdx];
        Vertex.Normal = (Normals.Num() == NumVerts) ? Normals[VertIdx] : FVector(0.f, 0.f, 1.f);
        Vertex.UV0 = (UV0.Num() == NumVerts) ? UV0[VertIdx] : FVector2D(0.f, 0.f);
        Vertex.UV1 = (UV1.Num() == NumVerts) ? UV1[VertIdx] : FVector2D(0.f, 0.f);
        Vertex.UV2 = (UV2.Num() == NumVerts) ? UV2[VertIdx] : FVector2D(0.f, 0.f);
        Vertex.UV3 = (UV3.Num() == NumVerts) ? UV3[VertIdx] : FVector2D(0.f, 0.f);
        Vertex.Color = (VertexColors.Num() == NumVerts) ? VertexColors[VertIdx] : FColor(255, 255, 255);
        Vertex.Tangent = (Tangents.Num() == NumVerts) ? Tangents[VertIdx] : FProcMeshTangent();

        // 同时重新计算边界框
        NewSection.SectionLocalBox += Vertex.Position;
    }

    // 复制三角索引数据
    int32 NumTriIndices = Triangles.Num();
    NumTriIndices = (NumTriIndices/3) * 3; // 确保三角形数量合法 即索引数据为3的整数倍

    NewSection.ProcIndexBuffer.Reset();
    NewSection.ProcIndexBuffer.AddUninitialized(NumTriIndices);
    for (int32 IndexIdx = 0; IndexIdx < NumTriIndices; IndexIdx++)
    {
        // 这里钳制索引范围 防止越界
        NewSection.ProcIndexBuffer[IndexIdx] = FMath::Min(Triangles[IndexIdx], NumVerts - 1);
    }

    NewSection.bEnableCollision = bCreateCollision;

    UpdateLocalBounds(); // 更新边界框
    UpdateCollision(); // 更新碰撞数据 见碰撞构建流程
    MarkRenderStateDirty(); // 表示渲染数据改变
}

可以看到此函数是将零散的三角面集整理成 PMC 分段数据,然后使用 MarkRenderStateDirty 通知渲染数据发生了改变,除了上面这种触发方式外,更新分段的材质也会触发下面的渲染器代表重建流程。

// Engine\Plugins\Runtime\ProceduralMeshComponent\Source\ProceduralMeshComponent\Private\ProceduralMeshComponent.cpp

// 通过此接口 图元组件获取特定的渲染器代表
FPrimitiveSceneProxy* UProceduralMeshComponent::CreateSceneProxy()
{
    SCOPE_CYCLE_COUNTER(STAT_ProcMesh_CreateSceneProxy);

    return new FProceduralMeshSceneProxy(this);
}

class FProceduralMeshSceneProxy final : public FPrimitiveSceneProxy
{
public:

    (......)

    FProceduralMeshSceneProxy(UProceduralMeshComponent* Component)
        : FPrimitiveSceneProxy(Component)
        , BodySetup(Component->GetBodySetup())
        , MaterialRelevance(Component->GetMaterialRelevance(GetScene().GetFeatureLevel()))
    {
        // 复制分段
        const int32 NumSections = Component->ProcMeshSections.Num();
        Sections.AddZeroed(NumSections);
        for (int SectionIdx = 0; SectionIdx < NumSections; SectionIdx++)
        {
            FProcMeshSection& SrcSection = Component->ProcMeshSections[SectionIdx];
            if (SrcSection.ProcIndexBuffer.Num() > 0 && SrcSection.ProcVertexBuffer.Num() > 0)
            {
                FProcMeshProxySection* NewSection = new FProcMeshProxySection(GetScene().GetFeatureLevel());

                // 复制顶点缓冲区
                const int32 NumVerts = SrcSection.ProcVertexBuffer.Num();

                TArray<FDynamicMeshVertex> Vertices;
                Vertices.SetNumUninitialized(NumVerts);
                for (int VertIdx = 0; VertIdx < NumVerts; VertIdx++)
                {
                    const FProcMeshVertex& ProcVert = SrcSection.ProcVertexBuffer[VertIdx];
                    FDynamicMeshVertex& Vert = Vertices[VertIdx];
                    ConvertProcMeshToDynMeshVertex(Vert, ProcVert);
                }

                // 复制索引缓冲区
                NewSection->IndexBuffer.Indices = SrcSection.ProcIndexBuffer;

                // 初始化顶点缓冲区
                NewSection->VertexBuffers.InitFromDynamicVertex(&NewSection->VertexFactory, Vertices, 4);

                // 初始化所有渲染资源
                BeginInitResource(&NewSection->VertexBuffers.PositionVertexBuffer);
                BeginInitResource(&NewSection->VertexBuffers.StaticMeshVertexBuffer);
                BeginInitResource(&NewSection->VertexBuffers.ColorVertexBuffer);
                BeginInitResource(&NewSection->IndexBuffer);
                BeginInitResource(&NewSection->VertexFactory);

                // 设置材质数据
                NewSection->Material = Component->GetMaterial(SectionIdx);
                if (NewSection->Material == NULL)
                {
                    NewSection->Material = UMaterial::GetDefaultMaterial(MD_Surface);
                }

                // 设置可见性
                NewSection->bSectionVisible = SrcSection.bSectionVisible;

                // 保存到分段集
                Sections[SectionIdx] = NewSection;

#if RHI_RAYTRACING

                (......)

#endif
            }
        }
    }

    (......)

};

以上代码主要功能为将数据从 PMC 分段转换成代理版本,也就是渲染器数据, FProcMeshProxySection 类储存分段的渲染器数据,主要有以下几个成员类型,这和 SM 的渲染数据大致相同。

  • UMaterialInterface – 此分段使用的材质
  • FStaticMeshVertexBuffers – 顶点缓冲区集
  • FDynamicMeshIndexBuffer32 – 32位索引缓冲区
  • FLocalVertexFactory – 顶点工厂,封装顶点缓冲区

其中 顶点缓冲区集 有以下几个主要成员类型。

  • FStaticMeshVertexBuffer – 储存 切线 和 UV
  • FPositionVertexBuffer – 储存 位置
  • FColorVertexBuffer – 储存 颜色

顶点缓冲区集 是通过 InitFromDynamicVertex 函数进行初始化的,实现如下。

// Engine\Source\Runtime\Engine\Private\StaticMesh.cpp

void FStaticMeshVertexBuffers::InitFromDynamicVertex(FLocalVertexFactory* VertexFactory, TArray<FDynamicMeshVertex>& Vertices, uint32 NumTexCoords, uint32 LightMapIndex)
{
    check(NumTexCoords < MAX_STATIC_TEXCOORDS && NumTexCoords > 0);
    check(LightMapIndex < NumTexCoords);

    // 判断是否包含三角形
    if (Vertices.Num())
    {
        // 按所需大小初始化缓冲区
        PositionVertexBuffer.Init(Vertices.Num());
        StaticMeshVertexBuffer.Init(Vertices.Num(), NumTexCoords);
        ColorVertexBuffer.Init(Vertices.Num());

        for (int32 i = 0; i < Vertices.Num(); i++)
        {
            const FDynamicMeshVertex& Vertex = Vertices[i];

            // 设置顶点位置
            PositionVertexBuffer.VertexPosition(i) = Vertex.Position;

            // 设置顶点切线
            StaticMeshVertexBuffer.SetVertexTangents(i, Vertex.TangentX.ToFVector(), Vertex.GetTangentY(), Vertex.TangentZ.ToFVector());

            // 设置顶点 UV
            for (uint32 j = 0; j < NumTexCoords; j++)
            {
                StaticMeshVertexBuffer.SetVertexUV(i, j, Vertex.TextureCoordinate[j]);
            }

            //设置顶点颜色
            ColorVertexBuffer.VertexColor(i) = Vertex.Color;
        }
    }
    else
    {
        // 初始化一个不具有有效三角形的顶点缓冲区集

        PositionVertexBuffer.Init(1);
        StaticMeshVertexBuffer.Init(1, 1);
        ColorVertexBuffer.Init(1);

        PositionVertexBuffer.VertexPosition(0) = FVector(0, 0, 0);
        StaticMeshVertexBuffer.SetVertexTangents(0, FVector(1, 0, 0), FVector(0, 1, 0), FVector(0, 0, 1));
        StaticMeshVertexBuffer.SetVertexUV(0, 0, FVector2D(0, 0));
        ColorVertexBuffer.VertexColor(0) = FColor(1,1,1,1);
        NumTexCoords = 1;
        LightMapIndex = 0;
    }

    // 插入操作到渲染线程的命令队列
    FStaticMeshVertexBuffers* Self = this;
    ENQUEUE_RENDER_COMMAND(StaticMeshVertexBuffersLegacyInit)(
        [VertexFactory, Self, LightMapIndex](FRHICommandListImmediate& RHICmdList)
        {
            // 初始化顶点 RHI 资源
            InitOrUpdateResource(&Self->PositionVertexBuffer);
            InitOrUpdateResource(&Self->StaticMeshVertexBuffer);
            InitOrUpdateResource(&Self->ColorVertexBuffer);

            // 将各缓冲区绑定到顶点工厂
            FLocalVertexFactory::FDataType Data;
            Self->PositionVertexBuffer.BindPositionVertexBuffer(VertexFactory, Data);
            Self->StaticMeshVertexBuffer.BindTangentVertexBuffer(VertexFactory, Data);
            Self->StaticMeshVertexBuffer.BindPackedTexCoordVertexBuffer(VertexFactory, Data);
            Self->StaticMeshVertexBuffer.BindLightMapVertexBuffer(VertexFactory, Data, LightMapIndex);
            Self->ColorVertexBuffer.BindColorVertexBuffer(VertexFactory, Data);
            VertexFactory->SetData(Data);

            // 初始化顶点工厂 RHI 资源
            InitOrUpdateResource(VertexFactory);
        });
};

至此我们已经准备好所有渲染数据,下面就是等待渲染器生成渲染命令。

渲染整合流程

在渲染数据都准备就绪后,渲染器会在合适的时机在渲染线程调用一系列接口以获取渲染需要信息,在图元代理刚被建立时 FPrimitiveSceneInfo 会收集图元信息以供渲染器使用。

// Engine\Plugins\Runtime\ProceduralMeshComponent\Source\ProceduralMeshComponent\Private\ProceduralMeshComponent.cpp

class FProceduralMeshSceneProxy final : public FPrimitiveSceneProxy
{
public:

    (......)

    // 获取图元相关性信息
    virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const
    {
        FPrimitiveViewRelevance Result;
        Result.bDrawRelevance = IsShown(View);
        Result.bShadowRelevance = IsShadowCast(View);
        Result.bDynamicRelevance = true; // 动态绘制路径 打开
        Result.bRenderInMainPass = ShouldRenderInMainPass();
        Result.bUsesLightingChannels = GetLightingChannelMask() != GetDefaultLightingChannelMask();
        Result.bRenderCustomDepth = ShouldRenderCustomDepth();
        Result.bTranslucentSelfShadow = bCastVolumetricTranslucentShadow;
        MaterialRelevance.SetPrimitiveViewRelevance(Result);
        Result.bVelocityRelevance = IsMovable() && Result.bOpaque && Result.bRenderInMainPass;
        return Result;
    }

    // 是否能够被遮挡
    virtual bool CanBeOccluded() const override
    {
        return !MaterialRelevance.bDisableDepthTest;
    }

    (......)

};

从 Result.bDynamicRelevance = true 可以看出 PMC 使用的是 动态绘制路径 ,在 动态绘制路径 下,每帧引擎都会通过 GetDynamicMeshElements 重新获取图元信息,生成 RHI 命令。

// Engine\Plugins\Runtime\ProceduralMeshComponent\Source\ProceduralMeshComponent\Private\ProceduralMeshComponent.cpp

class FProceduralMeshSceneProxy final : public FPrimitiveSceneProxy
{
public:

    (......)

    // 每帧都会被调用 渲染器通过此函数获取图元渲染信息 
    virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override
    {
        SCOPE_CYCLE_COUNTER(STAT_ProcMesh_GetMeshElements);

        // 是否需要线框材质 在视口设置线框模式后为 true
        const bool bWireframe = AllowDebugViewmodes() && ViewFamily.EngineShowFlags.Wireframe;

        // 生成并注册线框材质
        FColoredMaterialRenderProxy* WireframeMaterialInstance = NULL;
        if (bWireframe)
        {
            WireframeMaterialInstance = new FColoredMaterialRenderProxy(
                GEngine->WireframeMaterial ? GEngine->WireframeMaterial->GetRenderProxy() : NULL,
                FLinearColor(0, 0.5f, 1.f)
                );

            Collector.RegisterOneFrameMaterialProxy(WireframeMaterialInstance);
        }

        // 遍历每个分段
        for (const FProcMeshProxySection* Section : Sections)
        {
            if (Section != nullptr && Section->bSectionVisible)
            {
                // 获取材质的渲染器代表
                FMaterialRenderProxy* MaterialProxy = bWireframe ? WireframeMaterialInstance : Section->Material->GetRenderProxy();

                // 遍历每个视口
                for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
                {
                    // 检测图元在当前视口是否可见
                    if (VisibilityMap & (1 << ViewIndex))
                    {
                        const FSceneView* View = Views[ViewIndex];

                        // 绘制网格
                        FMeshBatch& Mesh = Collector.AllocateMesh();

                        // 每个网格批次自带一个网格元素 所以可以直接获取
                        FMeshBatchElement& BatchElement = Mesh.Elements[0];
                        BatchElement.IndexBuffer = &Section->IndexBuffer;
                        Mesh.bWireframe = bWireframe;
                        Mesh.VertexFactory = &Section->VertexFactory;
                        Mesh.MaterialRenderProxy = MaterialProxy;

                        // 获取 Uniform 信息
                        bool bHasPrecomputedVolumetricLightmap;
                        FMatrix PreviousLocalToWorld;
                        int32 SingleCaptureIndex;
                        bool bOutputVelocity;
                        GetScene().GetPrimitiveUniformShaderParameters_RenderThread(GetPrimitiveSceneInfo(), bHasPrecomputedVolumetricLightmap, PreviousLocalToWorld, SingleCaptureIndex, bOutputVelocity);

                        // 设置 Uniform
                        FDynamicPrimitiveUniformBuffer& DynamicPrimitiveUniformBuffer = Collector.AllocateOneFrameResource<FDynamicPrimitiveUniformBuffer>();
                        DynamicPrimitiveUniformBuffer.Set(GetLocalToWorld(), PreviousLocalToWorld, GetBounds(), GetLocalBounds(), true, bHasPrecomputedVolumetricLightmap, DrawsVelocity(), bOutputVelocity);
                        BatchElement.PrimitiveUniformBufferResource = &DynamicPrimitiveUniformBuffer.UniformBuffer;

                        // 其他参数
                        BatchElement.FirstIndex = 0;
                        BatchElement.NumPrimitives = Section->IndexBuffer.Indices.Num() / 3;
                        BatchElement.MinVertexIndex = 0;
                        BatchElement.MaxVertexIndex = Section->VertexBuffers.PositionVertexBuffer.GetNumVertices() - 1;
                        Mesh.ReverseCulling = IsLocalToWorldDeterminantNegative();
                        Mesh.Type = PT_TriangleList;
                        Mesh.DepthPriorityGroup = SDPG_World;
                        Mesh.bCanApplyViewModeOverrides = false;

                        // 将网格批次添加到收集器
                        Collector.AddMesh(ViewIndex, Mesh);
                    }
                }
            }
        }

#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
        {
            if (VisibilityMap & (1 << ViewIndex))
            {    
                // 如果需要 显示碰撞 将简单的碰撞绘制为线框
                if (ViewFamily.EngineShowFlags.Collision && IsCollisionEnabled() && BodySetup->GetCollisionTraceFlag() != ECollisionTraceFlag::CTF_UseComplexAsSimple)
                {
                    FTransform GeomTransform(GetLocalToWorld());
                    BodySetup->AggGeom.GetAggGeom(GeomTransform, GetSelectionColor(FColor(157, 149, 223, 255), IsSelected(), IsHovered()).ToFColor(true), NULL, false, false, DrawsVelocity(), ViewIndex, Collector);
                }

                RenderBounds(Collector.GetPDI(ViewIndex), ViewFamily.EngineShowFlags, GetBounds(), IsSelected());
            }
        }
#endif
    }

    (......)

};

碰撞构建流程

在三角形数据或凸体网格更新后,将会调用 UpdateCollision 更新组件的碰撞数据。

// Engine\Plugins\Runtime\ProceduralMeshComponent\Source\ProceduralMeshComponent\Private\ProceduralMeshComponent.cpp

void UProceduralMeshComponent::UpdateCollision()
{
    SCOPE_CYCLE_COUNTER(STAT_ProcMesh_UpdateCollision);

    UWorld* World = GetWorld();
    const bool bUseAsyncCook = World && World->IsGameWorld() && bUseAsyncCooking;

    // 是否采用异步方式构建碰撞
    if(bUseAsyncCook)
    {
        // 中断正在异步构建的任务
        for (UBodySetup* OldBody : AsyncBodySetupQueue)
        {
            OldBody->AbortPhysicsMeshAsyncCreation();
        }

        // 新增一个异步构建任务
        AsyncBodySetupQueue.Add(CreateBodySetupHelper());
    }
    else
    {
        AsyncBodySetupQueue.Empty(); // 清空异步构建队列
        CreateProcMeshBodySetup(); // 确保 BodySetup 刚体构建器存在
    }

    UBodySetup* UseBodySetup = bUseAsyncCook ? AsyncBodySetupQueue.Last() : ProcMeshBodySetup;

    // 将凸体碰撞添加到 BodySetup
    UseBodySetup->AggGeom.ConvexElems = CollisionConvexElems;

    // 设置是否将网格碰撞当做简单碰撞使用 将会忽略上面的 ConvexElems
    UseBodySetup->CollisionTraceFlag = bUseComplexAsSimpleCollision ? CTF_UseComplexAsSimple : CTF_UseDefault;

    // 是否采用异步方式构建碰撞
    if(bUseAsyncCook)
    {
        UseBodySetup->CreatePhysicsMeshesAsync(FOnAsyncPhysicsCookFinished::CreateUObject(this, &UProceduralMeshComponent::FinishPhysicsAsyncCook, UseBodySetup));
    }
    else
    {
        // 同步构建碰撞
        UseBodySetup->BodySetupGuid = FGuid::NewGuid();
        UseBodySetup->bHasCookedCollisionData = true;
        UseBodySetup->InvalidatePhysicsData();
        UseBodySetup->CreatePhysicsMeshes();
        RecreatePhysicsState(); // 更新组件的物理状态
    }
}

BodySetup 是刚体碰撞的构建器,从上代码中可以看出碰撞构建的 异步/同步 流程。
CollisionTraceFlag 决定了是否将复杂碰撞视为简单碰撞,此时简单碰撞会被忽略。
其中简单碰撞的构建是通过 UseBodySetup->AggGeom.ConvexElems 设置的。
而复杂碰撞在上述代码中看不到设置流程,事实上在 UseBodySetup->CreatePhysicsMeshes() 中, BodySetup 会获取其 Outer 在这里也就是 this ,然后通过 IInterface_CollisionDataProvider 接口获取碰撞信息, PMC 对此接口的实现如下:

// Engine\Plugins\Runtime\ProceduralMeshComponent\Source\ProceduralMeshComponent\Private\ProceduralMeshComponent.cpp

// 用于判断是否存在三角形碰撞数据
bool UProceduralMeshComponent::ContainsPhysicsTriMeshData(bool InUseAllTriData) const
{
    for (const FProcMeshSection& Section : ProcMeshSections)
    {
        if (Section.ProcIndexBuffer.Num() >= 3 && Section.bEnableCollision)
        {
            return true;
        }
    }

    return false;
}

// 用于获取三角形碰撞数据
bool UProceduralMeshComponent::GetPhysicsTriMeshData(struct FTriMeshCollisionData* CollisionData, bool InUseAllTriData)
{
    int32 VertexBase = 0; // 当期分段的顶点索引偏移

    // 查看是否需要填充 UV 数据
    bool bCopyUVs = UPhysicsSettings::Get()->bSupportUVFromHitResults; 
    if (bCopyUVs)
    {
        CollisionData->UVs.AddZeroed(1); // 只添加一个 UV 通道
    }

    // 遍历每个分段
    for (int32 SectionIdx = 0; SectionIdx < ProcMeshSections.Num(); SectionIdx++)
    {
        FProcMeshSection& Section = ProcMeshSections[SectionIdx];
        // 判断分段碰撞是否开启
        if (Section.bEnableCollision)
        {
            // 复制顶点数据
            for (int32 VertIdx = 0; VertIdx < Section.ProcVertexBuffer.Num(); VertIdx++)
            {
                CollisionData->Vertices.Add(Section.ProcVertexBuffer[VertIdx].Position);

                if (bCopyUVs)
                {
                    CollisionData->UVs[0].Add(Section.ProcVertexBuffer[VertIdx].UV0);
                }
            }

            // 复制三角形数据
            const int32 NumTriangles = Section.ProcIndexBuffer.Num() / 3;
            for (int32 TriIdx = 0; TriIdx < NumTriangles; TriIdx++)
            {
                // 通过三个顶点索引构成一个三角形
                FTriIndices Triangle;
                Triangle.v0 = Section.ProcIndexBuffer[(TriIdx * 3) + 0] + VertexBase;
                Triangle.v1 = Section.ProcIndexBuffer[(TriIdx * 3) + 1] + VertexBase;
                Triangle.v2 = Section.ProcIndexBuffer[(TriIdx * 3) + 2] + VertexBase;
                CollisionData->Indices.Add(Triangle);

                // 材质索引 这里以分段索引填充
                CollisionData->MaterialIndices.Add(SectionIdx);
            }

            // 重设分段的顶点索引偏移
            VertexBase = CollisionData->Vertices.Num();
        }
    }

    CollisionData->bFlipNormals = true;
    CollisionData->bDeformableMesh = true;
    CollisionData->bFastCook = true;

    return true;
}

// Engine\Plugins\Runtime\ProceduralMeshComponent\Source\ProceduralMeshComponent\Public\ProceduralMeshComponent.h

// 意义不明 没有找到调用点
bool UProceduralMeshComponent::WantsNegXTriMesh() { return false; }

以上就是碰撞构建的全部流程。

分段更新流程

通过 分段更新 接口我们可以实现更新网格的顶点缓冲区,这比重建分段更快,因为他不需要重建 PMC 渲染器代表 ,至于为什么不允许更改三角索引,大概是因为防止缓冲区大小需要变动,这在运行时是不允许的, PMC 提供了以下几个接口。

// Engine\Plugins\Runtime\ProceduralMeshComponent\Source\ProceduralMeshComponent\Public\ProceduralMeshComponent.h

void UpdateMeshSection(int32 SectionIndex, const TArray<FVector>& Vertices, const TArray<FVector>& Normals, const TArray<FVector2D>& UV0, const TArray<FColor>& VertexColors, const TArray<FProcMeshTangent>& Tangents);
void UpdateMeshSection(int32 SectionIndex, const TArray<FVector>& Vertices, const TArray<FVector>& Normals, const TArray<FVector2D>& UV0, const TArray<FVector2D>& UV1, const TArray<FVector2D>& UV2, const TArray<FVector2D>& UV3, const TArray<FColor>& VertexColors, const TArray<FProcMeshTangent>& Tangents);
void UpdateMeshSection_LinearColor(int32 SectionIndex, const TArray<FVector>& Vertices, const TArray<FVector>& Normals, const TArray<FVector2D>& UV0, const TArray<FVector2D>& UV1, const TArray<FVector2D>& UV2, const TArray<FVector2D>& UV3, const TArray<FLinearColor>& VertexColors, const TArray<FProcMeshTangent>& Tangents);
void UpdateMeshSection_LinearColor(int32 SectionIndex, const TArray<FVector>& Vertices, const TArray<FVector>& Normals, const TArray<FVector2D>& UV0, const TArray<FLinearColor>& VertexColors, const TArray<FProcMeshTangent>& Tangents);

最终他们都会被转发到第二个函数,其实现为。

// Engine\Plugins\Runtime\ProceduralMeshComponent\Source\ProceduralMeshComponent\Private\ProceduralMeshComponent.cpp

void UProceduralMeshComponent::UpdateMeshSection(int32 SectionIndex, const TArray<FVector>& Vertices, const TArray<FVector>& Normals, const TArray<FVector2D>& UV0, const TArray<FVector2D>& UV1, const TArray<FVector2D>& UV2, const TArray<FVector2D>& UV3, const TArray<FColor>& VertexColors, const TArray<FProcMeshTangent>& Tangents)
{
    SCOPE_CYCLE_COUNTER(STAT_ProcMesh_UpdateSectionGT);

    // 确保分段存在
    if(SectionIndex < ProcMeshSections.Num())
    {
        FProcMeshSection& Section = ProcMeshSections[SectionIndex];
        const int32 NumVerts = Vertices.Num();
        const int32 PreviousNumVerts = Section.ProcVertexBuffer.Num();

        // 确保分段顶点数没有变化
        const bool bSameVertexCount = PreviousNumVerts == NumVerts;

        if (bSameVertexCount)
        {
            // 重新计算边界框
            Section.SectionLocalBox = FBox(Vertices);

            // 遍历每个顶点 更新 PMC 内部数据
            for (int32 VertIdx = 0; VertIdx < NumVerts; VertIdx++)
            {
                FProcMeshVertex& ModifyVert = Section.ProcVertexBuffer[VertIdx];

                if (Vertices.Num() == NumVerts)
                {
                    ModifyVert.Position = Vertices[VertIdx];
                }

                if (Normals.Num() == NumVerts)
                {
                    ModifyVert.Normal = Normals[VertIdx];
                }

                if (Tangents.Num() == NumVerts)
                {
                    ModifyVert.Tangent = Tangents[VertIdx];
                }

                if (UV0.Num() == NumVerts)
                {
                    ModifyVert.UV0 = UV0[VertIdx];
                }

                if (UV1.Num() == NumVerts)
                {
                    ModifyVert.UV1 = UV1[VertIdx];
                }

                if (UV2.Num() == NumVerts)
                {
                    ModifyVert.UV2 = UV2[VertIdx];
                }

                if (UV3.Num() == NumVerts)
                {
                    ModifyVert.UV3 = UV3[VertIdx];
                }

                if (VertexColors.Num() == NumVerts)
                {
                    ModifyVert.Color = VertexColors[VertIdx];
                }
            }

            // 如果开启了碰撞 我们需要更新
            if (Section.bEnableCollision)
            {
                TArray<FVector> CollisionPositions;

                // 我们需要重新构建整个 PMC 的碰撞 所以要收集每个分段的网格
                for (const FProcMeshSection& CollisionSection : ProcMeshSections)
                {
                    if (CollisionSection.bEnableCollision)
                    {
                        for (int32 VertIdx = 0; VertIdx < CollisionSection.ProcVertexBuffer.Num(); VertIdx++)
                        {
                            CollisionPositions.Add(CollisionSection.ProcVertexBuffer[VertIdx].Position);
                        }
                    }
                }

                // 通过顶点位置更新碰撞
                BodyInstance.UpdateTriMeshVertices(CollisionPositions);
            }

            // 如果我们有一个有效的渲染器代表
            if (SceneProxy && !IsRenderStateDirty())
            {
                // 创建数据更新包
                FProcMeshSectionUpdateData* SectionData = new FProcMeshSectionUpdateData;
                SectionData->TargetSection = SectionIndex;
                SectionData->NewVertexBuffer = Section.ProcVertexBuffer;

                // 向渲染线程发送数据
                FProceduralMeshSceneProxy* ProcMeshSceneProxy = (FProceduralMeshSceneProxy*)SceneProxy;
                ENQUEUE_RENDER_COMMAND(FProcMeshSectionUpdate)
                ([ProcMeshSceneProxy, SectionData](FRHICommandListImmediate& RHICmdList) { ProcMeshSceneProxy->UpdateSection_RenderThread(SectionData); });
            }

            UpdateLocalBounds();         // 更新边界框
            MarkRenderTransformDirty();  // 标记图元变换变化 更新边界框
        }
        else
        {
            UE_LOG(LogProceduralComponent, Error, TEXT("Trying to update a procedural mesh component section with a different number of vertices [Previous: %i, New: %i] (clear and recreate mesh section instead)"), PreviousNumVerts, NumVerts);
        }
    }
}

通过上述接口,我们将更新的数据打包到 FProcMeshSectionUpdateData 结构中,然后通过代理的 UpdateSection_RenderThread 函数在渲染线程处理数据更新, UpdateSection_RenderThread 的实现如下。

// Engine\Plugins\Runtime\ProceduralMeshComponent\Source\ProceduralMeshComponent\Private\ProceduralMeshComponent.cpp

class FProceduralMeshSceneProxy final : public FPrimitiveSceneProxy
{
public:

    (......)

    void UpdateSection_RenderThread(FProcMeshSectionUpdateData* SectionData)
    {
        SCOPE_CYCLE_COUNTER(STAT_ProcMesh_UpdateSectionRT);

        // 确保我们在渲染线程
        check(IsInRenderingThread());

        // 如果数据合法
        if(SectionData != nullptr)
        { 
            if (SectionData->TargetSection < Sections.Num() &&
                Sections[SectionData->TargetSection] != nullptr)
            {
                FProcMeshProxySection* Section = Sections[SectionData->TargetSection];

                const int32 NumVerts = SectionData->NewVertexBuffer.Num();

                // 首先复制数据到缓冲区的 CPU 端
                for(int32 i=0; i<NumVerts; i++)
                {
                    const FProcMeshVertex& ProcVert = SectionData->NewVertexBuffer[i];
                    FDynamicMeshVertex Vertex;
                    ConvertProcMeshToDynMeshVertex(Vertex, ProcVert);

                    Section->VertexBuffers.PositionVertexBuffer.VertexPosition(i) = Vertex.Position;
                    Section->VertexBuffers.StaticMeshVertexBuffer.SetVertexTangents(i, Vertex.TangentX.ToFVector(), Vertex.GetTangentY(), Vertex.TangentZ.ToFVector());
                    Section->VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(i, 0, Vertex.TextureCoordinate[0]);
                    Section->VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(i, 1, Vertex.TextureCoordinate[1]);
                    Section->VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(i, 2, Vertex.TextureCoordinate[2]);
                    Section->VertexBuffers.StaticMeshVertexBuffer.SetVertexUV(i, 3, Vertex.TextureCoordinate[3]);
                    Section->VertexBuffers.ColorVertexBuffer.VertexColor(i) = Vertex.Color;
                }

                {
                    auto& VertexBuffer = Section->VertexBuffers.PositionVertexBuffer;
                    void* VertexBufferData = RHILockVertexBuffer(VertexBuffer.VertexBufferRHI, 0, VertexBuffer.GetNumVertices() * VertexBuffer.GetStride(), RLM_WriteOnly);
                    FMemory::Memcpy(VertexBufferData, VertexBuffer.GetVertexData(), VertexBuffer.GetNumVertices() * VertexBuffer.GetStride());
                    RHIUnlockVertexBuffer(VertexBuffer.VertexBufferRHI);
                }

                // 以只读锁定 RHI 缓冲区并从 CPU 端复制数据

                {
                    auto& VertexBuffer = Section->VertexBuffers.ColorVertexBuffer;
                    void* VertexBufferData = RHILockVertexBuffer(VertexBuffer.VertexBufferRHI, 0, VertexBuffer.GetNumVertices() * VertexBuffer.GetStride(), RLM_WriteOnly);
                    FMemory::Memcpy(VertexBufferData, VertexBuffer.GetVertexData(), VertexBuffer.GetNumVertices() * VertexBuffer.GetStride());
                    RHIUnlockVertexBuffer(VertexBuffer.VertexBufferRHI);
                }

                {
                    auto& VertexBuffer = Section->VertexBuffers.StaticMeshVertexBuffer;
                    void* VertexBufferData = RHILockVertexBuffer(VertexBuffer.TangentsVertexBuffer.VertexBufferRHI, 0, VertexBuffer.GetTangentSize(), RLM_WriteOnly);
                    FMemory::Memcpy(VertexBufferData, VertexBuffer.GetTangentData(), VertexBuffer.GetTangentSize());
                    RHIUnlockVertexBuffer(VertexBuffer.TangentsVertexBuffer.VertexBufferRHI);
                }

                {
                    auto& VertexBuffer = Section->VertexBuffers.StaticMeshVertexBuffer;
                    void* VertexBufferData = RHILockVertexBuffer(VertexBuffer.TexCoordVertexBuffer.VertexBufferRHI, 0, VertexBuffer.GetTexCoordSize(), RLM_WriteOnly);
                    FMemory::Memcpy(VertexBufferData, VertexBuffer.GetTexCoordData(), VertexBuffer.GetTexCoordSize());
                    RHIUnlockVertexBuffer(VertexBuffer.TexCoordVertexBuffer.VertexBufferRHI);
                }
            }

            // 施放数据包
            delete SectionData;
        }
    }

};

至此,渲染数据更新完成,在下一次 GetDynamicMeshElements 时将会画出新的网格。

One thought on “UE – ProceduralMeshComponent 分析

发表评论