UE4 地形 landscape

UE4 Landscape Mobile

1. UE4 渲染流程

1.1 UE4 渲染线程

  • Game Tread(游戏线程) :游戏逻辑运算
  • Rendering Thread(渲染线程) :从 TaskGraph 中取出任务,并生成平台无关的 Command List (渲染指令列表)
  • RHI Thread (Render Hardware Interface 线程):会执行和转换渲染线程的 Command List 成为指定图形 API 的调用(称为Graphical Command),并提交到GPU执行。

stat

这3个线程处理的数据通常是不同帧的,譬如 GameThread 处理N帧数据,RenderThread 和 RHIThread 处理 N-1 帧数据。

但也存在例外,比如 RenderThread 和 RHIThread 运行很快,几乎不存在延迟,这种情况下,GameThread 处理N帧,而 RenderThread 可能处理N或N-1帧,RHIThread 也可能在转换N或N-1帧。

但是,RenderThread 不能落后游戏线程一帧,否则 GameThread 会卡住,直到 RenderThread 处理完所有指令。

1.2 渲染概念

  • UPrimitiveComponent : 场景中需要绘制的 Actor 都会有 UPrimitiveComponent,这个数据是存在于 Game Thread 中

  • FPrimitiveSceneProxy 和FPrimitiveSceneInfo :

    • FPrimitiveSceneProxy:Render thread 上UPrimitiveComponent 的对应代理,只包含渲染Primitive 所需的数据,但和UPrimitiveComponent 引用同样的数据

    • FPrimitiveSceneInfo:和FPrimitiveSceneProxy 一一对应,在引擎的Renderer 模块下

游戏线程渲染线程
UWorldFScene
UPrimitiveComponentFPrimitiveSceneProxy / FPrimitiveSceneInfo

2. Landscape 渲染流程

2.1 UE4 地形类结构

我们创建好地形后,场景中会有一个 Landscape 类型的 Actor

stat

ALandscape 类型继承关系如下

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

stat

ALandscape 继承 ALandscapeProxy,在这个基础上实现了更多功能。

2.2 UE4 地形渲染过程

2.2.1 地形组成结构

UE4 地形渲染是以 Component 为基础渲染单元的。我们新建了一个场景,然后创建一个地形,地形参数如下:

Landscape 由两个 Component 组成,然后运行中,我们断点获取场景中所有的 Actor,下面是调试信息:

找到 Landscape 对象后,我们查看它的 Component,发现当我们给地形设置了两个 Component 后,对应的 ALandscape 对象就会生成两个 LandscapeComponent 组件:

然后每个 LandscapeComponent 就是一个基础的渲染单元,如下是地形需要的类的继承关系图,

stat

2.2.2 创建 SceneProxy

按照之前介绍的 UE4 渲染流程,首先会调用 CreateSceneProxy 来创建 SceneProxy(这里对应的就是 FLandscapeComponentSceneProxy 跟 FLandscapeComponentSceneProxyMobile)。

stat

具体调用堆栈如下:

这里会判断的当前 renderer feature level 来创建对应的 Proxy,下面是对应的平台的 enum 定义:

namespace ERHIFeatureLevel
{
    enum Type
    {
        /**  OpenGL ES2. Deprecated */
        ES2_REMOVED,

        /**  OpenGL ES3.1 & Metal/Vulkan. */
        ES3_1,

        /**  DX10 Shader Model 4.
        * SUPPORT FOR THIS FEATURE LEVEL HAS BEEN ENTIRELY REMOVED. */
        SM4_REMOVED,

        /** DX11 Shader Model 5. */
        SM5,
        Num
    };
};

移动端跟 PC 端的区别:创建的 Proxy 分别是 FLandscapeComponFLandeneProxy 跟 FLandscapeComponentSceneProxyMobile,FLandscapeComponentSceneProxyMobile 是 FLandscapeComponFLandeneProxy 的子类
两者都会调用基类的构造函数,在构造函数中差异如下:

  • AvailableMaterials 来源
