说明

本文为UE5-地形系统系列的第三篇文章,主要是对UE5地形系统中顶点的LOD机制以及Continuous LOD算法进行分析

LOD Distribution

游戏线程中

地形的 LOD 由以下变量影响:

LODDistribution

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

UCLASS(Abstract, MinimalAPI, NotBlueprintable, NotPlaceable, hidecategories=(Display, Attachment, Physics, Debug, Lighting), showcategories=(Lighting, Rendering, Transformation), hidecategories=(Mobility))
class ALandscapeProxy : public APartitionActor, public ILandscapeSplineInterface
{
    GENERATED_BODY()

public:
    (...)

    /** This is the starting screen size used to calculate the distribution. You can increase the value if you want less LOD0 component, and you use very large landscape component. */
    UPROPERTY(EditAnywhere, Category = "LOD Distribution", meta = (EditCondition = "!bUseScalableLODSettings", DisplayName = "LOD 0 Screen Size", ClampMin = "0.1", ClampMax = "10.0", UIMin = "0.1", UIMax = "10.0", LandscapeInherited))
    float LOD0ScreenSize = 0.5f;

    /** The distribution setting used to change the LOD 0 generation, 1.25 is the normal distribution, numbers influence directly the LOD0 proportion on screen. */
    UPROPERTY(EditAnywhere, Category = "LOD Distribution", meta = (EditCondition = "!bUseScalableLODSettings", DisplayName = "LOD 0", ClampMin = "1.0", ClampMax = "10.0", UIMin = "1.0", UIMax = "10.0", LandscapeInherited))
    float LOD0DistributionSetting = 1.25f;

    /** The distribution setting used to change the LOD generation, 3 is the normal distribution, small number mean you want your last LODs to take more screen space and big number mean you want your first LODs to take more screen space. */
    UPROPERTY(EditAnywhere, Category = "LOD Distribution", meta = (EditCondition = "!bUseScalableLODSettings", DisplayName = "Other LODs", ClampMin = "1.0", ClampMax = "10.0", UIMin = "1.0", UIMax = "10.0", LandscapeInherited))
    float LODDistributionSetting = 3.0f;

    /** Scalable (per-quality) version of 'LOD 0 Screen Size'. */
    UPROPERTY(EditAnywhere, Category = "LOD Distribution", meta = (EditCondition = "bUseScalableLODSettings", DisplayName = "Scalable LOD 0 Screen Size", ClampMin = "0.1", ClampMax = "10.0", UIMin = "0.1", UIMax = "10.0", LandscapeInherited))
    FPerQualityLevelFloat ScalableLOD0ScreenSize = 0.5f;

    /** Scalable (per-quality) version of 'LOD 0'. */
    UPROPERTY(EditAnywhere, Category = "LOD Distribution", meta = (EditCondition = "bUseScalableLODSettings", DisplayName = "Scalable LOD 0", ClampMin = "1.0", ClampMax = "10.0", UIMin = "1.0", UIMax = "10.0", LandscapeInherited))
    FPerQualityLevelFloat ScalableLOD0DistributionSetting = 1.25f;

    /** Scalable (per-quality) version of 'Other LODs'. */
    UPROPERTY(EditAnywhere, Category = "LOD Distribution", meta = (EditCondition = "bUseScalableLODSettings", DisplayName = "Scalable Other LODs", ClampMin = "1.0", ClampMax = "10.0", UIMin = "1.0", UIMax = "10.0", LandscapeInherited))
    FPerQualityLevelFloat ScalableLODDistributionSetting = 3.0f;

    /** Allows to specify LOD distribution settings per quality level. Using this will ignore the r.LandscapeLOD0DistributionScale CVar. */
    UPROPERTY(EditAnywhere, Category = "LOD Distribution", meta = (LandscapeInherited))
    bool bUseScalableLODSettings = false;

    /** This controls the area that blends LOD between neighboring sections. At 1.0 it blends across the entire section, and lower numbers reduce the blend region to be closer to the boundary. */
    UPROPERTY(EditAnywhere, Category = "LOD Distribution", meta = (DisplayName = "Blend Range", ClampMin = "0.01", ClampMax = "1.0", UIMin = "0.01", UIMax = "1.0", LandscapeInherited))
    float LODBlendRange = 1.0f;
    (...)
};

