说明

本文为UE5-地形系统系列的第一篇文章,主要是对UE5地形系统进行初步的整体分析

主要类型

ALandscapeProxy

继承体系如下:

  • UObjectBase
    • UObjectBaseUtility
      • UObject
        • AActor
          • APartitionActor
            • ALandscapeProxy

Detail 面板上显示的属性变量都存放在 ALandscapeProxy 类中,这个类主要用来保存地形的详细信息和属性设定值。

例如在 bEnableNanite 开启时,Detail 面板 Nanite 类目下 Advanced 选项就会变为可编辑

// Engine\Source\Runtime\Landscape\Classes\LandscapeProxy.h

class ALandscapeProxy : public APartitionActor, public ILandscapeSplineInterface
{
    GENERATED_BODY()
public:
    (...)
    UPROPERTY(EditAnywhere, Category = Nanite, AdvancedDisplay, meta = (EditCondition = "bEnableNanite", LandscapeInherited))
    int32 NaniteLODIndex = 0;

    UPROPERTY(EditAnywhere, Category = Nanite, AdvancedDisplay, meta = (EditCondition = "bEnableNanite", LandscapeInherited))
    bool bNaniteSkirtEnabled = false;

    UPROPERTY(EditAnywhere, Category = Nanite, AdvancedDisplay, meta = (EditCondition = "bEnableNanite", LandscapeInherited))
    float NaniteSkirtDepth = 0.1f;

    UPROPERTY(EditAnywhere, Category = Nanite, AdvancedDisplay, meta = (EditCondition = "bEnableNanite", LandscapeInherited))
    int32 NanitePositionPrecision = 0;

    UPROPERTY(EditAnywhere, Category = Nanite, AdvancedDisplay, meta = (EditCondition = "bEnableNanite", LandscapeInherited))
    (...)
}

LandscapeDetailPanel

ALandscape

ALandscape 继承自 ALandscapeProxy,在此基础上实现了更多功能。
ALanscape的主要作用是管理LandscapeComponent,地形相关的真正的渲染数据在ULandscapeComponent,地形渲染流程也是以ULandscapeComponent为基础单元。

ULandscapeComponent

LandscapeComponentClassDiagram

每个地形都会被划分成多个地形组件。
地形组件是虚幻引擎在渲染地形、计算地形可视性和处理地形碰撞时采用的基本单位。

LandscapeWireframeInEditor

地形中的所有地形组件都具有相同的大小,并且始终为正方形。地形组件尺寸是在创建地形时决定的,取决于地形的大小和细节设置。

LandscapeComponentSettings

每个 Component 可以被分为 2*2个 Sections,也可以只有一个 Section ,每个 Section 可以由不同数量的 Quads 组成,Section 每个方向上的 Quads 数量皆为 Log N - 1,比如7, 15, 31…,最大为255。

因此,每个 Section 单个方向上的顶点数一定为2的指数。

每个 LandscapeComponent 的高度数据存储在一张HeightMap纹理中。因此,纹理的大小必定是2的指数。

组件数、分段数、四边形数、定点数 计算示例:

总体大小(顶点数) 四边形数/分段 分段数/组件 地形组件尺寸 地形组件总数
8129 x 8129 127 4 (2x2) 254x254 1024 (32x32)
4033 x 4033 63 4 (2x2) 126x126 1024 (32x32)

FLandscapeComponentSceneProxy

FLandscapeComponentSceneProxy 继承自 FPrimitiveSceneProxy,是 ULandscapeComponent 在渲染线程的代表,镜像了 ULandscapeComponent 在渲染线程的状态,其中包含了 Landscape 的 VertexBuffer, IndexBuffer, VertexFactory 等数据。

类似UE Mesh Drawing Pipeline中所述,对于 Landscape 的渲染也是从构建其 FLandscapeComponentSceneProxy 开始的。

FLandscapeSharedBuffers

FLandscapeComponentSceneProxy 中包含一个 FLandscapeSharedBuffers, 上面提到的FLandscapeComponentSceneProxy 中的 VertexBuffer, IndexBuffer 等渲染所必需的数据,实际上就是在 FLandscapeSharedBuffers 之中。

FLandscapeComponentSceneProxy类的静态成员,TMap<uint32, FLandscapeSharedBuffers*> SharedBuffersMap 管理所有的FLandscapeSharedBuffers。

SharedBuffersKey 是 SharedBuffersMap 中的键,对应着各自的 SharedBuffers。在 FLandscapeComponentSceneProxy 创建时也会计算其 SharedBuffers所对应的 SharedBuffersKey

FLandscapeComponentSceneProxy::FLandscapeComponentSceneProxy(ULandscapeComponent* InComponent)
    : (...)
{
    (...)
    const int8 SubsectionSizeLog2 = static_cast<int8>(FMath::CeilLogTwo(InComponent->SubsectionSizeQuads + 1));
    SharedBuffersKey = (SubsectionSizeLog2 & 0xf) | ((NumSubsections & 0xf) << 4) | (XYOffsetmapTexture == nullptr ? 0 : 1 << 31);
    (...)
}

