说明
本文为UE5-地形系统系列的第一篇文章,主要是对UE5地形系统进行初步的整体分析
主要类型
ALandscapeProxy
继承体系如下:
- UObjectBase
- UObjectBaseUtility
- UObject
- AActor
- APartitionActor
- ALandscapeProxy
- APartitionActor
- AActor
- UObject
- UObjectBaseUtility
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))
(...)
}
ALandscape
ALandscape 继承自 ALandscapeProxy,在此基础上实现了更多功能。
ALanscape的主要作用是管理LandscapeComponent,地形相关的真正的渲染数据在ULandscapeComponent,地形渲染流程也是以ULandscapeComponent为基础单元。
ULandscapeComponent
每个地形都会被划分成多个地形组件。
地形组件是虚幻引擎在渲染地形、计算地形可视性和处理地形碰撞时采用的基本单位。
地形中的所有地形组件都具有相同的大小,并且始终为正方形。地形组件尺寸是在创建地形时决定的,取决于地形的大小和细节设置。
每个 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 的索引在组件中的排布如下:
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 中的排布如下:
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 等数据进行更新;
- AddPrimitive,创建 LandscapeComponentSceneProxy 并收集地形材质,在渲染线程 AddPrimitiveCommand 中完成 LandscapeUniformShaderParameters 的设置;
- 在 FSceneRenderer::Render 的 InitViews 过程中,调用 FSceneRenderer::OnRenderBegin --> FLandscapeSceneViewExtension::PreRenderView_RenderThread --> FLandscapeRenderSystem::UpdateBuffers 完成 LandscapeSectionLODUniformParameters 的设置。在 InitViews 后续流程中完成 MeshDrawCommand 收集;
- 在 BassPass 中添加 RDG Pass,在最后 ExecutePass;
- 在 Pass 执行时创建 ParallelCommandListSet,创建并入队 FDrawVisibleMeshCommandsAnyThreadTask;
- FDrawVisibleMeshCommandsAnyThreadTask 任务是提交 MeshDrawCommand 至新命令队列,并将新命令队列加入 ParallelCommandListSet 中;
- Pass 执行完毕退出前,ParallelCommandListSet 析构函数中进行 Dispatch 中,其中对其命令队列中的 MeshDrawCommand 进行异步转译,得到 RHICommand;
- 添加 RHI 线程任务提交得到的 RHIPlatformCommandList