说明
本文为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);
}
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 进行采样并对材质进行混合
地形材质编译
地形材质的编译与其他材质大同小异 ,材质系统与整体编译流程见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;
}