可见 SharedBuffersKey 是由 SubsectionSizeQuads、NumSubsections、XYOffsetmapTexture 唯一确定的。也就是说,同属于一个 ALandscape 的不同 LandscapeComponent 有可能共用一个 SharedBuffers

FLandscapeSharedBuffers 在构造过程中会创建并设置其中的 VertexBuffer(FLandscapeVertexBuffer 类型),IndexBuffer(FIndexBuffer[] 类型)

// Engine\Source\Runtime\Landscape\Private\LandscapeRender.cpp

FLandscapeSharedBuffers::FLandscapeSharedBuffers(FRHICommandListBase& RHICmdList, const int32 InSharedBuffersKey, const int32 InSubsectionSizeQuads, const int32 InNumSubsections, const ERHIFeatureLevel::Type InFeatureLevel, const FName& InOwnerName)
    : SharedBuffersKey(InSharedBuffersKey)
    , NumIndexBuffers(FMath::CeilLogTwo(InSubsectionSizeQuads + 1))
    , SubsectionSizeVerts(InSubsectionSizeQuads + 1)
    , NumSubsections(InNumSubsections)
    , VertexFactory(nullptr)
    , FixedGridVertexFactory(nullptr)
    , VertexBuffer(nullptr)
    , TileMesh(nullptr)
    , TileVertexFactory(nullptr)
    , TileDataBuffer(nullptr)
    , bUse32BitIndices(false)
    , GrassIndexBuffer(nullptr)
{
    NumVertices = FMath::Square(SubsectionSizeVerts) * FMath::Square(NumSubsections);

    VertexBuffer = new FLandscapeVertexBuffer(RHICmdList, InFeatureLevel, NumVertices, SubsectionSizeVerts, NumSubsections, InOwnerName);

    IndexBuffers = new FIndexBuffer * [NumIndexBuffers];
    FMemory::Memzero(IndexBuffers, sizeof(FIndexBuffer*) * NumIndexBuffers);
    IndexRanges = new FLandscapeIndexRanges[NumIndexBuffers]();

    (...)

    // See if we need to use 16 or 32-bit index buffers
    if (NumVertices > 65535)
    {
        bUse32BitIndices = true;
        CreateIndexBuffers<uint32>(RHICmdList, InOwnerName);
        if (UE::Landscape::ShouldBuildGrassMapRenderingResources())
        {
            CreateGrassIndexBuffer<uint32>(RHICmdList, InOwnerName);
        }
    }
    else
    {
        CreateIndexBuffers<uint16>(RHICmdList, InOwnerName);
        if (UE::Landscape::ShouldBuildGrassMapRenderingResources())
        {
            CreateGrassIndexBuffer<uint16>(RHICmdList, InOwnerName);
        }
    }
}