渲染线程中

在构造 FLandscapeComponentSceneProxy 时,根据以上 LOD 参数计算得到 LODSettings 中的成员

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

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

    const ALandscapeProxy* Proxy = InComponent->GetLandscapeProxy();

    float LOD0ScreenSize;
    float LOD0Distribution;
    float LODDistribution;
    VirtualShadowMapConstantDepthBias = Proxy->NonNaniteVirtualShadowMapConstantDepthBias;
    VirtualShadowMapInvalidationHeightErrorThreshold = Proxy->NonNaniteVirtualShadowMapInvalidationHeightErrorThreshold;
    float NonNaniteVirtualShadowMapInvalidationScreenSizeLimit = Proxy->NonNaniteVirtualShadowMapInvalidationScreenSizeLimit;

    if (Proxy->bUseScalableLODSettings)
    {
        const int32 LandscapeQuality = Scalability::GetQualityLevels().LandscapeQuality;
        
        LOD0ScreenSize = Proxy->ScalableLOD0ScreenSize.GetValue(LandscapeQuality);
        LOD0Distribution = Proxy->ScalableLOD0DistributionSetting.GetValue(LandscapeQuality);
        LODDistribution = Proxy->ScalableLODDistributionSetting.GetValue(LandscapeQuality);
    }
    else
    {
        LOD0ScreenSize = Proxy->LOD0ScreenSize;
        LOD0Distribution = Proxy->LOD0DistributionSetting * GLandscapeLOD0DistributionScale;
        LODDistribution = Proxy->LODDistributionSetting * GLandscapeLODDistributionScale;
    }

    (...)

    // Precompute screen ratios for each LOD level : 
    {
        float ScreenSizeRatioDivider = FMath::Max(LOD0Distribution, 1.01f);
        // Cancel out so that landscape is not affected by r.StaticMeshLODDistanceScale
        float CurrentScreenSizeRatio = LOD0ScreenSize / CVarStaticMeshLODDistanceScale.GetValueOnAnyThread();

        LODScreenRatioSquared.AddUninitialized(MaxLOD + 1);

        // LOD 0 handling
        LODScreenRatioSquared[0] = FMath::Square(CurrentScreenSizeRatio);
        LODSettings.LOD0ScreenSizeSquared = FMath::Square(CurrentScreenSizeRatio);
        CurrentScreenSizeRatio /= ScreenSizeRatioDivider;
        LODSettings.LOD1ScreenSizeSquared = FMath::Square(CurrentScreenSizeRatio);
        ScreenSizeRatioDivider = FMath::Max(LODDistribution, 1.01f);
        LODSettings.LODOnePlusDistributionScalarSquared = FMath::Square(ScreenSizeRatioDivider);

        // Other LODs
        for (int32 LODIndex = 1; LODIndex <= MaxLOD; ++LODIndex) // This should ALWAYS be calculated from the component size, not user MaxLOD override
        {
            LODScreenRatioSquared[LODIndex] = FMath::Square(CurrentScreenSizeRatio);
            CurrentScreenSizeRatio /= ScreenSizeRatioDivider;
        }
    }

    FirstLOD = 0;
    LastLOD = MaxLOD;   // we always need to go to MaxLOD regardless of LODBias as we could need the lowest LODs due to streaming.

    // Make sure out LastLOD is > of MinStreamedLOD otherwise we would not be using the right LOD->MIP, the only drawback is a possible minor memory usage for overallocating static mesh element batch
    const int32 MinStreamedLOD = HeightmapTexture ? FMath::Min<int32>(HeightmapTexture->GetNumMips() - HeightmapTexture->GetNumResidentMips(), FMath::CeilLogTwo(SubsectionSizeVerts) - 1) : 0;
    LastLOD = FMath::Max(MinStreamedLOD, LastLOD);

    // Clamp to MaxLODLevel
    const int32 MaxLODLevel = InComponent->GetLandscapeProxy()->MaxLODLevel;
    if (MaxLODLevel >= 0)
    {
        MaxLOD = FMath::Min<int8>(static_cast<int8>(MaxLODLevel), MaxLOD);
        LastLOD = FMath::Min<int32>(MaxLODLevel, LastLOD);
    }

    // Clamp ForcedLOD to the valid range and then apply
    int8 ForcedLOD = static_cast<int8>(InComponent->ForcedLOD);
    ForcedLOD = static_cast<int8>(ForcedLOD >= 0 ? FMath::Clamp<int32>(ForcedLOD, FirstLOD, LastLOD) : ForcedLOD);
    FirstLOD = ForcedLOD >= 0 ? ForcedLOD : FirstLOD;
    LastLOD = ForcedLOD >= 0 ? ForcedLOD : LastLOD;

    LODSettings.LastLODIndex = static_cast<int8>(LastLOD);
    LODSettings.LastLODScreenSizeSquared = LODScreenRatioSquared[LastLOD];
    LODSettings.ForcedLOD = ForcedLOD;

    (...)
}

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

