说明

本文为UE5-地形系统系列的第二篇文章,主要是对UE5地形系统在运行时的数据转换进行简要分析

概述

UE 中每个 Landscape 都会被划分成多个 LandscapeComponent
每个 Component 由单独1个 Section 或者2x2个 Section组成。

Component 中顶点的高度由高度图(ULandscapeComponent::HeightmapTexture)决定
Component 各层贴图的权重由权重图(ULandscapeComponent::WeightmapTextures)决定

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

class ULandscapeComponent : public UPrimitiveComponent
{
    GENERATED_UCLASS_BODY()

    (...)

    /** Heightmap texture reference */
    UPROPERTY()
    TObjectPtr<UTexture2D> HeightmapTexture;

    (...)

    /** Weightmap texture reference */
    UPROPERTY()
    TArray<TObjectPtr<UTexture2D>> WeightmapTextures;

    (...)
}

Heightmap

Heightmap数据组织形式

每个地形 Component 都有其各自的 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)));

顶点高度转换

UE 会在运行时对Heightmap进行采样,得到顶点高度,从而进一步产生地形 Mesh

地形的渲染资源创建过程中只是设置了 LandscapeVertex 在当前 LandscapeComponent 中的索引位置,因此在渲染过程中,需要将其转换为世界空间位置。而相关操作定义在 LandscapeVertexFactory.ush 中

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

VSIN

MeshOut

Weightmap

UE 可以创建多个 Landscape Layer,每一个 Layer 可以使用不同的材质,选择不同的 Layer 对地形进行绘制,绘制完成后,UE 会根据 Layer 的数量生成对应的 Weightmap,即材质权重图,其中保存了各个 Layer 材质混合的权重。

Weightmap数据组织形式

UE5 WeightMap的格式是 RGBA8,因此一张 Weightmap 至多能保存四个 Layer,因此增加 Layer 可能会额外生成 Weightmap,Layer 数量越多,显存消耗越大。在运行时,UE 会对当前地块的 Weightmap 和 Layer 纹理进行采样,并进行混合。不管地形块刷了多少层材质 Layers,最终地形某个点的各层权重总和为1.0(255)

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

class ULandscapeComponent : public UPrimitiveComponent
{
    GENERATED_UCLASS_BODY()

    (...)
private:

    /** Weightmap texture reference */
    UPROPERTY()
    TArray<TObjectPtr<UTexture2D>> WeightmapTextures;

    /** List of layers, and the weightmap and channel they are stored */
    UPROPERTY()
    TArray<FWeightmapLayerAllocationInfo> WeightmapLayerAllocations;

    (...)
}

每一个 Layer 使用哪一张 Weightmap 的哪一个通道,由 TArray<FWeightmapLayerAllocationInfo> WeightmapLayerAllocations 记录

其中 FWeightmapLayerAllocationInfo,中记录了该 Layer 对应的 WeightmapTexture 的索引和通道,并包含一个 ULandscapeLayerInfoObject 对象,其中记录了 Layer 名称、材质等信息

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

/** Stores information about which weightmap texture and channel each layer is stored */
USTRUCT()
struct FWeightmapLayerAllocationInfo
{
    GENERATED_USTRUCT_BODY()

    UPROPERTY()
    TObjectPtr<ULandscapeLayerInfoObject> LayerInfo;

    UPROPERTY()
    uint8 WeightmapTextureIndex;

    UPROPERTY()
    uint8 WeightmapTextureChannel;

    FWeightmapLayerAllocationInfo();
    FWeightmapLayerAllocationInfo(ULandscapeLayerInfoObject* InLayerInfo);

    bool operator == (const FWeightmapLayerAllocationInfo& RHS) const;
    FName GetLayerName() const;
    uint32 GetHash() const;
    void Free();
    bool IsAllocated() const { return (WeightmapTextureChannel != 255 && WeightmapTextureIndex != 255); }
};

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

UCLASS(MinimalAPI, BlueprintType)
class ULandscapeLayerInfoObject : public UObject
{
    GENERATED_UCLASS_BODY()

    UPROPERTY(VisibleAnywhere, Category = LandscapeLayerInfoObject, AssetRegistrySearchable)
    FName LayerName;

    UPROPERTY(EditAnywhere, Category = LandscapeLayerInfoObject, Meta = (DisplayName = "Physical Material", Tooltip = "Physical material to use when this layer is the predominant one at a given location. Note: this is ignored if the Landscape Physical Material node is used in the landscape material. "))
    TObjectPtr<UPhysicalMaterial> PhysMaterial;

    UPROPERTY(EditAnywhere, Category = LandscapeLayerInfoObject, Meta = (Tooltip = "The color to use for layer usage debug"))
    FLinearColor LayerUsageDebugColor;

    (...)
};