template <typename INDEX_TYPE>
void FLandscapeSharedBuffers::CreateIndexBuffers(FRHICommandListBase& RHICmdList, const FName& InOwnerName)
{
    TArray<INDEX_TYPE> VertexToIndexMap;
    VertexToIndexMap.AddUninitialized(FMath::Square(SubsectionSizeVerts * NumSubsections));
    FMemory::Memset(VertexToIndexMap.GetData(), 0xff, NumVertices * sizeof(INDEX_TYPE));

    INDEX_TYPE VertexCount = 0;
    int32 SubsectionSizeQuads = SubsectionSizeVerts - 1;

    // 对各Lod创建不同大小的IndexBuffer
    int32 MaxLOD = NumIndexBuffers - 1;
    for (int32 Mip = MaxLOD; Mip >= 0; Mip--)
    {
        int32 LodSubsectionSizeQuads = (SubsectionSizeVerts >> Mip) - 1;

        TArray<INDEX_TYPE> NewIndices;
        int32 ExpectedNumIndices = FMath::Square(NumSubsections) * FMath::Square(LodSubsectionSizeQuads) * 6;
        NewIndices.Empty(ExpectedNumIndices);

        int32& MaxIndexFull = IndexRanges[Mip].MaxIndexFull;
        int32& MinIndexFull = IndexRanges[Mip].MinIndexFull;
        MaxIndexFull = 0;
        MinIndexFull = MAX_int32;
        {
            int32 SubOffset = 0;
            for (int32 SubY = 0; SubY < NumSubsections; SubY++)
            {
                for (int32 SubX = 0; SubX < NumSubsections; SubX++)
                {
                    int32& MaxIndex = IndexRanges[Mip].MaxIndex[SubX][SubY];
                    int32& MinIndex = IndexRanges[Mip].MinIndex[SubX][SubY];
                    MaxIndex = 0;
                    MinIndex = MAX_int32;

                    for (int32 y = 0; y < LodSubsectionSizeQuads; y++)
                    {
                        for (int32 x = 0; x < LodSubsectionSizeQuads; x++)
                        {
                            INDEX_TYPE i00 = static_cast<INDEX_TYPE>((x + 0) + (y + 0) * SubsectionSizeVerts + SubOffset);
                            INDEX_TYPE i10 = static_cast<INDEX_TYPE>((x + 1) + (y + 0) * SubsectionSizeVerts + SubOffset);
                            INDEX_TYPE i11 = static_cast<INDEX_TYPE>((x + 1) + (y + 1) * SubsectionSizeVerts + SubOffset);
                            INDEX_TYPE i01 = static_cast<INDEX_TYPE>((x + 0) + (y + 1) * SubsectionSizeVerts + SubOffset);

                            NewIndices.Add(i00);
                            NewIndices.Add(i11);
                            NewIndices.Add(i10);

                            NewIndices.Add(i00);
                            NewIndices.Add(i01);
                            NewIndices.Add(i11);

                            // Update the min/max index ranges
                            MaxIndex = FMath::Max<int32>(MaxIndex, i00);
                            MinIndex = FMath::Min<int32>(MinIndex, i00);
                            MaxIndex = FMath::Max<int32>(MaxIndex, i10);
                            MinIndex = FMath::Min<int32>(MinIndex, i10);
                            MaxIndex = FMath::Max<int32>(MaxIndex, i11);
                            MinIndex = FMath::Min<int32>(MinIndex, i11);
                            MaxIndex = FMath::Max<int32>(MaxIndex, i01);
                            MinIndex = FMath::Min<int32>(MinIndex, i01);
                        }
                    }

                    // update min/max for full subsection
                    MaxIndexFull = FMath::Max<int32>(MaxIndexFull, MaxIndex);
                    MinIndexFull = FMath::Min<int32>(MinIndexFull, MinIndex);

                    SubOffset += FMath::Square(SubsectionSizeVerts);
                }
            }

            check(MinIndexFull <= (uint32)((INDEX_TYPE)(~(INDEX_TYPE)0)));
            check(NewIndices.Num() == ExpectedNumIndices);
        }

        // Create and init new index buffer with index data
        (...)
    }
}

地形渲染

HeightMap格式

HeightMap的格式为RGBA8,RG存储顶点的高度值,R为高8位,G为低8位;BA存储了顶点法线。

高度值、法线向HeightMap数据转换计算方法

// Engine\Source\Runtime\Landscape\Private\LandscapeEditInterface.cpp

TexData.R = Height >> 8;
TexData.G = Height & 255;

TexData.B = static_cast<uint8>(FMath::RoundToInt32(127.5f * (Normal.X + 1.0f)));
TexData.A = static_cast<uint8>(FMath::RoundToInt32(127.5f * (Normal.Y + 1.0f)));

组件中Vertex和Index的组织形式

Index

// Engine\Source\Runtime\Landscape\Public\LandscapeRender.h

class FLandscapeSharedBuffers : public FRefCountedObject
{
public:
    (...)

    // array per mip level, storing FIndexBuffer pointers
    FIndexBuffer** IndexBuffers;

    (...)
};

SharedBuffers 中保存的是 IndexBuffer 的数组,其 IndexBuffer 的索引在组件中的排布如下:

IndexLayout

Vertex

Vertex 的创建是在 FLandscapeVertexBuffer::InitRHI 中

// Engine\Source\Runtime\Landscape\Private\LandscapeRender.cpp

void FLandscapeVertexBuffer::InitRHI(FRHICommandListBase& RHICmdList)
{
    SCOPED_LOADTIMER(FLandscapeVertexBuffer_InitRHI);

    (...)

    VertexBufferRHI = RHICmdList.CreateBuffer(NumVertices * sizeof(FLandscapeVertex), BUF_Static | BUF_VertexBuffer, 0, ERHIAccess::VertexOrIndexBuffer, CreateInfo);
    FLandscapeVertex* Vertex = (FLandscapeVertex*)RHICmdList.LockBuffer(VertexBufferRHI, 0, NumVertices * sizeof(FLandscapeVertex), RLM_WriteOnly);
    int32 VertexIndex = 0;
    for (int32 SubY = 0; SubY < NumSubsections; SubY++)
    {
        for (int32 SubX = 0; SubX < NumSubsections; SubX++)
        {
            for (int32 y = 0; y < SubsectionSizeVerts; y++)
            {
                for (int32 x = 0; x < SubsectionSizeVerts; x++)
                {
                    Vertex->VertexX = static_cast<uint8>(x);
                    Vertex->VertexY = static_cast<uint8>(y);
                    Vertex->SubX = static_cast<uint8>(SubX);
                    Vertex->SubY = static_cast<uint8>(SubY);
                    Vertex++;
                    VertexIndex++;
                }
            }
        }
    }
    check(NumVertices == VertexIndex);
    RHICmdList.UnlockBuffer(VertexBufferRHI);
}