FLandscapeComponentSceneProxy::FLandscapeComponentSceneProxy(...)
{
    const auto FeatureLevel = GetScene().GetFeatureLevel();
    // PC
    if (FeatureLevel >= ERHIFeatureLevel::SM5)
    {
        if (InComponent->GetLandscapeProxy()->bUseDynamicMaterialInstance)
        {
            AvailableMaterials.Append(InComponent->MaterialInstancesDynamic);
        }
        else
        {
            AvailableMaterials.Append(InComponent->MaterialInstances);
        }
    }
    // Mobile
    else
    {
        AvailableMaterials.Append(InComponent->MobileMaterialInterfaces);
    }
}
  • SharedBuffersKey : 可以看到如果忽略掉 XYOffsetmapTexture,渲染平台,SharedBuffersKey 只由 SubsectionSizeQuads、NumSubsections 唯一确定。因为所有属于同一个 ALandscape 的 Component 的这两个参数都是一样的,所以这些 Component 的 Proxy 共用一个 SharedBuffersKey.

XYOffsetmapTexture : PC 可以传一张 XYOffsetmapTexture,后面阅读 shader 代码可以看出这个可以对顶点的 xy 坐标做偏移

// SharedBuffer 根据 SharedBufferKey 来创建
const int8 SubsectionSizeLog2 = FMath::CeilLogTwo(InComponent->SubsectionSizeQuads + 1);
SharedBuffersKey = (SubsectionSizeLog2 & 0xf) | ((NumSubsections & 0xf) << 4) |
    (FeatureLevel <= ERHIFeatureLevel::ES3_1 ? 0 : 1 << 30) |
    (XYOffsetmapTexture == nullptr ? 0 : 1 << 31);
  • HeightMap
// 
class FLandscapeNeighborInfo
{
    UTexture2D* HeightmapTexture; // PC : Heightmap, Mobile : Weightmap
}

class FLandscapeComponentSceneProxy : public FPrimitiveSceneProxy, 
    public FLandscapeNeighborInfo
{
}

if (FeatureLevel <= ERHIFeatureLevel::ES3_1)
{
    HeightmapTexture = nullptr;
    HeightmapSubsectionOffsetU = 0;
    HeightmapSubsectionOffsetV = 0;
}
else
{
    HeightmapSubsectionOffsetU = ((float)(InComponent->SubsectionSizeQuads + 1) / 
        (float)FMath::Max<int32>(1, HeightmapTexture->GetSizeX()));
    HeightmapSubsectionOffsetV = ((float)(InComponent->SubsectionSizeQuads + 1) / 
        (float)FMath::Max<int32>(1, HeightmapTexture->GetSizeY()));
}
  • WeightmapTextures 跟 NormalmapTexture
// PC
FLandscapeComponentSceneProxy::FLandscapeComponentSceneProxy(ULandscapeComponent* InComponent) :
    WeightmapTextures(InComponent->GetWeightmapTextures())
    , NormalmapTexture(InComponent->GetHeightmap())

// Mobile
FLandscapeComponentSceneProxyMobile::FLandscapeComponentSceneProxyMobile(ULandscapeComponent* InComponent)
{
    WeightmapTextures = InComponent->MobileWeightmapTextures;
    NormalmapTexture = InComponent->MobileWeightmapTextures[0];
}
  • HasTessellationEnabled : 手机不支持曲面细分
{
    for (UMaterialInterface*& MaterialInterface : AvailableMaterials)
    {

        bool HasTessellationEnabled = false;
        // PC
        if (FeatureLevel >= ERHIFeatureLevel::SM5)
        {
            HasTessellationEnabled = LandscapeMaterial->D3D11TessellationMode !=
                EMaterialTessellationMode::MTM_NoTessellation;
        }

        MaterialHasTessellationEnabled.Add(HasTessellationEnabled);
    }
}

// PC    : true
// Moble : false
bSupportsHeightfieldRepresentation = FeatureLevel <= 
    ERHIFeatureLevel::ES3_1 ? false : true;

class FPrimitiveSceneProxy {
    inline bool SupportsHeightfieldRepresentation() const 
    { return bSupportsHeightfieldRepresentation; }
}

2.2.3 创建渲染资源

创建完 Proxy 然后创建 PrimitiveSceneInfo 并且在渲染进程上创建渲染资源:

