Unreal – ProceduralMeshComponent Analysis

engine: UE4.26.2 build version

ProceduralMeshComponent, referred to as PMC, provides a programmatic mesh component that can make renderable meshes from custom triangles, uses dynamic rendering paths, and collects rendering data at each frame without any data caching.

Class structure

Game type diagram

classDiagram

Interface_CollisionDataProvider <|.. UProceduralMeshComponent
FPrimitiveSceneProxy <|-- FProceduralMeshSceneProxy
UPrimitiveComponent .. FPrimitiveSceneProxy : 镜像
UProceduralMeshComponent .. FProceduralMeshSceneProxy : 镜像
FProceduralMeshSceneProxy --> FProcMeshProxySection
FProcMeshSection .. FProcMeshProxySection : 镜像
UActorComponent <|-- USceneComponent
USceneComponent <|-- UPrimitiveComponent
UPrimitiveComponent <|-- UMeshComponent
UMeshComponent <|-- UProceduralMeshComponent
UProceduralMeshComponent --> FProcMeshSection
FProcMeshSection --> FProcMeshVertex
FProcMeshVertex --> FProcMeshTangent

class UActorComponent {
    +MarkRenderStateDirty()
    +MarkRenderTransformDirty()
    +MarkRenderDynamicDataDirty()
    +RecreatePhysicsState()
}

class USceneComponent {
    CalcBounds(const FTransform& LocalToWorld) FBoxSphereBounds
}

class UPrimitiveComponent {
    CreateSceneProxy() FPrimitiveSceneProxy*
    GetBodySetup() UBodySetup*
    GetMaterialFromCollisionFaceIndex(...) UMaterialInterface*
}

class UMeshComponent {
    GetNumMaterials() int32
}

class Interface_CollisionDataProvider {
    GetPhysicsTriMeshData(...) bool
    ContainsPhysicsTriMeshData(...) bool
    WantsNegXTriMesh() bool
}

class UProceduralMeshComponent {
    +bool bUseComplexAsSimpleCollision
    +bool bUseAsyncCooking
    +UBodySetup* ProcMeshBodySetup
    +CreateMeshSection(...)
    +UpdateMeshSection(...)
    +ClearMeshSection(...)
    +ClearAllMeshSections(...)
    +SetMeshSectionVisible(...)
    +IsMeshSectionVisible(...) bool
    +GetNumSections(...) int32
    +AddCollisionConvexMesh(...)
    +ClearCollisionConvexMeshes(...)
    +SetCollisionConvexMeshes(...)
}

class FProcMeshSection {
    +TArray~FProcMeshVertex~ ProcVertexBuffer
    +TArray~uint32~ ProcIndexBuffer
    +FBox SectionLocalBox
    +bool bEnableCollision
    +bool bSectionVisible
}

class FProcMeshVertex {
    +FVector Position
    +FVector Normal
    +FProcMeshTangent Tangent
    +FColor Color
    +FVector2D UV0
    +FVector2D UV1
    +FVector2D UV2
    +FVector2D UV3
}

class FProcMeshTangent {
    +FVector TangentX
    +bool bFlipTangentY
}

Renderer class diagram

classDiagram

UPrimitiveComponent <|-- UMeshComponent
UMeshComponent <|-- UProceduralMeshComponent
UProceduralMeshComponent --> FProcMeshSection
FPrimitiveSceneProxy <|-- FProceduralMeshSceneProxy
UPrimitiveComponent .. FPrimitiveSceneProxy : 镜像
UProceduralMeshComponent .. FProceduralMeshSceneProxy : 镜像
FProceduralMeshSceneProxy --> FProcMeshProxySection
FProcMeshSection .. FProcMeshProxySection : 镜像

class FPrimitiveSceneProxy {
    GetTypeHash() SIZE_T
    GetDynamicMeshElements(...)
    GetViewRelevance(...) FPrimitiveViewRelevance
    CanBeOccluded() bool
    GetMemoryFootprint() uint32
}

class FProceduralMeshSceneProxy {
    -TArray~FProcMeshProxySection*~ Sections
    -UBodySetup* BodySetup
    -FMaterialRelevance MaterialRelevance
    +UpdateSection_RenderThread(...)
    +SetSectionVisibility_RenderThread(...)
}

class FProcMeshProxySection {
    +UMaterialInterface* Material
    +FStaticMeshVertexBuffers VertexBuffers
    +FDynamicMeshIndexBuffer32 IndexBuffer
    +FLocalVertexFactory VertexFactory
    +bool bSectionVisible
}