// Engine\Source\Runtime\Landscape\Public\LandscapeRender.h

struct FLandscapeVertex
{
    uint8 VertexX; // 顶点在所属Section中的X坐标 
    uint8 VertexY; // 顶点在所属Section中的Y坐标
    uint8 SubX; // 顶点所属Section在Component中的X坐标
    uint8 SubY; // 顶点所属Section在Component中的Y坐标
};

Vertex 以及 Section 坐标在 Component 中的排布如下:

VertexLayout

Landscape渲染过程

创建FLandscapeComponentSceneProxy

首先调用FScene::AddPrimitive,在其中嵌套调用FScene::BatchAddPrimitivesInternal

FScene::BatchAddPrimitivesInternal 中遍历所有 UPrimitiveComponent 并创建其 FPrimitiveSceneProxy

// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp

void FScene::AddPrimitive(UPrimitiveComponent* Primitive)
{
    // If the bulk reregister flag is set, add / remove will be handled in bulk by the FStaticMeshComponentBulkReregisterContext
    if (Primitive->bBulkReregister)
    {
        return;
    }
    BatchAddPrimitivesInternal(MakeArrayView(&Primitive, 1));
}

// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp

template<class T>
void FScene::BatchAddPrimitivesInternal(TArrayView<T*> InPrimitives)
{
    (...)

    for (T* Primitive : InPrimitives)
    {
        (...)

        FPrimitiveSceneProxy* PrimitiveSceneProxy  = nullptr;

        if (Primitive->GetPrimitiveComponentInterface())
        {
            checkf(!Primitive->GetSceneProxy(), TEXT("Primitive has already been added to the scene!"));
            PrimitiveSceneProxy = Primitive->GetPrimitiveComponentInterface()->CreateSceneProxy();
            check(SceneData.SceneProxy == PrimitiveSceneProxy); // CreateSceneProxy has access to the shared SceneData and should set it properly
        }
        else
        {
            check(!Primitive->ShouldRecreateProxyOnUpdateTransform()); // recreating proxies when updating the transform requires a IPrimitiveComponentInterface
            PrimitiveSceneProxy = Primitive->GetSceneProxy();
        }

        if(!PrimitiveSceneProxy)
        {
            // Primitives which don't have a proxy are irrelevant to the scene manager.
            continue;
        }

        // Create the primitive scene info.
        FPrimitiveSceneInfo* PrimitiveSceneInfo = new FPrimitiveSceneInfo(Primitive, this);
        PrimitiveSceneProxy->PrimitiveSceneInfo = PrimitiveSceneInfo;

        (...)
    }

    (...)
}

当遍历到的 UPrimitiveComponent 为 ULandscapeComponent,则创建 FPrimitiveSceneProxy 时会调用 ULandscapeComponent::CreateSceneProxy,于是创建出的便是地形的 FLandscapeComponentSceneProxy

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

FPrimitiveSceneProxy* ULandscapeComponent::CreateSceneProxy()
{
    return new FLandscapeComponentSceneProxy(this);
}

FLandscapeComponentSceneProxy 的构造函数,在其中主要完成了调用父类 FPrimitiveSceneProxy 的构造函数;完成 Sections、四边形、顶点数量的设置;设置高度图、权重图;设置LOD;地形材质收集;计算SharedBuffersKey等工作。

需要注意的是,FLandscapeComponentSceneProxy 的构造函数虽然初始化了 SharedBuffers, VertexFactory 等,但是并未真正完成其设置,也就是说此时还未创建 VertexBuffer 和 IndexBuffer 的数据。

渲染资源创建

FLandscapeComponentSceneProxy 创建完成后,使用 ENQUEUE_RENDER_COMMAND(AddPrimitiveCommand) 宏向渲染线程入队AddPrimitiveCommand

// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp

template<class T>
void FScene::BatchAddPrimitivesInternal(TArrayView<T*> InPrimitives)
{
    (...)

    for (T* Primitive : InPrimitives)
    {
        (...)

        CreateCommands.Emplace(
            PrimitiveSceneInfo,
            PrimitiveSceneProxy,
            // If this primitive has a simulated previous transform, ensure that the velocity data for the scene representation is correct.
            FMotionVectorSimulation::Get().GetPreviousTransform(ToUObject(Primitive)),
            RenderMatrix,
            Primitive->Bounds,
            AttachmentRootPosition,
            Primitive->GetLocalBounds()
        );

        (...)
    }

    if (!CreateCommands.IsEmpty())
    {
        ENQUEUE_RENDER_COMMAND(AddPrimitiveCommand)(
            [this, CreateCommands = MoveTemp(CreateCommands)](FRHICommandListBase& RHICmdList)
        {
            for (const FCreateCommand& Command : CreateCommands)
            {
                FScopeCycleCounter Context(Command.PrimitiveSceneProxy->GetStatId());
                Command.PrimitiveSceneProxy->SetTransform(RHICmdList, Command.RenderMatrix, Command.WorldBounds, Command.LocalBounds, Command.AttachmentRootPosition);
                Command.PrimitiveSceneProxy->CreateRenderThreadResources(RHICmdList);

                AddPrimitiveSceneInfo_RenderThread(Command.PrimitiveSceneInfo, Command.PreviousTransform);
            }
        });
    }
}