void FScene::AddPrimitive(UPrimitiveComponent* Primitive)
{
    // 上面的 CreateProxy
    FPrimitiveSceneProxy* PrimitiveSceneProxy = Primitive->CreateSceneProxy();
    Primitive->SceneProxy = PrimitiveSceneProxy;

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

    FCreateRenderThreadParameters Params =
    {
        PrimitiveSceneProxy,
        RenderMatrix,
        Primitive->Bounds,
        AttachmentRootPosition,
        Primitive->CalcBounds(FTransform::Identity)
    };

    // 放到渲染线程创建资源
    ENQUEUE_RENDER_COMMAND(AddPrimitiveCommand)(
        [Params = MoveTemp(Params), Scene, PrimitiveSceneInfo, PreviousTransform = MoveTemp(PreviousTransform)](FRHICommandListImmediate& RHICmdList)
        {
            FPrimitiveSceneProxy* SceneProxy = Params.PrimitiveSceneProxy;

            // SetTransform 这里 mark 一下
            SceneProxy->SetTransform(Params.RenderMatrix, Params.WorldBounds, 
                Params.LocalBounds, Params.AttachmentRootPosition);

            // 创建渲染资源
            SceneProxy->CreateRenderThreadResources();
            Scene->AddPrimitiveSceneInfo_RenderThread(PrimitiveSceneInfo, PreviousTransform);
        });
}

首先介绍下 SetTransform 具体做了哪些操作,这个会涉及到后续 Shader 中 Landscape 顶点计算过程,函数参数中包含了 Primitive 的一些基础属性:局部坐标转世界坐标的变换矩阵,包围盒等。

void FPrimitiveSceneProxy::SetTransform(
    const FMatrix& InLocalToWorld, 
    const FBoxSphereBounds& InBounds, 
    const FBoxSphereBounds& InLocalBounds, 
    FVector InActorPosition)
{
    LocalToWorld = InLocalToWorld;
    // 这里会创建 Primitive 的 UniformBufferObject
    UpdateUniformBuffer()
    {
        const FPrimitiveUniformShaderParameters PrimitiveUniformShaderParameters = 
            GetPrimitiveUniformShaderParameters(
                LocalToWorld, 
                PreviousLocalToWorld,
                ActorPosition, 
                Bounds, 
                LocalBounds, 
                PreSkinnedLocalBounds,
                bReceivesDecals, 
                HasDistanceFieldRepresentation(), 
                HasDynamicIndirectShadowCasterRepresentation(), 
                UseSingleSampleShadowFromStationaryLights(),
                bHasPrecomputedVolumetricLightmap,
                DrawsVelocity(), 
                GetLightingChannelMask(),
                LpvBiasMultiplier,
                PrimitiveSceneInfo ? PrimitiveSceneInfo->GetLightmapDataOffset() : 0,
                SingleCaptureIndex, 
                bOutputVelocity || AlwaysHasVelocity(),
                GetCustomPrimitiveData(),
                CastsContactShadow());

        if (UniformBuffer.GetReference())
        {
            UniformBuffer.UpdateUniformBufferImmediate(PrimitiveUniformShaderParameters);
        }
        else
        {
            UniformBuffer = TUniformBufferRef<FPrimitiveUniformShaderParameters>::CreateUniformBufferImmediate(PrimitiveUniformShaderParameters, UniformBuffer_MultiFrame);
        }
    }
}

// Primitive Uniform 参数定义
inline FPrimitiveUniformShaderParameters GetPrimitiveUniformShaderParameters(
    const FMatrix& LocalToWorld,
    const FMatrix& PreviousLocalToWorld,
    FVector ActorPosition,
    const FBoxSphereBounds& WorldBounds,
    const FBoxSphereBounds& LocalBounds,
    const FBoxSphereBounds& PreSkinnedLocalBounds,
    bool bReceivesDecals,
    bool bHasDistanceFieldRepresentation,   // Currently unused
    bool bHasCapsuleRepresentation,
    bool bUseSingleSampleShadowFromStationaryLights,
    bool bUseVolumetricLightmap,
    bool bDrawsVelocity,
    uint32 LightingChannelMask,
    float LpvBiasMultiplier,
    uint32 LightmapDataIndex,
    int32 SingleCaptureIndex,
    bool bOutputVelocity,
    const FCustomPrimitiveData* CustomPrimitiveData,
    bool bCastContactShadow = true
)
{
    FPrimitiveUniformShaderParameters Result;
    Result.LocalToWorld = LocalToWorld;
    Result.WorldToLocal = LocalToWorld.Inverse();
    // 省略一堆参数设置
    return Result;
}