Introduction

  • UActorComponent – ​​the base class for all components.
  • USceneComponent – ​​A component with transform properties.
  • UPrimitiveComponent – ​​The base class of primitive components, indicating that the component has renderable primitives.
  • FPrimitiveSceneProxy – A renderer representation of a primitive component that mirrors component rendering data for multi-threaded rendering.
  • UMeshComponent – ​​Base class for mesh components, representing components with renderable mesh primitives.
  • Interface_CollisionDataProvider – Collision data interface, indicating that the object has collision data.
  • UProceduralMeshComponent - PMC component, the core of the entire system, provides an interface for customizing triangle face sets.
  • FProceduralMeshSceneProxy – Renderer representation of PMC, mirroring PMC data.
  • FProcMeshSection – PMC mesh segment, the segment can independently set the material collision visibility.
  • FProcMeshProxySection – Renderer representation of PMC mesh segments, mirroring mesh segment data.
  • FProcMeshVertex – PMC vertices, a vertex set and triangle index forming a PMC mesh segment.
  • FProcMeshTangent – ​​PMC tangent, each vertex has an independent tangent, where the Z-tangent is the normal and the Y-tangent is calculated dynamically.

Process

Segment creation process

The most commonly used interface of PMC and the first interface to learn is the segment creation interface. Its function is to create/update a segmented data on the grid. The main process is to independently obtain the segmented vertex triangle index color UV method through parameters. Line tangents, etc., assembled as PMC internal data, open functions with the following signature:

// 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)

But it will eventually be forwarded to the second function, which is actually the behavior.

// 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(); // 表示渲染数据改变
}

You can see that this function organizes the scattered triangular surface sets into PMC segmented data, and then uses MarkRenderStateDirty to notify the rendering data of changes. In addition to the above triggering method, updating the segmented material will also trigger the following renderer representation. Rebuild the process.

// 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
            }
        }
    }

    (......)

};

The main function of the above code is to convert data from PMC segments to proxy versions, that is, renderer data. The FProcMeshProxySection class stores segmented renderer data. It mainly has the following member types, which are roughly the same as SM rendering data.

  • UMaterialInterface – the material used by this segment
  • FStaticMeshVertexBuffers – Set of vertex buffers
  • FDynamicMeshIndexBuffer32 – 32-bit index buffer
  • FLocalVertexFactory – Vertex factory, encapsulating vertex buffer

The vertex buffer set has the following main member types.

  • FStaticMeshVertexBuffer – stores tangents and UVs
  • FPositionVertexBuffer – storage location
  • FColorVertexBuffer – stores colors

The vertex buffer set is initialized through the InitFromDynamicVertex function, which is implemented as follows.

// 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);
        });
};

At this point we have prepared all the rendering data, and the next step is to wait for the renderer to generate rendering commands.

Rendering integration process

After the rendering data is ready, the renderer will call a series of interfaces in the rendering thread at the appropriate time to obtain the information required for rendering. When the primitive proxy is first established, FPrimitiveSceneInfo will collect primitive information for use by the renderer.

// 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;
    }

    (......)

};

From Result.bDynamicRelevance = true, we can see that PMC uses a dynamic drawing path. Under the dynamic drawing path, the engine will re-obtain the primitive information through GetDynamicMeshElements at each frame and generate RHI commands.

// 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
    }

    (......)

};

Collision build process

After the triangle data or convex mesh is updated, UpdateCollision will be called to update the component's collision data.

// 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 is the builder of rigid body collision. From the above code, you can see the asynchronous/synchronous process of collision construction.
CollisionTraceFlag determines whether complex collisions are treated as simple collisions, in which case simple collisions will be ignored.
The construction of simple collision is set through UseBodySetup->AggGeom.ConvexElems.
The setting process for complex collisions cannot be seen in the above code. In fact, in UseBodySetup->CreatePhysicsMeshes(), BodySetup will get its Outer, which is this, and then obtain the collision information through the IInterface_CollisionDataProvider interface. PMC implements this interface as follows :

// 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; }

The above is the entire process of collision construction.

Segmented update process

Through the segmentation update interface, we can update the vertex buffer of the mesh, which is faster than rebuilding segments, because it does not need to rebuild the PMC renderer representative. As for why the triangle index is not allowed to be changed, it is probably because of the need to prevent the buffer size. Changes are not allowed at runtime. PMC provides the following interfaces.

// 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);

Eventually they will all be forwarded to the second function, which actually behaves.

// 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);
        }
    }
}

Through the above interface, we package the updated data into the FProcMeshSectionUpdateData structure, and then process the data update in the rendering thread through the agent's UpdateSection_RenderThread function. The implementation of UpdateSection_RenderThread is as follows.

// 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;
        }
    }

};

At this point, the rendering data update is completed, and a new mesh will be drawn the next time GetDynamicMeshElements is called.

One thought on "Unreal – ProceduralMeshComponent Analysis"

Post Reply