AddPrimitiveCommand执行时,渲染线程会先调用 FPrimitiveSceneProxy::SetTransform 设置包围盒、变换矩阵,并且创建并设置FLandscapeUniformShaderParams,然后将其加入 FLandscapeComponentSceneProxy::LandscapeUniformShaderParameters(TUniformBuffer类型)

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

void FPrimitiveSceneProxy::SetTransform(FRHICommandListBase& RHICmdList, const FMatrix& InLocalToWorld, const FBoxSphereBounds& InBounds, const FBoxSphereBounds& InLocalBounds, FVector InActorPosition)
{
    // Update the cached transforms.
    LocalToWorld = InLocalToWorld;
    bIsLocalToWorldDeterminantNegative = LocalToWorld.Determinant() < 0.0f;

    // Update the cached bounds. Pad them to account for max WPO and material displacement
    const float PadAmount = GetAbsMaxDisplacement();
    Bounds = PadBounds(InBounds, PadAmount);
    LocalBounds = PadLocalBounds(InLocalBounds, LocalToWorld, PadAmount);
    ActorPosition = InActorPosition;

    // Update cached reflection capture.
    if (PrimitiveSceneInfo)
    {
        PrimitiveSceneInfo->bNeedsCachedReflectionCaptureUpdate = true;
        Scene->RequestUniformBufferUpdate(*PrimitiveSceneInfo);
    }

    // Notify the proxy's implementation of the change.
    OnTransformChanged(RHICmdList);
}
SharedBuffers

然后调用 FLandscapeComponentSceneProxy::CreateRenderThreadResources。在其中才是真正设置渲染所需的资源。在 FLandscapeSharedBuffers 构造函数中设置 VertexBuffer, IndexBuffer。然后进一步设置 SharedBuffers 中的 VertexFactory,以及Lod相关参数(以UniformBuffer的形式)

// Engine\Source\Runtime\Landscape\Private\LandscapeRender.cpp

void FLandscapeComponentSceneProxy::CreateRenderThreadResources(FRHICommandListBase& RHICmdList)
{
    (...)

    // 若SharedBuffers已在SharedBuffersMap中,则直接获取;若不在,则根据SharedBufferKey来创建 
    SharedBuffers = FLandscapeComponentSceneProxy::SharedBuffersMap.FindRef(SharedBuffersKey);
    if (SharedBuffers == nullptr)
    {
        FName BufferOwnerName;

        SharedBuffers = new FLandscapeSharedBuffers(RHICmdList, SharedBuffersKey, SubsectionSizeQuads,
                                                                NumSubsections, FeatureLevel, BufferOwnerName);
        FLandscapeComponentSceneProxy::SharedBuffersMap.Add(SharedBuffersKey, SharedBuffers);

        // 设置VertexFactory
        if (!XYOffsetmapTexture)
        {
            FLandscapeVertexFactory* LandscapeVertexFactory = new FLandscapeVertexFactory(FeatureLevel);
            LandscapeVertexFactory->Data.PositionComponent = FVertexStreamComponent(SharedBuffers->VertexBuffer, 0, sizeof(FLandscapeVertex), VET_UByte4);
            LandscapeVertexFactory->InitResource(RHICmdList);
            SharedBuffers->VertexFactory = LandscapeVertexFactory;
        }
        else
        {
            FLandscapeXYOffsetVertexFactory* LandscapeXYOffsetVertexFactory = new FLandscapeXYOffsetVertexFactory(FeatureLevel);
            LandscapeXYOffsetVertexFactory->Data.PositionComponent = FVertexStreamComponent(SharedBuffers->VertexBuffer, 0, sizeof(FLandscapeVertex), VET_UByte4);
            LandscapeXYOffsetVertexFactory->InitResource(RHICmdList);
            SharedBuffers->VertexFactory = LandscapeXYOffsetVertexFactory;
        }

        (...)
    }
    SharedBuffers->AddRef();
    // Assign vertex factory
    VertexFactory = SharedBuffers->VertexFactory;
    FixedGridVertexFactory = SharedBuffers->FixedGridVertexFactory;

    (...)

    LandscapeUniformShaderParameters.InitResource(RHICmdList);

    // 为每个Lod创建并设置uniform buffer
    const int32 NumMips = FMath::CeilLogTwo(SubsectionSizeVerts);
    // create as many as there are potential mips (even if MaxLOD can be inferior than that), because the grass could need that much :
    LandscapeFixedGridUniformShaderParameters.AddDefaulted(NumMips);
    for (int32 LodIndex = 0; LodIndex < NumMips; ++LodIndex)
    {
        FLandscapeFixedGridUniformShaderParameters Parameters;
        Parameters.LodValues = FVector4f(
            static_cast<float>(LodIndex),
            0.f,
            (float)((SubsectionSizeVerts >> LodIndex) - 1),
            1.f / (float)((SubsectionSizeVerts >> LodIndex) - 1));
        LandscapeFixedGridUniformShaderParameters[LodIndex].SetContents(RHICmdList, Parameters);
        LandscapeFixedGridUniformShaderParameters[LodIndex].InitResource(RHICmdList);
    }

    // Create MeshBatch for grass rendering
    (...)
}