接下来在渲染进程中调用函数 CreateRenderThreadResource 中初始化顶点 Buffer 以及 Shader 所需要的 UBO

stat

PC 端创建流程如下:

stat

  • SharedBuffers
    SharedBuffers 是根据 Proxy 构造时生成的 SharedBuffersKey 来创建,
SharedBuffers = FLandscapeComponentSceneProxy::SharedBuffersMap.FindRef(SharedBuffersKey);
if (SharedBuffers == nullptr)
{
    int32 NumOcclusionVertices = MobileRenderData->OccluderVerticesSP.IsValid() ? 
        MobileRenderData->OccluderVerticesSP->Num() : 0;
            
    SharedBuffers = new FLandscapeSharedBuffers(
        SharedBuffersKey, SubsectionSizeQuads, NumSubsections,
        GetScene().GetFeatureLevel(), false, NumOcclusionVertices);

    FLandscapeComponentSceneProxy::SharedBuffersMap.Add(SharedBuffersKey, SharedBuffers);
}
SharedBuffers->AddRef();

FLandscapeSharedBuffer 创建时,会新建 VertexIndex Buff。

FLandscapeSharedBuffers::FLandscapeSharedBuffers(...)
    :  NumIndexBuffers(FMath::CeilLogTwo(InSubsectionSizeQuads + 1))
{
    // SubsectionSizeVerts 7 * 7 : 8 | 15 * 15 : 16
    // NumSubsections      2 * 2 : 2 | 1 * 1 : 1
    NumVertices = FMath::Square(SubsectionSizeVerts) * 
        FMath::Square(NumSubsections);

    // PC 
    // Mobile 的 VertextBuffer 在 
    // FLandscapeComponentSceneProxyMobile::MobileRenderData.VertexBuffer
    if (InFeatureLevel > ERHIFeatureLevel::ES3_1)
    {
        // Vertex Buffer cannot be shared
        VertexBuffer = new FLandscapeVertexBuffer(InFeatureLevel, 
            NumVertices, SubsectionSizeVerts, NumSubsections);
    }

    // 7 -> 3
    // 15 -> 4
    IndexBuffers = new FIndexBuffer*[NumIndexBuffers];

    if (NumVertices > 65535)
    {
        bUse32BitIndices = true;
        CreateIndexBuffers<uint32>(InFeatureLevel, bRequiresAdjacencyInformation);
    }
    else
    {
        CreateIndexBuffers<uint16>(InFeatureLevel, bRequiresAdjacencyInformation);
    }
}

CreateIndexBuffer 函数大致如下:

int32 MaxLOD = NumIndexBuffers - 1;

// 逐 LOD
for (int32 Mip = MaxLOD; Mip >= 0; Mip--)
{
    // 每个 Section 2 * 2/ 1 * 1
    for (int32 SubY = 0; SubY < NumSubsections; SubY++)
    {
        for (int32 SubX = 0; SubX < NumSubsections; SubX++)
        {
            // 逐 Quad 遍历
        }
    }
}

顶点排列顺序如下

  • Component 只有一个 Section,每个 Section 有 15 * 15 个 Quad 时
  • Component 有 2 * 2 个 Section,每个 Section 有 7 * 7 个 Quad 时:

Mobile 计算就稍微有点复杂了,需要计算两个变量 LodSubsectionSizeQuads 和 MipRatio。

假如 Section 构成是 15 * 15,则

NumLOD = NumIndexBuffers // IndexBuffer 数量
       = FMath::CeilLogTwo(InSubsectionSizeQuads + 1)
       = log(2, 15 + 1)

MaxLOD = NumLOD - 1
       = 4 - 1 = 3  // LOD(0, 1, 2, 3)

LodSubsectionSizeQuads = (SubsectionSizeVerts >> Mip) - 1;
                       = (16 >> LOD) - 1

MipRatio = (float)SubectionSizeQuads / (float)LodSubsectionSizeQuads;
         = 15.0 / LodSubsectionSizeQuads

于是有如下表格:

LODLodSubsectionSizeQuadsMipRatio
3115.00
235.00
172.143
0151.00

则 7 * 7 Section 的表格如下:

LODLodSubsectionSizeQuadsMipRatio
217.00
132.33
071.00