运行时地形权重混合

混合 6 个 Layer,使用 RenderDoc 抓帧并得到 PixelShader 代码,可以看到 6 个 Layer 的权重被存在两张 Weightmap 中,在 PS 中对两张 Weightmap 进行采样并对材质进行混合

WeightmapBlend

RenderDocResource

RenderDocPS

地形材质编译

地形材质的编译与其他材质大同小异 ,材质系统与整体编译流程见UE5-材质及其编译

简而言之,地形材质的编译也是从 FMaterial::BeginCompileShaderMap 开始,在其中新建shadermap、创建Translator、执行表达式转换、填充MaterialTemplate.ush、编译材质 shader 代码并存入 shadermap

// Engine\Source\Runtime\Engine\Private\Materials\MaterialShared.cpp

bool FMaterial::BeginCompileShaderMap(const FMaterialShaderMapId& ShaderMapId, const FStaticParameterSet &StaticParameterSet,
 EShaderPlatform Platform, TRefCountPtr<FMaterialShaderMap>& OutShaderMap, const ITargetPlatform* TargetPlatform)
{
    // 注意只在编辑器期间才会执行.
#if WITH_EDITORONLY_DATA
    bool bSuccess = false;
    // 新建shader map.
    TRefCountPtr<FMaterialShaderMap> NewShaderMap = new FMaterialShaderMap();

#if WITH_EDITOR
    NewShaderMap->AssociateWithAsset(GetAssetPath());
#endif
    
    // 生成材质shader代码.
    // 输出结果.
    FMaterialCompilationOutput NewCompilationOutput;
    // 转换器.
    FHLSLMaterialTranslator MaterialTranslator(this, NewCompilationOutput, StaticParameterSet, Platform,GetQualityLevel(), ShaderMapId.FeatureLevel, TargetPlatform);
    // 执行表达式转换, 填充到MaterialTemplate.ush.
    bSuccess = MaterialTranslator.Translate();

    // 表达式转换成功才需要执行后续操作.
    if(bSuccess)
    {
        // 为材质创建一个着色器编译环境,所有的编译作业将共享此材质.
        TRefCountPtr<FShaderCompilerEnvironment> MaterialEnvironment = new FShaderCompilerEnvironment();
        MaterialEnvironment->TargetPlatform = TargetPlatform;
        // 获取材质环境.
        MaterialTranslator.GetMaterialEnvironment(Platform, *MaterialEnvironment);
        // 获取材质shader代码.
        const FString MaterialShaderCode = MaterialTranslator.GetMaterialShaderCode();
        
        const bool bSynchronousCompile = RequiresSynchronousCompilation() || !GShaderCompilingManager->AllowAsynchronousShaderCompiling();

        // 包含虚拟的材质文件路径.
        MaterialEnvironment->IncludeVirtualPathToContentsMap.Add(TEXT("/Engine/Generated/Material.ush"), MaterialShaderCode);

        // 编译材质的shader代码.
        NewShaderMap->Compile(this, ShaderMapId, MaterialEnvironment, NewCompilationOutput, Platform, bSynchronousCompile);

        if (bSynchronousCompile) // 同步编译
        {
            // 同步模式, 直接赋值给OutShaderMap.
            OutShaderMap = NewShaderMap->CompiledSuccessfully() ? NewShaderMap : nullptr;
        }
        else // 异步编译
        {
            // 先将NewShaderMap放到等待编译结束的列表.
            OutstandingCompileShaderMapIds.AddUnique( NewShaderMap->GetCompilingId() );
            // 异步模式, OutShaderMap先设为null, 会回退到默认的材质.
            OutShaderMap = nullptr;
        }
    }

    return bSuccess;
#else
    UE_LOG(LogMaterial, Fatal,TEXT("Not supported."));
    return false;
#endif
}

地形系统中进行各 Layer 混合的材质表达式为 UMaterialExpressionLandscapeLayerBlend,也就是上面 LandscapeLayer Blend 节点所包含的表达式。

地形数据更新

当使用地形工具编辑地形时,会调用相应的 Apply 函数,其中会对相应 Layer 的 Heightmap 或 Weightmap 数据进行更新

其中最关键的是 UpdateTextureRegions 函数,其中使用 ENQUEUE_RENDER_COMMAND 向渲染线程入队更新 Texture 的命令,命令执行时调用 RHIUpdateTexture2D 进行 RHITexture 的更新

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