struct FLandscapeRenderSystem
{
    struct LODSettingsComponent
    {
        float LOD0ScreenSizeSquared;
        float LOD1ScreenSizeSquared;
        float LODOnePlusDistributionScalarSquared;
        float LastLODScreenSizeSquared;
        float VirtualShadowMapInvalidationLimitLOD;
        int8 LastLODIndex;
        int8 ForcedLOD;
        int8 DrawCollisionPawnLOD;
        int8 DrawCollisionVisibilityLOD;
    };

    (...)
};

VertexFactory

地形顶点工厂的定义如下

// Engine\Shaders\Private\LandscapeVertexFactory.ush

struct FVertexFactoryInput
{
    // UByte4
    uint4 Position : ATTRIBUTE0;

#if LANDSCAPE_TILE
    // UByte4
    uint4 TileData : ATTRIBUTE1;
#else
    // Dynamic instancing related attributes with InstanceIdOffset : ATTRIBUTE1
    VF_GPUSCENE_DECLARE_INPUT_BLOCK(1)
#endif
    VF_INSTANCED_STEREO_DECLARE_INPUT_BLOCK()
};

Position.xy 为该顶点在所属 Section 中的局部坐标,Position.zw 为顶点所属 Section 在其 Component 中的坐标。

LandscapeComponentInfo

VSInput

VSOutput

LOD顶点数计算示例

VertexCal

Component 的 LOD 增加 1 级,其 Section X和Y方向上的顶点数减半

LOD 0
VertexCalVSInputLOD0

VertexNum=(641)(641)226=95256VertexNum=(64-1)\cdot(64-1)\cdot2\cdot2\cdot6=95256

LOD 1
VertexCalVSInputLOD1
VertexNum=(321)(321)226=23064VertexNum=(32-1)\cdot(32-1)\cdot2\cdot2\cdot6=23064

Uniform Parameters

Uniform Shader Parameters

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