最终计算得出的 LOD 如下:

  • LOD2:
  • LOD1:
  • LOD0:

随后构造 FLandscapeVertexFactoryMobile,主要是用来定义如何将顶点数据以正确的格式发送到 GPU。

class FLandscapeVertexFactory : public FVertexFactory
{
    struct FDataType
    {
        /** The stream to read the vertex position from. */
        FVertexStreamComponent PositionComponent;
    };
}

class FLandscapeVertexFactoryMobile : public FLandscapeVertexFactory
{
    struct FDataType : FLandscapeVertexFactory::FDataType
    {
        /** stream which has heights of each LOD levels */
        TArray<FVertexStreamComponent,TFixedAllocator<LANDSCAPE_MAX_ES_LOD_COMP> > LODHeightsComponent;
    };
}

#define LANDSCAPE_MAX_ES_LOD_COMP   2
#define LANDSCAPE_MAX_ES_LOD        6

struct FLandscapeMobileVertex
{
    uint8 Position[4]; // Pos + LOD 0 Height
    uint8 LODHeights[LANDSCAPE_MAX_ES_LOD_COMP*4];
};

void FLandscapeComponentSceneProxyMobile::CreateRenderThreadResources()
{
    // Init vertex buffer
    {
        check(MobileRenderData->VertexBuffer);
        MobileRenderData->VertexBuffer->InitResource();

        FLandscapeVertexFactoryMobile* LandscapeVertexFactory = new FLandscapeVertexFactoryMobile(FeatureLevel);
        LandscapeVertexFactory->MobileData.PositionComponent = FVertexStreamComponent(MobileRenderData->VertexBuffer, 
            STRUCT_OFFSET(FLandscapeMobileVertex, Position), sizeof(FLandscapeMobileVertex), VET_UByte4N);

        for (uint32 Index = 0; Index < LANDSCAPE_MAX_ES_LOD_COMP; ++Index)
        {
            LandscapeVertexFactory->MobileData.LODHeightsComponent.Add(FVertexStreamComponent(MobileRenderData->VertexBuffer, 
                STRUCT_OFFSET(FLandscapeMobileVertex, LODHeights) + sizeof(uint8) * 4 * Index, 
                sizeof(FLandscapeMobileVertex), VET_UByte4N));
        }

        LandscapeVertexFactory->InitResource();
        VertexFactory = LandscapeVertexFactory;
    }
}

分两个 FVertexStreamComponent:PositionComponent 和 LODHeightsComponent.

  • PositionComponent 对应 STRUCT_OFFSET(FLandscapeMobileVertex, Position),即来源为 MobileRenderData->VertexBuffer 的每个顶点数据 (FLandscapeMobileVertex)的高度 Field

  • LODHeightsComponent 对应 STRUCT_OFFSET(FLandscapeMobileVertex, LODHeights) + sizeof(uint8) * 4 * Index,即来源为 MobileRenderData->VertexBuffer 的每个顶点数据(FLandscapeMobileVertex) 的 LOD 高度(LODHeights)数据,加上此 LOD 的偏移,一共有多少 LOD 就有多少 FVertexStreamComponent 被添加到了 LODHeightsComponent. 这里的 MobileRenderData 就是FLandscapeComponentSceneProxyMobile::MobileRenderData,之前从 Platform 反序列化来的。

这两个分别对应 Shader 里的参数 PackedPosition 跟 LODHeights

// Engine\Shaders\Private\LandscapeVertexFactory.ush
struct FVertexFactoryInput
{
#if FEATURE_LEVEL >= FEATURE_LEVEL_SM4
    float4 Position: ATTRIBUTE0;
#else
    float4 PackedPosition: ATTRIBUTE0; // 
    float4 LODHeights[2]: ATTRIBUTE1;  
#endif
};

这里详细解释一下 LODHeights 中的数据
LODHeights:每个顶点的各LOD高度数组,这样编码:
LODHeights[0].x:MinHeight >> 8,其中 MinHeight 为此顶点的所有LOD高度的最小值
LODHeights[0].y:MaxHeight >> 8,其中 MaxHeight 为此顶点的所有LOD高度的最大值
LODHeights[0].zw ~ LODHeight[1].xyzw 为 LOD0~LOD5 的高度值,被归一化到了 LODHeights[0] 到 LODHeight[1] 之间,后面可以看到,在 shader 里会反向解码这些数据。