AddPrimitiveCommand 执行的最后,渲染线程调用 AddPrimitiveSceneInfo_RenderThread,进行 Primitive 信息的收集,加入到AddedPrimitiveSceneInfos 中

地形材质

地形材质的收集实际上在 FLandscapeComponentSceneProxy 的构造函数中完成,收集并创建的 FMaterialRenderProxy 最终保存在FLandscapeComponentSceneProxy::AvailableMaterials 中

// Engine\Source\Runtime\Landscape\Private\LandscapeRender.cpp

FLandscapeComponentSceneProxy::FLandscapeComponentSceneProxy(ULandscapeComponent* InComponent)
    : (...)
{
    (...)

    TArray<UMaterialInterface*> AvailableMaterialInterfaces;
    if (FeatureLevel == ERHIFeatureLevel::ES3_1)
    {
        WeightmapTextures = InComponent->MobileWeightmapTextures;
        Algo::Transform(InComponent->MobileMaterialInterfaces, AvailableMaterials, GetRenderProxy);
        AvailableMaterialInterfaces.Append(InComponent->MobileMaterialInterfaces);
        //TODO: Add support for bUseDynamicMaterialInstance ?
    }
    else
    {
        WeightmapTextures = InComponent->GetWeightmapTextures();
        if (InComponent->GetLandscapeProxy()->bUseDynamicMaterialInstance)
        {
            Algo::Transform(InComponent->MaterialInstancesDynamic, AvailableMaterials, GetRenderProxy);
            AvailableMaterialInterfaces.Append(InComponent->MaterialInstancesDynamic);
        }
        else
        {
            Algo::Transform(InComponent->MaterialInstances, AvailableMaterials, GetRenderProxy);
            AvailableMaterialInterfaces.Append(InComponent->MaterialInstances);
        }
    }

    (...)

    if (ensure(AvailableMaterialInterfaces.Num() > 0))
    {
        for(int Index = 0; Index < AvailableMaterialInterfaces.Num(); ++Index)
        {
            bool bIsValidMaterial = false;
            UMaterialInterface* MaterialInterface = AvailableMaterialInterfaces[Index];
            if (MaterialInterface != nullptr)
            {
                bIsValidMaterial = true;

                const UMaterial* LandscapeMaterial = MaterialInterface->GetMaterial_Concurrent();

                // In some case it's possible that the Material Instance we have and the Material are not related, for example, in case where content was force deleted, we can have a MIC with no parent, so GetMaterial will fallback to the default material.
                // and since the MIC is not really valid, fallback to 
                UMaterialInstance* MaterialInstance = Cast<UMaterialInstance>(MaterialInterface);
                bIsValidMaterial &= (MaterialInstance == nullptr) || MaterialInstance->IsChildOf(LandscapeMaterial);

                // Check usage flags : 
                bIsValidMaterial &= !bHasStaticLighting || MaterialInterface->CheckMaterialUsage_Concurrent(MATUSAGE_StaticLighting);
            }

            if (!bIsValidMaterial)
            {
                // Replace the landscape material by the default material : 
                MaterialInterface = UMaterial::GetDefaultMaterial(MD_Surface);
                AvailableMaterialInterfaces[Index] = MaterialInterface;
                AvailableMaterials[Index] = MaterialInterface->GetRenderProxy();
            }
        }
    }
    else
    {
        AvailableMaterialInterfaces.Add(UMaterial::GetDefaultMaterial(MD_Surface));
        AvailableMaterials.Add(AvailableMaterialInterfaces.Last()->GetRenderProxy());
    }

    (...)
}
Mesh Drawing

后续的绘制流程与UE Mesh Drawing Pipeline中所述基本一致,此处不再赘述

但需要注意的是,前述的渲染资源创建过程中只是设置了 LandscapeVertex 在当前 LandscapeComponent 中的索引位置。
在BasePass中,需要获取各 VertexFactory 的世界空间位置