void UTexture2D::UpdateTextureRegions(int32 MipIndex, uint32 NumRegions, const FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, TFunction<void(uint8* SrcData, const FUpdateTextureRegion2D* Regions)> DataCleanupFunc)
{
    (...)

    FTexture2DResource* Texture2DResource = GetResource() ? GetResource()->GetTexture2DResource() : nullptr;
    if (!bTemporarilyDisableStreaming && IsStreamable())
    {
        UE_LOG(LogTexture, Log, TEXT("UpdateTextureRegions called for %s without calling TemporarilyDisableStreaming"), *GetPathName());
    }
    else if (Texture2DResource)
    {
        struct FUpdateTextureRegionsData
        {
            FTexture2DResource* Texture2DResource;
            int32 MipIndex;
            uint32 NumRegions;
            const FUpdateTextureRegion2D* Regions;
            uint32 SrcPitch;
            uint32 SrcBpp;
            uint8* SrcData;
        };

        FUpdateTextureRegionsData* RegionData = new FUpdateTextureRegionsData;

        RegionData->Texture2DResource = Texture2DResource;
        RegionData->MipIndex = MipIndex;
        RegionData->NumRegions = NumRegions;
        RegionData->Regions = Regions;
        RegionData->SrcPitch = SrcPitch;
        RegionData->SrcBpp = SrcBpp;
        RegionData->SrcData = SrcData;

        ENQUEUE_RENDER_COMMAND(UpdateTextureRegionsData)(
            [RegionData, DataCleanupFunc](FRHICommandListImmediate& RHICmdList)
            {
                for (uint32 RegionIndex = 0; RegionIndex < RegionData->NumRegions; ++RegionIndex)
                {
                    int32 CurrentFirstMip = RegionData->Texture2DResource->State.AssetLODBias;
                    if (RegionData->MipIndex >= CurrentFirstMip)
                    {
                        // Some RHIs don't support source offsets. Offset source data pointer now and clear source offsets
                        FUpdateTextureRegion2D RegionCopy = RegionData->Regions[RegionIndex];
                        const uint8* RegionSourceData = RegionData->SrcData
                            + RegionCopy.SrcY * RegionData->SrcPitch
                            + RegionCopy.SrcX * RegionData->SrcBpp;
                        RegionCopy.SrcX = 0;
                        RegionCopy.SrcY = 0;

                        RHIUpdateTexture2D(
                            RegionData->Texture2DResource->TextureRHI->GetTexture2D(),
                            RegionData->MipIndex - CurrentFirstMip,
                            RegionCopy,
                            RegionData->SrcPitch,
                            RegionSourceData);
                    }
                }

                // The deletion of source data may need to be deferred to the RHI thread after the updates occur
                RHICmdList.EnqueueLambda([RegionData, DataCleanupFunc](FRHICommandList&)
                {
                    DataCleanupFunc(RegionData->SrcData, RegionData->Regions);
                    delete RegionData;
                });
            });
    }
}

地形子系统在 Tick 时,调用 ALandscape::TickLayers,根据 EditLayers 中 Heightmap 和 Weightmap 的更新情况,来更新 Component 实际的 Heightmap 和 Weightmap

关键类型:

  • FTextureToComponentHelper:保存 heightmaps/weightmaps 到 components 的映射;
  • FUpdateLayersContentContext:收集 DirtyLandscapeComponents 和 NonDirtyLandscapeComponents,需要回读的 Heightmaps 和 Weightmaps,需要回读 Texture 的 Components 等上下文信息,在其 Refresh 函数中完成收集;(其中 NonDirtyLandscapeComponents,并不一定无需更新,因为 Heightmap 与相邻 Component 有关,相邻 Component 的更新可能使得当前 Component 也需要相应更新);
  • FLandscapeEditLayerReadback

关键函数:

  • ALandscape::ResolveLayersHeightmapTexture 和 ALandscape::ResolveLayersWeightmapTexture:使用 FLandscapeEditLayerReadback 类分别从 GPU 端读回 Heightmap 和 Weightmap;
  • UpdateAfterReadbackResolves:更新 LayerContentUpdateModes 标志位,以指示哪些内容需要更新;
  • RegenerateLayersHeightmaps:对于需要更新 Heightmaps 的 Components 重新生成 Heightmaps,并在最后完成各个 Layers 高度图的合并;
    • PerformLayersHeightmapsLocalMerge:
  • ResolveLayersWeightmapTexture: 对于需要更新 Weightmaps 的 Components 重新生成 Weightmaps;
  • UpdateAfterReadbackResolves:
    • UpdateForChangedHeightmaps:更新地形 Collision 数据;
    • UpdateForChangedWeightmaps:更新各个 Component 各 LOD 的材质实例。
// Engine\Source\Runtime\Landscape\Private\LandscapeEditLayers.cpp

void ALandscape::TickLayers(float DeltaTime)
{
    check(GIsEditor);

    if (!bEnableEditorLayersTick)
    {
        return;
    }

    UWorld* World = GetWorld();
    if (World && !World->IsPlayInEditor() && GetLandscapeInfo() && GEditor->PlayWorld == nullptr)
    {
        if (CVarLandscapeSimulatePhysics.GetValueOnAnyThread() == 1)
        {
            World->bShouldSimulatePhysics = true;
        }

        UpdateLayersContent();
    }
}