2.2.4 顶点数据

VertextBuffer 里的数据存储如下:Position 是由 4 个 float 组成的,数据结构如下:

struct FLandscapeVertex
{
    float VertexX; // 对应到 Section 中的 x
    float VertexY; // 对应到 Section 中的 y
    float SubX;    // Component 中 Section 位置
    float SubY;    
};

VertextX/VertexY 表示的是顶点在 Section 中的位置:

SubX/SubY 表示的是 Section 在 Component 中的位置

在 PC 下是在创建 FLandscapeSharedBuffers 时创建,然后通过调用 FLandscapeVertextBuffer::InitRHI,新建的,可以看到顶点是逐 Section 生成的。

void FLandscapeVertexBuffer::InitRHI()
{
    FRHIResourceCreateInfo CreateInfo;
    void* BufferData = nullptr;
    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 = x;
                    Vertex->VertexY = y;
                    Vertex->SubX = SubX;
                    Vertex->SubY = SubY;
                    Vertex++;
                    VertexIndex++;
                }
            }
        }
    }
}

RenderDoc 中抓帧数据如下:对应到 ATTRIBUTE 中的数据,下面这个顶点是 Section 0,0 下的顶点 7,2

Mobile 下则是读取 PlatformData 中的数据,不过这里的数据需要乘以 255 才能得出最终的坐标值:

最终地表的顶点数据是在 LandscapeVertextFactory.ush 中生成的,PC 通过 VertexBuffers 跟 HeightMapTexture 生成最终的 Mesh 顶点,Mobile 中的顶点跟高度数据通过读取 PlatformData 中的数据,分别将 VertextBuffers 跟高度数据传给 Shader 计算。

2.2.5 vertext shader

下面是从 RenderDoc 抓帧查看 Shader 代码逻辑

下面列出 vertext shader 主要逻辑,GetVertexFactoryIntermediates 。

// Engine\Shaders\Private\LandscapeVertexFactory.ush
struct FVertexFactoryInput
{
#if FEATURE_LEVEL >= FEATURE_LEVEL_SM4
    float4 Position: ATTRIBUTE0;
#else
    float4 PackedPosition: ATTRIBUTE0; // 
    float4 LODHeights[2]: ATTRIBUTE1;  
#endif
};

// mobile
#define TERRAIN_ZSCALE (1.0f/128.0f)

FVertexFactoryIntermediates GetVertexFactoryIntermediates(FVertexFactoryInput Input)
{
    FVertexFactoryIntermediates Intermediates;
    // 从 PackedPosition 中 xy 读取顶点在 Section 中的位置
    Intermediates.InputPosition.xy = Input.PackedPosition.xy * 255.f;

    // PackedPosition.z 最后两位存储 Section 编号 00,01,10,11
    uint PackedExtraData = (uint)(Input.PackedPosition.z * 255.f);
    float SubX = (float)((PackedExtraData >> 1) & 1);
    float SubY = (float)((PackedExtraData >> 0) & 1);

    Intermediates.InputPosition.zw = float2(SubX, SubY);

    // 计算高度
    float MinHeight = DecodePackedHeight(float2(Input.LODHeights[0].x, Input.LODHeights[0].y));
    float HeightDelta = Input.PackedPosition.w * 255.0 * 256.0 * TERRAIN_ZSCALE;

    // 忽略 LOD 计算过程 LODHeightIndex 是计算结果
    float InputHeight = Input.LODHeights[LODHeightIndex >> 2][LODHeightIndex & 3];

    float Height = MinHeight + InputHeight * HeightDelta;

    // 计算 xy
    float InvLODScaleFactor = 1.f / (float)(1 << (uint)LodValue);
    // LodValues.x is always 0 on mobile.
    float2 ActualLODCoordsInt = floor(Intermediates.InputPosition.xy * InvLODScaleFactor);

    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;

    // InputPositionLODAdjusted 怎么算出来的还没弄懂?
    Intermediates.LocalPosition = lerp( float3(InputPositionLODAdjusted, Height), 
        float3(InputPositionNextLOD, HeightNextLOD), MorphAlpha );
    return Intermediates;
}

然后下面是 PC 上的逻辑