// Engine\Shaders\Private\BasePassVertexShader.usf

void Main(
    FVertexFactoryInput Input,
    out FBasePassVSOutput Output
    (...))
{
    (...)

    FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input);
    float4 WorldPositionExcludingWPO = VertexFactoryGetWorldPosition(Input, VFIntermediates);
    float4 WorldPosition = WorldPositionExcludingWPO;

    (...)
}

各种 VertexFactory 都实现了各自的 GetVertexFactoryIntermediates 和 VertexFactoryGetWorldPosition 方法

LandscapeVertexFactory 中相关的主要函数的实现如下。在其中定义了采样高度图,并将顶点转换到世界空间等操作

// Engine\Shaders\Private\LandscapeVertexFactory.ush

FVertexFactoryIntermediates GetVertexFactoryIntermediates(FVertexFactoryInput Input)
{
    FVertexFactoryIntermediates Intermediates;
    Intermediates.SceneData = VF_GPUSCENE_GET_INTERMEDIATES(Input);
    Intermediates.ComponentIndex = GetComponentLinearIndex();
    Intermediates.LodValues = GetLodValues(Intermediates.ComponentIndex);
    Intermediates.LodBias = GetLodBias(Intermediates.ComponentIndex);
    Intermediates.InputPosition = Input.Position;

#if LANDSCAPE_TILE   
    Intermediates.InputPosition = Intermediates.InputPosition + Input.TileData;
    Intermediates.InputPosition.xy = min(Intermediates.InputPosition.xy, float2(Intermediates.LodValues.z, Intermediates.LodValues.z));
#endif

    float2 xy = Intermediates.InputPosition.xy * Intermediates.LodValues.w;

    float LODCalculated = CalcLOD(Intermediates.ComponentIndex, xy, Intermediates.InputPosition.zw);
    float LodValue = floor(LODCalculated);
    float MorphAlpha = LODCalculated - LodValue;

    // InputPositionLODAdjusted : Position for actual LOD in base LOD units
    float2 ActualLODCoordsInt = floor(Intermediates.InputPosition.xy * pow(2, -(LodValue - Intermediates.LodValues.x)));
    float InvLODScaleFactor = pow(2, -LodValue);

    // Base to Actual LOD, Base to Next LOD
    float2 CoordTranslate = float2( LandscapeParameters.SubsectionSizeVertsLayerUVPan.x * InvLODScaleFactor - 1, max(LandscapeParameters.SubsectionSizeVertsLayerUVPan.x * 0.5f * InvLODScaleFactor, 2) - 1 ) * LandscapeParameters.SubsectionSizeVertsLayerUVPan.y;
    float2 InputPositionLODAdjusted = ActualLODCoordsInt / CoordTranslate.x;

    // InputPositionNextLOD : Position for next LOD in base LOD units
    float2 NextLODCoordsInt = floor(ActualLODCoordsInt * 0.5);
    float2 InputPositionNextLOD = NextLODCoordsInt / CoordTranslate.y;

    // 计算采样纹理坐标
    float2 SampleCoords = InputPositionLODAdjusted * LandscapeParameters.HeightmapUVScaleBias.xy + LandscapeParameters.HeightmapUVScaleBias.zw + 0.5*LandscapeParameters.HeightmapUVScaleBias.xy + Intermediates.InputPosition.zw * LandscapeParameters.SubsectionOffsetParams.xy;
    // 采样高度图
    float4 SampleValue = Texture2DSampleLevel(LandscapeParameters.HeightmapTexture, LandscapeParameters.HeightmapTextureSampler, SampleCoords, LodValue-Intermediates.LodBias.x);
    float Height = DecodePackedHeight(SampleValue.xy);

    float2 SampleCoordsNextLOD = InputPositionNextLOD * LandscapeParameters.HeightmapUVScaleBias.xy + LandscapeParameters.HeightmapUVScaleBias.zw + 0.5*LandscapeParameters.HeightmapUVScaleBias.xy + Intermediates.InputPosition.zw * LandscapeParameters.SubsectionOffsetParams.xy;
    float4 SampleValueNextLOD = Texture2DSampleLevel(LandscapeParameters.HeightmapTexture, LandscapeParameters.HeightmapTextureSampler, SampleCoordsNextLOD, LodValue+1-Intermediates.LodBias.x);
    float HeightNextLOD = DecodePackedHeight(SampleValueNextLOD.xy);

#if LANDSCAPE_XYOFFSET // FEATURE_LEVEL >= FEATURE_LEVEL_SM4 only
    float2 SampleCoords2 = float2(InputPositionLODAdjusted * LandscapeParameters.WeightmapUVScaleBias.xy + LandscapeParameters.WeightmapUVScaleBias.zw + Intermediates.InputPosition.zw * LandscapeParameters.SubsectionOffsetParams.zz);
    float4 OffsetValue = Texture2DSampleLevel( LandscapeParameters.XYOffsetmapTexture, LandscapeParameters.XYOffsetmapTextureSampler, SampleCoords2, LodValue- Intermediates.LodBias.y );
    float2 SampleCoordsNextLOD2 = float2(InputPositionNextLOD * LandscapeParameters.WeightmapUVScaleBias.xy + LandscapeParameters.WeightmapUVScaleBias.zw + Intermediates.InputPosition.zw * LandscapeParameters.SubsectionOffsetParams.zz);
    float4 OffsetValueNextLOD = Texture2DSampleLevel( LandscapeParameters.XYOffsetmapTexture, LandscapeParameters.XYOffsetmapTextureSampler, SampleCoordsNextLOD2, LodValue+1-Intermediates.LodBias.y );
    float2 XYOffset = float2(((OffsetValue.r * 255.0 * 256.0 + OffsetValue.g * 255.0) - 32768.0) * XYOFFSET_SCALE, ((OffsetValue.b * 255.0 * 256.0 + OffsetValue.a * 255.0) - 32768.0) * XYOFFSET_SCALE );
    float2 XYOffsetNextLOD = float2(((OffsetValueNextLOD.r * 255.0 * 256.0 + OffsetValueNextLOD.g * 255.0) - 32768.0) * XYOFFSET_SCALE, ((OffsetValueNextLOD.b * 255.0 * 256.0 + OffsetValueNextLOD.a * 255.0) - 32768.0) * XYOFFSET_SCALE );

    InputPositionLODAdjusted = InputPositionLODAdjusted + XYOffset;
    InputPositionNextLOD = InputPositionNextLOD + XYOffsetNextLOD;
#endif

    Intermediates.LocalPosition = lerp( float3(InputPositionLODAdjusted, Height), float3(InputPositionNextLOD, HeightNextLOD), MorphAlpha );

    float2 Normal = float2(SampleValue.b, SampleValue.a);
    float2 NormalNextLOD = float2(SampleValueNextLOD.b, SampleValueNextLOD.a);
    float2 InterpNormal = lerp( Normal, NormalNextLOD, MorphAlpha ) * float2(2.0,2.0) - float2(1.0,1.0);
    Intermediates.WorldNormal = float3( InterpNormal, sqrt(max(1.0-dot(InterpNormal,InterpNormal),0.0)) );

    return Intermediates;
}