/** 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(uint32, VirtualTexturePerPixelHeight)
    SHADER_PARAMETER(float, InvLODBlendRange)
    SHADER_PARAMETER(float, NonNaniteVirtualShadowMapConstantDepthBias)
    SHADER_PARAMETER(FVector4f, HeightmapTextureSize)
    SHADER_PARAMETER(FVector4f, HeightmapUVScaleBias)
    SHADER_PARAMETER(FVector4f, WeightmapUVScaleBias)
    SHADER_PARAMETER(FVector4f, LandscapeLightmapScaleBias)
    SHADER_PARAMETER(FVector4f, SubsectionSizeVertsLayerUVPan)
    SHADER_PARAMETER(FVector4f, SubsectionOffsetParams)
    SHADER_PARAMETER(FVector4f, LightmapSubsectionOffsetParams)
    SHADER_PARAMETER(FMatrix44f, 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()

LOD Uniform Parameters

LOD相关的Uniform Buffer 的声明和实现,在 Shader 中对应的 UniformBuffer 为 LandscapeContinuousLODParameters

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

BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT(FLandscapeSectionLODUniformParameters, LANDSCAPE_API)
    SHADER_PARAMETER(int32, LandscapeIndex) // Landscape索引
    SHADER_PARAMETER(FIntPoint, Min) //
    SHADER_PARAMETER(FIntPoint, Size)
    SHADER_PARAMETER_SRV(Buffer<float>, SectionLODBias)
END_GLOBAL_SHADER_PARAMETER_STRUCT()

// Engine\Source\Runtime\Landscape\Private\LandscapeRender.cpp
IMPLEMENT_GLOBAL_SHADER_PARAMETER_STRUCT(FLandscapeSectionLODUniformParameters, "LandscapeContinuousLODParameters");

在场景中随着相机的移动,Component 的 LOD 会发生变化,因此 LOD Uniform Buffer 中的数据需要每帧更新

在 FSceneRenderer::OnRenderBegin 中调用 FLandscapeSceneViewExtension::PreRenderView_RenderThread,在其中创建并向渲染线程入队计算更新 LOD 的任务

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

void FLandscapeSceneViewExtension::PreRenderView_RenderThread(FRDGBuilder& GraphBuilder, FSceneView& InView)
{
    LandscapeViews.Emplace(InView);

    (...)

    TArray<FLandscapeRenderSystem*> SceneLandscapeRenderSystems = GetLandscapeRenderSystems(InView.Family->Scene);

    // Kick the job once all views have been collected.
    if (!SceneLandscapeRenderSystems.IsEmpty() && LandscapeViews.Num() == InView.Family->AllViews.Num())
    {
        LandscapeSetupTask = GraphBuilder.AddCommandListSetupTask([SceneRenderSystems = MoveTemp(SceneLandscapeRenderSystems), LocalLandscapeViewsPtr = &LandscapeViews](FRHICommandListBase& RHICmdList)
        {
            TRACE_CPUPROFILER_EVENT_SCOPE(FLandscapeRenderSystem::ComputeLODs);
            FOptionalTaskTagScope Scope(ETaskTag::EParallelRenderingThread);
            check(!SceneRenderSystems.IsEmpty())

            for (FLandscapeRenderSystem* RenderSystem : SceneRenderSystems)
            {
                RenderSystem->FetchHeightmapLODBiases();
            }

            for (FLandscapeViewData& LandscapeView : *LocalLandscapeViewsPtr)
            {
                const uint32 ViewStateKey = LandscapeView.View->GetViewKey() ;

                LandscapeView.LandscapeIndirection.SetNum(FLandscapeRenderSystem::LandscapeIndexAllocator.Num());

                for (FLandscapeRenderSystem* RenderSystem : SceneRenderSystems)
                {
                    // Store index where the LOD data for this landscape starts
                    LandscapeView.LandscapeIndirection[RenderSystem->LandscapeIndex] = LandscapeView.LandscapeLODData.Num();

                    // Compute sections lod values for this view & append to the global landscape LOD data
                    const TResourceArray<float>& CachedSectionLODValues = RenderSystem->ComputeSectionsLODForView(*LandscapeView.View, LandscapeView.ShadowInvalidatingInstances);
                    LandscapeView.LandscapeLODData.Append(CachedSectionLODValues);
                }
            }

            for (FLandscapeRenderSystem* RenderSystem : SceneRenderSystems)
            {
                RenderSystem->UpdateBuffers(RHICmdList);
            }

            for (FLandscapeViewData& LandscapeView : *LocalLandscapeViewsPtr)
            {
                FRHIResourceCreateInfo CreateInfoLODBuffer(TEXT("LandscapeLODDataBuffer"), &LandscapeView.LandscapeLODData);
                FBufferRHIRef LandscapeLODDataBuffer = RHICmdList.CreateVertexBuffer(LandscapeView.LandscapeLODData.GetResourceDataSize(), BUF_ShaderResource | BUF_Volatile, CreateInfoLODBuffer);
                LandscapeView.View->LandscapePerComponentDataBuffer = RHICmdList.CreateShaderResourceView(LandscapeLODDataBuffer, sizeof(float), PF_R32_FLOAT);

                FRHIResourceCreateInfo CreateInfoIndirection(TEXT("LandscapeIndirectionBuffer"), &LandscapeView.LandscapeIndirection);
                FBufferRHIRef LandscapeIndirectionBuffer = RHICmdList.CreateVertexBuffer(LandscapeView.LandscapeIndirection.GetResourceDataSize(), BUF_ShaderResource | BUF_Volatile, CreateInfoIndirection);
                LandscapeView.View->LandscapeIndirectionBuffer = RHICmdList.CreateShaderResourceView(LandscapeIndirectionBuffer, sizeof(uint32), PF_R32_UINT);
            }

        }, GIsThreadedRendering && GLandscapeUseAsyncTasksForLODComputation);
    }
}

其中,LOD Uniform Buffer 在 FLandscapeRenderSystem::UpdateBuffers 中更新

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

void FLandscapeRenderSystem::UpdateBuffers(FRHICommandListBase& RHICmdList)
{
    TRACE_CPUPROFILER_EVENT_SCOPE(FLandscapeRenderSystem::UpdateBuffers);

    bool bUpdateUB = false;

    if (Size != FIntPoint::ZeroValue)
    {
        if (!SectionLODBiasBuffer.IsValid())
        {
            FRHIResourceCreateInfo CreateInfo(TEXT("SectionLODBiasBuffer"), &SectionLODBiases);
            const static FLazyName ClassName(TEXT("FLandscapeRenderSystem"));
            CreateInfo.ClassName = ClassName;
            SectionLODBiasBuffer = RHICmdList.CreateVertexBuffer(SectionLODBiases.GetResourceDataSize(), BUF_ShaderResource | BUF_Dynamic, CreateInfo);
            SectionLODBiasSRV = RHICmdList.CreateShaderResourceView(SectionLODBiasBuffer, sizeof(float), PF_R32_FLOAT);
            bUpdateUB = true;
        }
        else
        {
            float* Data = (float*)RHICmdList.LockBuffer(SectionLODBiasBuffer, 0, SectionLODBiases.GetResourceDataSize(), RLM_WriteOnly);
            FMemory::Memcpy(Data, SectionLODBiases.GetData(), SectionLODBiases.GetResourceDataSize());
            RHICmdList.UnlockBuffer(SectionLODBiasBuffer);
        }

        if (bUpdateUB)
        {
            FLandscapeSectionLODUniformParameters Parameters;
            Parameters.LandscapeIndex = LandscapeIndex;
            Parameters.Min = Min;
            Parameters.Size = Size;
            Parameters.SectionLODBias = SectionLODBiasSRV;

            RHICmdList.UpdateUniformBuffer(SectionLODUniformBuffer, &Parameters);
        }
    }
}

LOD更新

地形 LOD 的计算发生在 FLandscapeRenderSystem::ComputeSectionsLODForView,在其中对每一个 LandscapeComponentSceneProxy 调用其 ComputeLODForView 函数进行 LOD 计算

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

const TResourceArray<float>& FLandscapeRenderSystem::ComputeSectionsLODForView(const FSceneView& InView, UE::Renderer::Private::IShadowInvalidatingInstances* InShadowInvalidatingInstances)
{
    TRACE_CPUPROFILER_EVENT_SCOPE(FLandscapeRenderSystem::ComputeSectionsLODForView);

    (..)

    CachedSectionLODValues->Reset(SectionInfos.Num());
    for (FLandscapeSectionInfo* SectionInfo : SectionInfos)
    {
        constexpr float DefaultLODValue = 0.0f;
        float& LODSectionValue = CachedSectionLODValues->Add_GetRef(DefaultLODValue);
        if (SectionInfo != nullptr)
        {
            LODSectionValue = SectionInfo->ComputeLODForView(InView);

            (..)
        }
    }
    return *CachedSectionLODValues;
}

float FLandscapeComponentSceneProxy::ComputeLODForView(const FSceneView& InView) const
{
    int32 ViewLODOverride = GetViewLodOverride(InView, LandscapeKey);
    float ViewLODDistanceFactor = InView.LODDistanceFactor;
    bool ViewEngineShowFlagCollisionPawn = InView.Family->EngineShowFlags.CollisionPawn;
    bool ViewEngineShowFlagCollisionVisibility = InView.Family->EngineShowFlags.CollisionVisibility;
    const FVector& ViewOrigin = GetLODView(InView).ViewMatrices.GetViewOrigin();
    const FMatrix& ViewProjectionMatrix = GetLODView(InView).ViewMatrices.GetProjectionMatrix();

    float LODScale = ViewLODDistanceFactor * CVarStaticMeshLODDistanceScale.GetValueOnRenderThread();

    FLandscapeRenderSystem* LandscapeRenderSystem = LandscapeRenderSystems.FindChecked(LandscapeKey);

    // Prefer the RenderSystem's ForcedLODOverride if set over any per-component LOD override
    int32 ForcedLODLevel = LandscapeRenderSystem->ForcedLODOverride >= 0 ? LandscapeRenderSystem->ForcedLODOverride : LODSettings.ForcedLOD;
    ForcedLODLevel = ViewLODOverride >= 0 ? ViewLODOverride : ForcedLODLevel;
    const int32 DrawCollisionLODOverride = GetDrawCollisionLodOverride(ViewEngineShowFlagCollisionPawn, ViewEngineShowFlagCollisionVisibility, LODSettings.DrawCollisionPawnLOD, LODSettings.DrawCollisionVisibilityLOD);
    ForcedLODLevel = DrawCollisionLODOverride >= 0 ? DrawCollisionLODOverride : ForcedLODLevel;
    ForcedLODLevel = FMath::Min<int32>(ForcedLODLevel, LODSettings.LastLODIndex);

    float LODLevel = static_cast<float>(ForcedLODLevel);
    if (ForcedLODLevel < 0)
    {
        float SectionScreenSizeSquared = ComputeBoundsScreenRadiusSquared(GetBounds().Origin, static_cast<float>(GetBounds().SphereRadius), ViewOrigin, ViewProjectionMatrix);
        SectionScreenSizeSquared /= FMath::Max(LODScale * LODScale, UE_SMALL_NUMBER);
        LODLevel = FLandscapeRenderSystem::ComputeLODFromScreenSize(LODSettings, SectionScreenSizeSquared);
    }

    return FMath::Max(LODLevel, 0.f);
}

FLandscapeComponentSceneProxy::ComputeLODForView 中两个关键步骤:

  • ComputeBoundsScreenRadiusSquared:根据 Component 的位置和大小,计算其 Bounding 投影至屏幕空间上的大小
  • FLandscapeRenderSystem::ComputeLODFromScreenSize:更新 Component 的 LOD 值,Uniform Buffer 中的FLandscapeSectionLODUniformParameters::SectionLOD
// Engine\Source\Runtime\Landscape\Private\LandscapeRender.cpp

float FLandscapeRenderSystem::ComputeLODFromScreenSize(const LODSettingsComponent& InLODSettings, float InScreenSizeSquared)
{
    // Component投影至屏幕空间上的大小 < LastLOD对应的屏幕空间大小
    // 则返回LastLOD
    if (InScreenSizeSquared <= InLODSettings.LastLODScreenSizeSquared)
    {
        return InLODSettings.LastLODIndex;
    }
    // Component投影至屏幕空间上的大小 > LOD1对应的屏幕空间大小
    // 则计算Component投影至屏幕空间上的大小,位于LOD0~LOD1之间的哪个比例处,最大是LOD0
    else if (InScreenSizeSquared > InLODSettings.LOD1ScreenSizeSquared)
    {
        return (InLODSettings.LOD0ScreenSizeSquared - FMath::Min(InScreenSizeSquared, InLODSettings.LOD0ScreenSizeSquared)) / (InLODSettings.LOD0ScreenSizeSquared - InLODSettings.LOD1ScreenSizeSquared);
    }
    // Component投影至屏幕空间上的大小 < LOD1对应的屏幕空间大小
    else
    {
        // No longer linear fraction, but worth the cache misses
        return 1.0f + FMath::LogX(InLODSettings.LODOnePlusDistributionScalarSquared, InLODSettings.LOD1ScreenSizeSquared / InScreenSizeSquared);
    }
}

Continuous LOD机制

使用 Continuous LOD 计算顶点实际位置的主要过程是:计算顶点所属LOD层级;对于LOD整数部分,分别计算顶点在当前LOD和下一级LOD的XY坐标;采样高度图分别得到顶点在当前LOD和下一级LOD的高度;对上述坐标、高度以及顶点法线进行插值,得到最后的顶点

ContinuousLOD

// Engine\Shaders\Private\LandscapeVertexFactory.ush

FVertexFactoryIntermediates GetVertexFactoryIntermediates(FVertexFactoryInput Input)
{
    FVertexFactoryIntermediates Intermediates;
    Intermediates.SceneData = VF_GPUSCENE_GET_INTERMEDIATES(Input);
    // Component线性索引
    Intermediates.ComponentIndex = GetComponentLinearIndex();
    // Component LOD, float4(Lod, 0, LodSubSectionSizeQuads, rcp(LodSubSectionSizeQuads))
    Intermediates.LodValues = GetLodValues(Intermediates.ComponentIndex);
    // LodBias, float2(LodBias, 0.f)
    Intermediates.LodBias = GetLodBias(Intermediates.ComponentIndex);
    Intermediates.InputPosition = Input.Position;

    (...)

    // 顶点在Section内归一化坐标
    float2 xy = Intermediates.InputPosition.xy * Intermediates.LodValues.w;
    // 计算与最近邻混合后的LOD
    float LODCalculated = CalcLOD(Intermediates.ComponentIndex, xy, Intermediates.InputPosition.zw);
    // LOD整数部分
    float LodValue = floor(LODCalculated);
    // LOD小数部分
    float MorphAlpha = LODCalculated - LodValue;


    // 实际坐标
    // 若LODCalculated向高LOD混合,则LOD整数部分与原LOD相等,实际顶点坐标(未归一化)无需变化
    // 若LODCalculated向低LOD混合,则LOD整数部分比原LOD低一级,实际顶点坐标(未归一化)应变为两倍
    float2 ActualLODCoordsInt = floor(Intermediates.InputPosition.xy * pow(2, -(LodValue - Intermediates.LodValues.x)));
    float InvLODScaleFactor = pow(2, -LodValue);

    // 坐标变换,将顶点在当前LOD下的未归一化局部坐标变换至LOD0下的归一化坐标,对NextLOD也是如此
    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;
    float2 NextLODCoordsInt = floor(ActualLODCoordsInt * 0.5);
    float2 InputPositionNextLOD = NextLODCoordsInt / CoordTranslate.y;
    // -------------------------------------------------------------------------------------------------
    // 例:SubsectionQuads为63*63,那么在LOD0下,其顶点未归一化的局部坐标范围是[0,63]^2
    //    若当前LOD为LOD1,顶点未归一化的局部坐标范围是[0,31]^2;NextLOD为LOD2,顶点未归一化的局部坐标范围是[0,15]^2
    //    假设有在当前LOD下局部坐标为(0,15)的顶点,在NextLOD下局部坐标为(0,7)
    //    将它们变换到LOD0下的局部坐标分别为(0,floor(15*63/31))=(0,30), (0, floor(7*63/15))=(0,29)
    // -------------------------------------------------------------------------------------------------

    // 对当前LOD和NextLOD分别计算HeightmapUV,并采样得到高度值(归一化到最大与最小高度之间的值),然后反算出实际高度
    // Intermediates.InputPosition.zw * LandscapeParameters.SubsectionOffsetParams.xy 是subsection的原点
    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);

    (...)

    // 顶点局部位置插值
    // 根据LOD小数部分,在当前LOD和NextLOD的(未归一化局部XY坐标,高度)中插值
    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;
}


float CalcLOD(uint ComponentIndex, float2 xyLocalToSubsection, float2 Subsection)
{
    (...)

    // 顶点在Component内归一化坐标
    float2 xy = (xyLocalToSubsection + Subsection) / LandscapeParameters.NumSubsections;

    // calculate blend value (normalized distance from section border: 1 at the border, 0 at the center)
    float2 Delta = xy * 2 - 1;
    float2 AbsDelta = abs(Delta);
    float LB = max(AbsDelta.x, AbsDelta.y);

    // AbsDelta
    // (1,1)----(0,1)----(1,1)
    //   |        |        |
    // (1,0)----(0,0)----(1,0)
    //   |        |        |
    // (1,1)----(0,1)----(1,1)

    // shift the blend towards 0 to reduce the lod blend range
    float k = LandscapeParameters.InvLODBlendRange;
    LB = saturate(1 - (1-LB) * k);

    // 到最近相邻Component的Offset
    int2 NeighborOffset = (AbsDelta.x > AbsDelta.y) ? int2(sign(Delta.x), 0) : int2(0, sign(Delta.y));

    // 当前Component(Center)的LOD,以及最近邻Component的LOD
    float CenterLod = GetSectionLod(ComponentIndex);
    float NeighborLod = GetNeighborSectionLodFromOffset(NeighborOffset, CenterLod);

    // 混合LOD
    float LODCalculated = (1-LB) * CenterLod + LB * NeighborLod;

    return LODCalculated;
#endif
}

Reference

Unreal Engine Documentation: Landscape Technical Guide

UE4地形系统(Landscape)

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

UE4 地形 landscape

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