FVertexFactoryIntermediates GetVertexFactoryIntermediates(FVertexFactoryInput Input)
{
    FVertexFactoryIntermediates Intermediates;
#if FEATURE_LEVEL >= FEATURE_LEVEL_SM4
    Intermediates.InputPosition = Input.Position;
#endif
    // 计算采样纹理坐标
    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);

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

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

float4 VertexFactoryGetWorldPosition(FVertexFactoryInput Input, FVertexFactoryIntermediates Intermediates)
{
    return INVARIANT(TransformLocalToTranslatedWorld(GetLocalPosition(Intermediates)));
}

然后是将局部坐标转成世界坐标

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

float4 VertexFactoryGetWorldPosition(FVertexFactoryInput Input, 
    FVertexFactoryIntermediates Intermediates)
{
    return INVARIANT(TransformLocalToTranslatedWorld(GetLocalPosition(Intermediates)));
}

// Engine\Shaders\Private\VertexFactoryCommon.ush
float4 TransformLocalToTranslatedWorld(float3 LocalPosition)
{
    float3 RotatedPosition = Primitive.LocalToWorld[0].xyz * LocalPosition.xxx + 
        Primitive.LocalToWorld[1].xyz * LocalPosition.yyy + 
        Primitive.LocalToWorld[2].xyz * LocalPosition.zzz;
    
    return float4(RotatedPosition + (Primitive.LocalToWorld[3].xyz + 
        ResolvedView.PreViewTranslation.xyz),1);
}

下面是 Uniform 结构体定义,对应 Shader 中的 LandscapeParameters

/** The uniform shader parameters for a landscape draw call. */
BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT(FLandscapeUniformShaderParameters, LANDSCAPE_API)
SHADER_PARAMETER(int32, ComponentBaseX)
SHADER_PARAMETER(int32, ComponentBaseY)
SHADER_PARAMETER(int32, SubsectionSizeVerts)
SHADER_PARAMETER(int32, NumSubsections)
SHADER_PARAMETER(int32, LastLOD)
SHADER_PARAMETER(FVector4, HeightmapUVScaleBias)
SHADER_PARAMETER(FVector4, WeightmapUVScaleBias)
SHADER_PARAMETER(FVector4, LandscapeLightmapScaleBias)
SHADER_PARAMETER(FVector4, SubsectionSizeVertsLayerUVPan)
SHADER_PARAMETER(FVector4, SubsectionOffsetParams)
SHADER_PARAMETER(FVector4, LightmapSubsectionOffsetParams)
    SHADER_PARAMETER(FVector4, BlendableLayerMask)
SHADER_PARAMETER(FMatrix, LocalToWorldNoScaling)
SHADER_PARAMETER_TEXTURE(Texture2D, HeightmapTexture)
SHADER_PARAMETER_SAMPLER(SamplerState, HeightmapTextureSampler)
SHADER_PARAMETER_TEXTURE(Texture2D, NormalmapTexture)
SHADER_PARAMETER_SAMPLER(SamplerState, NormalmapTextureSampler)
SHADER_PARAMETER_TEXTURE(Texture2D, XYOffsetmapTexture)
SHADER_PARAMETER_SAMPLER(SamplerState, XYOffsetmapTextureSampler)
END_GLOBAL_SHADER_PARAMETER_STRUCT()

// 在下面函数会对这个 Uniform 进行赋值
void FLandscapeComponentSceneProxy::OnTransformChanged()
{
    // Set FLandscapeUniformVSParameters for this subsection
    FLandscapeUniformShaderParameters LandscapeParams;
    LandscapeParams.ComponentBaseX = ComponentBase.X;
    LandscapeParams.ComponentBaseY = ComponentBase.Y;
    LandscapeParams.SubsectionSizeVerts = SubsectionSizeVerts;
    LandscapeParams.NumSubsections = NumSubsections;
    LandscapeParams.LastLOD = LastLOD;
    LandscapeParams.HeightmapUVScaleBias = HeightmapScaleBias;
    LandscapeParams.WeightmapUVScaleBias = WeightmapScaleBias;
}

RenderDoc 抓帧查看 Uniform Buff 数据

Primitive 数据如下:

Landscape 参数

最终计算得出的 Mesh 如下:

参考文献

1.UE4 Mobile Landscape 总览及源码解析

2.UE4移动端地形理解 - 高度LOD