float4 VertexFactoryGetWorldPosition(FVertexFactoryInput Input, FVertexFactoryIntermediates Intermediates)
{
    FDFMatrix LocalToWorld = GetInstanceData(Intermediates).LocalToWorld;
    return TransformLocalToTranslatedWorld(GetLocalPosition(Intermediates), LocalToWorld);
}

float3 GetLocalPosition(FVertexFactoryIntermediates Intermediates)
{
    return Intermediates.LocalPosition+float3(Intermediates.InputPosition.zw * LandscapeParameters.SubsectionOffsetParams.ww,0);
}

渲染流程总结

在一帧中地形渲染大致会经历如下流程:

  • 在 GameThread 中进行地形子系统的 Tick,在其中会对 Heightmap, Weightmap 等数据进行更新;

Trace1

  • AddPrimitive,创建 LandscapeComponentSceneProxy 并收集地形材质,在渲染线程 AddPrimitiveCommand 中完成 LandscapeUniformShaderParameters 的设置;

Trace2

  • 在 FSceneRenderer::Render 的 InitViews 过程中,调用 FSceneRenderer::OnRenderBegin --> FLandscapeSceneViewExtension::PreRenderView_RenderThread --> FLandscapeRenderSystem::UpdateBuffers 完成 LandscapeSectionLODUniformParameters 的设置。在 InitViews 后续流程中完成 MeshDrawCommand 收集;

Trace3

  • 在 BassPass 中添加 RDG Pass,在最后 ExecutePass;

Trace4

  • 在 Pass 执行时创建 ParallelCommandListSet,创建并入队 FDrawVisibleMeshCommandsAnyThreadTask;

Trace5

  • FDrawVisibleMeshCommandsAnyThreadTask 任务是提交 MeshDrawCommand 至新命令队列,并将新命令队列加入 ParallelCommandListSet 中;
  • Pass 执行完毕退出前,ParallelCommandListSet 析构函数中进行 Dispatch 中,其中对其命令队列中的 MeshDrawCommand 进行异步转译,得到 RHICommand;

Trace6

  • 添加 RHI 线程任务提交得到的 RHIPlatformCommandList

Trace7

RenderDoc

Reference

Unreal Engine Documentation: Landscape Technical Guide

UE4地形系统(Landscape)

UE5引擎 PC端的Landscape渲染浅分析

UE4 地形 landscape

完美世界丁许朋:UE4开放世界ARPG《幻塔》技术分享