void ALandscape::UpdateLayersContent(bool bInWaitForStreaming, bool bInSkipMonitorLandscapeEdModeChanges, bool bIntermediateRender, bool bFlushRender)
{
    (...)

    // Gather mappings between heightmaps/weightmaps and components
    FTextureToComponentHelper MapHelper(*LandscapeInfo);

    // Poll and complete any outstanding resolve work
    // If bIntermediateRender then we want to flush all work here before we do the intermediate render later on
    // if bFlushRender then we skip this because we will flush later anyway
    if (bProcessReadbacks && (bIntermediateRender || !bFlushRender))
    {
        // These flags might look like they're being mixed up but they're not!
        const bool bDoIntermediateRender = false; // bIntermediateRender flag is for the work queued up this frame not the delayed resolves
        const bool bDoFlushRender = bIntermediateRender; // Flush before we do an intermediate render later in this frame

        TArray<FLandscapeEditLayerComponentReadbackResult> ComponentReadbackResults;

        // 
        ResolveLayersHeightmapTexture(MapHelper, MapHelper.Heightmaps, bDoIntermediateRender, bDoFlushRender, ComponentReadbackResults);
        ResolveLayersWeightmapTexture(MapHelper, MapHelper.Weightmaps, bDoIntermediateRender, bDoFlushRender, ComponentReadbackResults);
        LayerContentUpdateModes |= UpdateAfterReadbackResolves(ComponentReadbackResults);
    }

    if (LayerContentUpdateModes == 0 && !bForceRender)
    {
        return;
    }

    bool bUpdateAll = LayerContentUpdateModes & Update_All;
    bool bPartialUpdate = !bForceRender && !bUpdateAll && CVarLandscapeLayerOptim.GetValueOnAnyThread() == 1;

    FUpdateLayersContentContext UpdateLayersContentContext(MapHelper, bPartialUpdate);

    // Regenerate any heightmaps and weightmaps
    int32 ProcessedModes = 0;
    ProcessedModes |= RegenerateLayersHeightmaps(UpdateLayersContentContext);
    ProcessedModes |= RegenerateLayersWeightmaps(UpdateLayersContentContext);
    ProcessedModes |= (LayerContentUpdateModes & ELandscapeLayerUpdateMode::Update_Client_Deferred);
    ProcessedModes |= (LayerContentUpdateModes & ELandscapeLayerUpdateMode::Update_Client_Editing);

    // If we are flushing then read back resolved textures immediately
    if (bFlushRender || CVarLandscapeForceFlush.GetValueOnGameThread() != 0)
    {
        const bool bDoFlushRender = true;
        ResolveLayersHeightmapTexture(UpdateLayersContentContext.MapHelper, UpdateLayersContentContext.HeightmapsToResolve, bIntermediateRender, bDoFlushRender, UpdateLayersContentContext.AllLandscapeComponentReadbackResults);
        ResolveLayersWeightmapTexture(UpdateLayersContentContext.MapHelper, UpdateLayersContentContext.WeightmapsToResolve, bIntermediateRender, bDoFlushRender, UpdateLayersContentContext.AllLandscapeComponentReadbackResults);
    }

    // Clear processed mode flags
    LayerContentUpdateModes &= ~ProcessedModes;
    for (ULandscapeComponent* Component : UpdateLayersContentContext.AllLandscapeComponentsToResolve)
    {
        Component->ClearUpdateFlagsForModes(ProcessedModes);
    }

    // Apply post resolve updates
    const uint32 ToProcessModes = UpdateAfterReadbackResolves(UpdateLayersContentContext.AllLandscapeComponentReadbackResults);
    (...)
}


uint32 ALandscape::UpdateAfterReadbackResolves(const TArrayView<FLandscapeEditLayerComponentReadbackResult>& InComponentReadbackResults)
{
    TRACE_CPUPROFILER_EVENT_SCOPE(LandscapeLayers_PostResolve_Updates);

    uint32 NewUpdateFlags = 0;

    if (InComponentReadbackResults.Num())
    {
        UpdateForChangedHeightmaps(InComponentReadbackResults);
        UpdateForChangedWeightmaps(InComponentReadbackResults);

        GetLandscapeInfo()->UpdateAllAddCollisions();

        NewUpdateFlags |= UpdateCollisionAndClients(InComponentReadbackResults);
    }

    return NewUpdateFlags;
}

Reference

Unreal Engine Documentation: Landscape Technical Guide

UE4地形系统(Landscape)

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

UE4 地形 landscape

UE地形系统材质混合实现和Shader生成分析(UE5 5.2)