UE4 地形 landscape
UE4 Landscape Mobile
1. UE4 渲染流程
1.1 UE4 渲染线程
- Game Tread(游戏线程) :游戏逻辑运算
- Rendering Thread(渲染线程) :从 TaskGraph 中取出任务,并生成平台无关的 Command List (渲染指令列表)
- RHI Thread (Render Hardware Interface 线程):会执行和转换渲染线程的 Command List 成为指定图形 API 的调用(称为Graphical Command),并提交到GPU执行。
这3个线程处理的数据通常是不同帧的,譬如 GameThread 处理N帧数据,RenderThread 和 RHIThread 处理 N-1 帧数据。
但也存在例外,比如 RenderThread 和 RHIThread 运行很快,几乎不存在延迟,这种情况下,GameThread 处理N帧,而 RenderThread 可能处理N或N-1帧,RHIThread 也可能在转换N或N-1帧。
但是,RenderThread 不能落后游戏线程一帧,否则 GameThread 会卡住,直到 RenderThread 处理完所有指令。
1.2 渲染概念
UPrimitiveComponent : 场景中需要绘制的 Actor 都会有 UPrimitiveComponent,这个数据是存在于 Game Thread 中
FPrimitiveSceneProxy 和FPrimitiveSceneInfo :
FPrimitiveSceneProxy:Render thread 上UPrimitiveComponent 的对应代理,只包含渲染Primitive 所需的数据,但和UPrimitiveComponent 引用同样的数据
FPrimitiveSceneInfo:和FPrimitiveSceneProxy 一一对应,在引擎的Renderer 模块下
游戏线程 | 渲染线程 |
---|---|
UWorld | FScene |
UPrimitiveComponent | FPrimitiveSceneProxy / FPrimitiveSceneInfo |
2. Landscape 渲染流程
2.1 UE4 地形类结构
我们创建好地形后,场景中会有一个 Landscape 类型的 Actor
ALandscape 类型继承关系如下
Detail 面板上显示的属性变量都存放在 ALandscapeProxy 类中,这个类主要用来保存地形的详细信息和属性设定值。
ALandscape 继承 ALandscapeProxy,在这个基础上实现了更多功能。
2.2 UE4 地形渲染过程
2.2.1 地形组成结构
UE4 地形渲染是以 Component 为基础渲染单元的。我们新建了一个场景,然后创建一个地形,地形参数如下:
Landscape 由两个 Component 组成,然后运行中,我们断点获取场景中所有的 Actor,下面是调试信息:
找到 Landscape 对象后,我们查看它的 Component,发现当我们给地形设置了两个 Component 后,对应的 ALandscape 对象就会生成两个 LandscapeComponent 组件:
然后每个 LandscapeComponent 就是一个基础的渲染单元,如下是地形需要的类的继承关系图,
2.2.2 创建 SceneProxy
按照之前介绍的 UE4 渲染流程,首先会调用 CreateSceneProxy 来创建 SceneProxy(这里对应的就是 FLandscapeComponentSceneProxy 跟 FLandscapeComponentSceneProxyMobile)。
具体调用堆栈如下:
这里会判断的当前 renderer feature level 来创建对应的 Proxy,下面是对应的平台的 enum 定义:
namespace ERHIFeatureLevel
{
enum Type
{
/** OpenGL ES2. Deprecated */
ES2_REMOVED,
/** OpenGL ES3.1 & Metal/Vulkan. */
ES3_1,
/** DX10 Shader Model 4.
* SUPPORT FOR THIS FEATURE LEVEL HAS BEEN ENTIRELY REMOVED. */
SM4_REMOVED,
/** DX11 Shader Model 5. */
SM5,
Num
};
};
移动端跟 PC 端的区别:创建的 Proxy 分别是 FLandscapeComponFLandeneProxy 跟 FLandscapeComponentSceneProxyMobile,FLandscapeComponentSceneProxyMobile 是 FLandscapeComponFLandeneProxy 的子类
两者都会调用基类的构造函数,在构造函数中差异如下:
- AvailableMaterials 来源
FLandscapeComponentSceneProxy::FLandscapeComponentSceneProxy(...)
{
const auto FeatureLevel = GetScene().GetFeatureLevel();
// PC
if (FeatureLevel >= ERHIFeatureLevel::SM5)
{
if (InComponent->GetLandscapeProxy()->bUseDynamicMaterialInstance)
{
AvailableMaterials.Append(InComponent->MaterialInstancesDynamic);
}
else
{
AvailableMaterials.Append(InComponent->MaterialInstances);
}
}
// Mobile
else
{
AvailableMaterials.Append(InComponent->MobileMaterialInterfaces);
}
}
- SharedBuffersKey : 可以看到如果忽略掉 XYOffsetmapTexture,渲染平台,SharedBuffersKey 只由 SubsectionSizeQuads、NumSubsections 唯一确定。因为所有属于同一个 ALandscape 的 Component 的这两个参数都是一样的,所以这些 Component 的 Proxy 共用一个 SharedBuffersKey.
XYOffsetmapTexture : PC 可以传一张 XYOffsetmapTexture,后面阅读 shader 代码可以看出这个可以对顶点的 xy 坐标做偏移
// SharedBuffer 根据 SharedBufferKey 来创建
const int8 SubsectionSizeLog2 = FMath::CeilLogTwo(InComponent->SubsectionSizeQuads + 1);
SharedBuffersKey = (SubsectionSizeLog2 & 0xf) | ((NumSubsections & 0xf) << 4) |
(FeatureLevel <= ERHIFeatureLevel::ES3_1 ? 0 : 1 << 30) |
(XYOffsetmapTexture == nullptr ? 0 : 1 << 31);
- HeightMap
//
class FLandscapeNeighborInfo
{
UTexture2D* HeightmapTexture; // PC : Heightmap, Mobile : Weightmap
}
class FLandscapeComponentSceneProxy : public FPrimitiveSceneProxy,
public FLandscapeNeighborInfo
{
}
if (FeatureLevel <= ERHIFeatureLevel::ES3_1)
{
HeightmapTexture = nullptr;
HeightmapSubsectionOffsetU = 0;
HeightmapSubsectionOffsetV = 0;
}
else
{
HeightmapSubsectionOffsetU = ((float)(InComponent->SubsectionSizeQuads + 1) /
(float)FMath::Max<int32>(1, HeightmapTexture->GetSizeX()));
HeightmapSubsectionOffsetV = ((float)(InComponent->SubsectionSizeQuads + 1) /
(float)FMath::Max<int32>(1, HeightmapTexture->GetSizeY()));
}
- WeightmapTextures 跟 NormalmapTexture
// PC
FLandscapeComponentSceneProxy::FLandscapeComponentSceneProxy(ULandscapeComponent* InComponent) :
WeightmapTextures(InComponent->GetWeightmapTextures())
, NormalmapTexture(InComponent->GetHeightmap())
// Mobile
FLandscapeComponentSceneProxyMobile::FLandscapeComponentSceneProxyMobile(ULandscapeComponent* InComponent)
{
WeightmapTextures = InComponent->MobileWeightmapTextures;
NormalmapTexture = InComponent->MobileWeightmapTextures[0];
}
- HasTessellationEnabled : 手机不支持曲面细分
{
for (UMaterialInterface*& MaterialInterface : AvailableMaterials)
{
bool HasTessellationEnabled = false;
// PC
if (FeatureLevel >= ERHIFeatureLevel::SM5)
{
HasTessellationEnabled = LandscapeMaterial->D3D11TessellationMode !=
EMaterialTessellationMode::MTM_NoTessellation;
}
MaterialHasTessellationEnabled.Add(HasTessellationEnabled);
}
}
// PC : true
// Moble : false
bSupportsHeightfieldRepresentation = FeatureLevel <=
ERHIFeatureLevel::ES3_1 ? false : true;
class FPrimitiveSceneProxy {
inline bool SupportsHeightfieldRepresentation() const
{ return bSupportsHeightfieldRepresentation; }
}
2.2.3 创建渲染资源
创建完 Proxy 然后创建 PrimitiveSceneInfo 并且在渲染进程上创建渲染资源:
void FScene::AddPrimitive(UPrimitiveComponent* Primitive)
{
// 上面的 CreateProxy
FPrimitiveSceneProxy* PrimitiveSceneProxy = Primitive->CreateSceneProxy();
Primitive->SceneProxy = PrimitiveSceneProxy;
// Create the primitive scene info.
FPrimitiveSceneInfo* PrimitiveSceneInfo = new FPrimitiveSceneInfo(Primitive, this);
PrimitiveSceneProxy->PrimitiveSceneInfo = PrimitiveSceneInfo;
FCreateRenderThreadParameters Params =
{
PrimitiveSceneProxy,
RenderMatrix,
Primitive->Bounds,
AttachmentRootPosition,
Primitive->CalcBounds(FTransform::Identity)
};
// 放到渲染线程创建资源
ENQUEUE_RENDER_COMMAND(AddPrimitiveCommand)(
[Params = MoveTemp(Params), Scene, PrimitiveSceneInfo, PreviousTransform = MoveTemp(PreviousTransform)](FRHICommandListImmediate& RHICmdList)
{
FPrimitiveSceneProxy* SceneProxy = Params.PrimitiveSceneProxy;
// SetTransform 这里 mark 一下
SceneProxy->SetTransform(Params.RenderMatrix, Params.WorldBounds,
Params.LocalBounds, Params.AttachmentRootPosition);
// 创建渲染资源
SceneProxy->CreateRenderThreadResources();
Scene->AddPrimitiveSceneInfo_RenderThread(PrimitiveSceneInfo, PreviousTransform);
});
}
首先介绍下 SetTransform 具体做了哪些操作,这个会涉及到后续 Shader 中 Landscape 顶点计算过程,函数参数中包含了 Primitive 的一些基础属性:局部坐标转世界坐标的变换矩阵,包围盒等。
void FPrimitiveSceneProxy::SetTransform(
const FMatrix& InLocalToWorld,
const FBoxSphereBounds& InBounds,
const FBoxSphereBounds& InLocalBounds,
FVector InActorPosition)
{
LocalToWorld = InLocalToWorld;
// 这里会创建 Primitive 的 UniformBufferObject
UpdateUniformBuffer()
{
const FPrimitiveUniformShaderParameters PrimitiveUniformShaderParameters =
GetPrimitiveUniformShaderParameters(
LocalToWorld,
PreviousLocalToWorld,
ActorPosition,
Bounds,
LocalBounds,
PreSkinnedLocalBounds,
bReceivesDecals,
HasDistanceFieldRepresentation(),
HasDynamicIndirectShadowCasterRepresentation(),
UseSingleSampleShadowFromStationaryLights(),
bHasPrecomputedVolumetricLightmap,
DrawsVelocity(),
GetLightingChannelMask(),
LpvBiasMultiplier,
PrimitiveSceneInfo ? PrimitiveSceneInfo->GetLightmapDataOffset() : 0,
SingleCaptureIndex,
bOutputVelocity || AlwaysHasVelocity(),
GetCustomPrimitiveData(),
CastsContactShadow());
if (UniformBuffer.GetReference())
{
UniformBuffer.UpdateUniformBufferImmediate(PrimitiveUniformShaderParameters);
}
else
{
UniformBuffer = TUniformBufferRef<FPrimitiveUniformShaderParameters>::CreateUniformBufferImmediate(PrimitiveUniformShaderParameters, UniformBuffer_MultiFrame);
}
}
}
// Primitive Uniform 参数定义
inline FPrimitiveUniformShaderParameters GetPrimitiveUniformShaderParameters(
const FMatrix& LocalToWorld,
const FMatrix& PreviousLocalToWorld,
FVector ActorPosition,
const FBoxSphereBounds& WorldBounds,
const FBoxSphereBounds& LocalBounds,
const FBoxSphereBounds& PreSkinnedLocalBounds,
bool bReceivesDecals,
bool bHasDistanceFieldRepresentation, // Currently unused
bool bHasCapsuleRepresentation,
bool bUseSingleSampleShadowFromStationaryLights,
bool bUseVolumetricLightmap,
bool bDrawsVelocity,
uint32 LightingChannelMask,
float LpvBiasMultiplier,
uint32 LightmapDataIndex,
int32 SingleCaptureIndex,
bool bOutputVelocity,
const FCustomPrimitiveData* CustomPrimitiveData,
bool bCastContactShadow = true
)
{
FPrimitiveUniformShaderParameters Result;
Result.LocalToWorld = LocalToWorld;
Result.WorldToLocal = LocalToWorld.Inverse();
// 省略一堆参数设置
return Result;
}
接下来在渲染进程中调用函数 CreateRenderThreadResource 中初始化顶点 Buffer 以及 Shader 所需要的 UBO
PC 端创建流程如下:
- SharedBuffers
SharedBuffers 是根据 Proxy 构造时生成的 SharedBuffersKey 来创建,
SharedBuffers = FLandscapeComponentSceneProxy::SharedBuffersMap.FindRef(SharedBuffersKey);
if (SharedBuffers == nullptr)
{
int32 NumOcclusionVertices = MobileRenderData->OccluderVerticesSP.IsValid() ?
MobileRenderData->OccluderVerticesSP->Num() : 0;
SharedBuffers = new FLandscapeSharedBuffers(
SharedBuffersKey, SubsectionSizeQuads, NumSubsections,
GetScene().GetFeatureLevel(), false, NumOcclusionVertices);
FLandscapeComponentSceneProxy::SharedBuffersMap.Add(SharedBuffersKey, SharedBuffers);
}
SharedBuffers->AddRef();
FLandscapeSharedBuffer 创建时,会新建 VertexIndex Buff。
FLandscapeSharedBuffers::FLandscapeSharedBuffers(...)
: NumIndexBuffers(FMath::CeilLogTwo(InSubsectionSizeQuads + 1))
{
// SubsectionSizeVerts 7 * 7 : 8 | 15 * 15 : 16
// NumSubsections 2 * 2 : 2 | 1 * 1 : 1
NumVertices = FMath::Square(SubsectionSizeVerts) *
FMath::Square(NumSubsections);
// PC
// Mobile 的 VertextBuffer 在
// FLandscapeComponentSceneProxyMobile::MobileRenderData.VertexBuffer
if (InFeatureLevel > ERHIFeatureLevel::ES3_1)
{
// Vertex Buffer cannot be shared
VertexBuffer = new FLandscapeVertexBuffer(InFeatureLevel,
NumVertices, SubsectionSizeVerts, NumSubsections);
}
// 7 -> 3
// 15 -> 4
IndexBuffers = new FIndexBuffer*[NumIndexBuffers];
if (NumVertices > 65535)
{
bUse32BitIndices = true;
CreateIndexBuffers<uint32>(InFeatureLevel, bRequiresAdjacencyInformation);
}
else
{
CreateIndexBuffers<uint16>(InFeatureLevel, bRequiresAdjacencyInformation);
}
}
CreateIndexBuffer 函数大致如下:
int32 MaxLOD = NumIndexBuffers - 1;
// 逐 LOD
for (int32 Mip = MaxLOD; Mip >= 0; Mip--)
{
// 每个 Section 2 * 2/ 1 * 1
for (int32 SubY = 0; SubY < NumSubsections; SubY++)
{
for (int32 SubX = 0; SubX < NumSubsections; SubX++)
{
// 逐 Quad 遍历
}
}
}
顶点排列顺序如下
- Component 只有一个 Section,每个 Section 有 15 * 15 个 Quad 时
- Component 有 2 * 2 个 Section,每个 Section 有 7 * 7 个 Quad 时:
Mobile 计算就稍微有点复杂了,需要计算两个变量 LodSubsectionSizeQuads 和 MipRatio。
假如 Section 构成是 15 * 15,则
NumLOD = NumIndexBuffers // IndexBuffer 数量
= FMath::CeilLogTwo(InSubsectionSizeQuads + 1)
= log(2, 15 + 1)
MaxLOD = NumLOD - 1
= 4 - 1 = 3 // LOD(0, 1, 2, 3)
LodSubsectionSizeQuads = (SubsectionSizeVerts >> Mip) - 1;
= (16 >> LOD) - 1
MipRatio = (float)SubectionSizeQuads / (float)LodSubsectionSizeQuads;
= 15.0 / LodSubsectionSizeQuads
于是有如下表格:
LOD | LodSubsectionSizeQuads | MipRatio |
---|---|---|
3 | 1 | 15.00 |
2 | 3 | 5.00 |
1 | 7 | 2.143 |
0 | 15 | 1.00 |
则 7 * 7 Section 的表格如下:
LOD | LodSubsectionSizeQuads | MipRatio |
---|---|---|
2 | 1 | 7.00 |
1 | 3 | 2.33 |
0 | 7 | 1.00 |
最终计算得出的 LOD 如下:
- LOD2:
- LOD1:
- LOD0:
随后构造 FLandscapeVertexFactoryMobile,主要是用来定义如何将顶点数据以正确的格式发送到 GPU。
class FLandscapeVertexFactory : public FVertexFactory
{
struct FDataType
{
/** The stream to read the vertex position from. */
FVertexStreamComponent PositionComponent;
};
}
class FLandscapeVertexFactoryMobile : public FLandscapeVertexFactory
{
struct FDataType : FLandscapeVertexFactory::FDataType
{
/** stream which has heights of each LOD levels */
TArray<FVertexStreamComponent,TFixedAllocator<LANDSCAPE_MAX_ES_LOD_COMP> > LODHeightsComponent;
};
}
#define LANDSCAPE_MAX_ES_LOD_COMP 2
#define LANDSCAPE_MAX_ES_LOD 6
struct FLandscapeMobileVertex
{
uint8 Position[4]; // Pos + LOD 0 Height
uint8 LODHeights[LANDSCAPE_MAX_ES_LOD_COMP*4];
};
void FLandscapeComponentSceneProxyMobile::CreateRenderThreadResources()
{
// Init vertex buffer
{
check(MobileRenderData->VertexBuffer);
MobileRenderData->VertexBuffer->InitResource();
FLandscapeVertexFactoryMobile* LandscapeVertexFactory = new FLandscapeVertexFactoryMobile(FeatureLevel);
LandscapeVertexFactory->MobileData.PositionComponent = FVertexStreamComponent(MobileRenderData->VertexBuffer,
STRUCT_OFFSET(FLandscapeMobileVertex, Position), sizeof(FLandscapeMobileVertex), VET_UByte4N);
for (uint32 Index = 0; Index < LANDSCAPE_MAX_ES_LOD_COMP; ++Index)
{
LandscapeVertexFactory->MobileData.LODHeightsComponent.Add(FVertexStreamComponent(MobileRenderData->VertexBuffer,
STRUCT_OFFSET(FLandscapeMobileVertex, LODHeights) + sizeof(uint8) * 4 * Index,
sizeof(FLandscapeMobileVertex), VET_UByte4N));
}
LandscapeVertexFactory->InitResource();
VertexFactory = LandscapeVertexFactory;
}
}
分两个 FVertexStreamComponent:PositionComponent 和 LODHeightsComponent.
PositionComponent 对应 STRUCT_OFFSET(FLandscapeMobileVertex, Position),即来源为 MobileRenderData->VertexBuffer 的每个顶点数据 (FLandscapeMobileVertex)的高度 Field
LODHeightsComponent 对应 STRUCT_OFFSET(FLandscapeMobileVertex, LODHeights) + sizeof(uint8) * 4 * Index,即来源为 MobileRenderData->VertexBuffer 的每个顶点数据(FLandscapeMobileVertex) 的 LOD 高度(LODHeights)数据,加上此 LOD 的偏移,一共有多少 LOD 就有多少 FVertexStreamComponent 被添加到了 LODHeightsComponent. 这里的 MobileRenderData 就是FLandscapeComponentSceneProxyMobile::MobileRenderData,之前从 Platform 反序列化来的。
这两个分别对应 Shader 里的参数 PackedPosition 跟 LODHeights
// Engine\Shaders\Private\LandscapeVertexFactory.ush
struct FVertexFactoryInput
{
#if FEATURE_LEVEL >= FEATURE_LEVEL_SM4
float4 Position: ATTRIBUTE0;
#else
float4 PackedPosition: ATTRIBUTE0; //
float4 LODHeights[2]: ATTRIBUTE1;
#endif
};
这里详细解释一下 LODHeights 中的数据
LODHeights:每个顶点的各LOD高度数组,这样编码:
LODHeights[0].x:MinHeight >> 8,其中 MinHeight 为此顶点的所有LOD高度的最小值
LODHeights[0].y:MaxHeight >> 8,其中 MaxHeight 为此顶点的所有LOD高度的最大值
LODHeights[0].zw ~ LODHeight[1].xyzw 为 LOD0~LOD5 的高度值,被归一化到了 LODHeights[0] 到 LODHeight[1] 之间,后面可以看到,在 shader 里会反向解码这些数据。
2.2.4 顶点数据
VertextBuffer 里的数据存储如下:Position 是由 4 个 float 组成的,数据结构如下:
struct FLandscapeVertex
{
float VertexX; // 对应到 Section 中的 x
float VertexY; // 对应到 Section 中的 y
float SubX; // Component 中 Section 位置
float SubY;
};
VertextX/VertexY 表示的是顶点在 Section 中的位置:
SubX/SubY 表示的是 Section 在 Component 中的位置
在 PC 下是在创建 FLandscapeSharedBuffers 时创建,然后通过调用 FLandscapeVertextBuffer::InitRHI,新建的,可以看到顶点是逐 Section 生成的。
void FLandscapeVertexBuffer::InitRHI()
{
FRHIResourceCreateInfo CreateInfo;
void* BufferData = nullptr;
int32 VertexIndex = 0;
for (int32 SubY = 0; SubY < NumSubsections; SubY++)
{
for (int32 SubX = 0; SubX < NumSubsections; SubX++)
{
for (int32 y = 0; y < SubsectionSizeVerts; y++)
{
for (int32 x = 0; x < SubsectionSizeVerts; x++)
{
Vertex->VertexX = x;
Vertex->VertexY = y;
Vertex->SubX = SubX;
Vertex->SubY = SubY;
Vertex++;
VertexIndex++;
}
}
}
}
}
RenderDoc 中抓帧数据如下:对应到 ATTRIBUTE 中的数据,下面这个顶点是 Section 0,0 下的顶点 7,2
Mobile 下则是读取 PlatformData 中的数据,不过这里的数据需要乘以 255 才能得出最终的坐标值:
最终地表的顶点数据是在 LandscapeVertextFactory.ush 中生成的,PC 通过 VertexBuffers 跟 HeightMapTexture 生成最终的 Mesh 顶点,Mobile 中的顶点跟高度数据通过读取 PlatformData 中的数据,分别将 VertextBuffers 跟高度数据传给 Shader 计算。
2.2.5 vertext shader
下面是从 RenderDoc 抓帧查看 Shader 代码逻辑
下面列出 vertext shader 主要逻辑,GetVertexFactoryIntermediates 。
// Engine\Shaders\Private\LandscapeVertexFactory.ush
struct FVertexFactoryInput
{
#if FEATURE_LEVEL >= FEATURE_LEVEL_SM4
float4 Position: ATTRIBUTE0;
#else
float4 PackedPosition: ATTRIBUTE0; //
float4 LODHeights[2]: ATTRIBUTE1;
#endif
};
// mobile
#define TERRAIN_ZSCALE (1.0f/128.0f)
FVertexFactoryIntermediates GetVertexFactoryIntermediates(FVertexFactoryInput Input)
{
FVertexFactoryIntermediates Intermediates;
// 从 PackedPosition 中 xy 读取顶点在 Section 中的位置
Intermediates.InputPosition.xy = Input.PackedPosition.xy * 255.f;
// PackedPosition.z 最后两位存储 Section 编号 00,01,10,11
uint PackedExtraData = (uint)(Input.PackedPosition.z * 255.f);
float SubX = (float)((PackedExtraData >> 1) & 1);
float SubY = (float)((PackedExtraData >> 0) & 1);
Intermediates.InputPosition.zw = float2(SubX, SubY);
// 计算高度
float MinHeight = DecodePackedHeight(float2(Input.LODHeights[0].x, Input.LODHeights[0].y));
float HeightDelta = Input.PackedPosition.w * 255.0 * 256.0 * TERRAIN_ZSCALE;
// 忽略 LOD 计算过程 LODHeightIndex 是计算结果
float InputHeight = Input.LODHeights[LODHeightIndex >> 2][LODHeightIndex & 3];
float Height = MinHeight + InputHeight * HeightDelta;
// 计算 xy
float InvLODScaleFactor = 1.f / (float)(1 << (uint)LodValue);
// LodValues.x is always 0 on mobile.
float2 ActualLODCoordsInt = floor(Intermediates.InputPosition.xy * InvLODScaleFactor);
float2 CoordTranslate = float2( LandscapeParameters.SubsectionSizeVertsLayerUVPan.x * InvLODScaleFactor - 1,
max(LandscapeParameters.SubsectionSizeVertsLayerUVPan.x * 0.5f * InvLODScaleFactor, 2) - 1 )
* LandscapeParameters.SubsectionSizeVertsLayerUVPan.y;
float2 InputPositionLODAdjusted = ActualLODCoordsInt / CoordTranslate.x;
// InputPositionNextLOD : Position for next LOD in base LOD units
float2 NextLODCoordsInt = floor(ActualLODCoordsInt * 0.5);
float2 InputPositionNextLOD = NextLODCoordsInt / CoordTranslate.y;
// InputPositionLODAdjusted 怎么算出来的还没弄懂?
Intermediates.LocalPosition = lerp( float3(InputPositionLODAdjusted, Height),
float3(InputPositionNextLOD, HeightNextLOD), MorphAlpha );
return Intermediates;
}
然后下面是 PC 上的逻辑
FVertexFactoryIntermediates GetVertexFactoryIntermediates(FVertexFactoryInput Input)
{
FVertexFactoryIntermediates Intermediates;
#if FEATURE_LEVEL >= FEATURE_LEVEL_SM4
Intermediates.InputPosition = Input.Position;
#endif
// 计算采样纹理坐标
float2 SampleCoords = InputPositionLODAdjusted * LandscapeParameters.HeightmapUVScaleBias.xy
+ LandscapeParameters.HeightmapUVScaleBias.zw + 0.5*LandscapeParameters.HeightmapUVScaleBias.xy
+ Intermediates.InputPosition.zw * LandscapeParameters.SubsectionOffsetParams.xy;
// 采样高度图
float4 SampleValue = Texture2DSampleLevel(LandscapeParameters.HeightmapTexture,
LandscapeParameters.HeightmapTextureSampler, SampleCoords, LodValue-Intermediates.LodBias.x);
float Height = DecodePackedHeight(SampleValue.xy);
Intermediates.LocalPosition = lerp( float3(InputPositionLODAdjusted, Height),
float3(InputPositionNextLOD, HeightNextLOD), MorphAlpha );
return Intermediates;
}
float3 GetLocalPosition(FVertexFactoryIntermediates Intermediates)
{
return INVARIANT(Intermediates.LocalPosition+float3(Intermediates.InputPosition.zw *
LandscapeParameters.SubsectionOffsetParams.ww,0));
}
float4 VertexFactoryGetWorldPosition(FVertexFactoryInput Input, FVertexFactoryIntermediates Intermediates)
{
return INVARIANT(TransformLocalToTranslatedWorld(GetLocalPosition(Intermediates)));
}
然后是将局部坐标转成世界坐标
float3 GetLocalPosition(FVertexFactoryIntermediates Intermediates)
{
return INVARIANT(Intermediates.LocalPosition +
float3(Intermediates.InputPosition.zw * LandscapeParameters.SubsectionOffsetParams.ww,0));
}
float4 VertexFactoryGetWorldPosition(FVertexFactoryInput Input,
FVertexFactoryIntermediates Intermediates)
{
return INVARIANT(TransformLocalToTranslatedWorld(GetLocalPosition(Intermediates)));
}
// Engine\Shaders\Private\VertexFactoryCommon.ush
float4 TransformLocalToTranslatedWorld(float3 LocalPosition)
{
float3 RotatedPosition = Primitive.LocalToWorld[0].xyz * LocalPosition.xxx +
Primitive.LocalToWorld[1].xyz * LocalPosition.yyy +
Primitive.LocalToWorld[2].xyz * LocalPosition.zzz;
return float4(RotatedPosition + (Primitive.LocalToWorld[3].xyz +
ResolvedView.PreViewTranslation.xyz),1);
}
下面是 Uniform 结构体定义,对应 Shader 中的 LandscapeParameters
/** The uniform shader parameters for a landscape draw call. */
BEGIN_GLOBAL_SHADER_PARAMETER_STRUCT(FLandscapeUniformShaderParameters, LANDSCAPE_API)
SHADER_PARAMETER(int32, ComponentBaseX)
SHADER_PARAMETER(int32, ComponentBaseY)
SHADER_PARAMETER(int32, SubsectionSizeVerts)
SHADER_PARAMETER(int32, NumSubsections)
SHADER_PARAMETER(int32, LastLOD)
SHADER_PARAMETER(FVector4, HeightmapUVScaleBias)
SHADER_PARAMETER(FVector4, WeightmapUVScaleBias)
SHADER_PARAMETER(FVector4, LandscapeLightmapScaleBias)
SHADER_PARAMETER(FVector4, SubsectionSizeVertsLayerUVPan)
SHADER_PARAMETER(FVector4, SubsectionOffsetParams)
SHADER_PARAMETER(FVector4, LightmapSubsectionOffsetParams)
SHADER_PARAMETER(FVector4, BlendableLayerMask)
SHADER_PARAMETER(FMatrix, LocalToWorldNoScaling)
SHADER_PARAMETER_TEXTURE(Texture2D, HeightmapTexture)
SHADER_PARAMETER_SAMPLER(SamplerState, HeightmapTextureSampler)
SHADER_PARAMETER_TEXTURE(Texture2D, NormalmapTexture)
SHADER_PARAMETER_SAMPLER(SamplerState, NormalmapTextureSampler)
SHADER_PARAMETER_TEXTURE(Texture2D, XYOffsetmapTexture)
SHADER_PARAMETER_SAMPLER(SamplerState, XYOffsetmapTextureSampler)
END_GLOBAL_SHADER_PARAMETER_STRUCT()
// 在下面函数会对这个 Uniform 进行赋值
void FLandscapeComponentSceneProxy::OnTransformChanged()
{
// Set FLandscapeUniformVSParameters for this subsection
FLandscapeUniformShaderParameters LandscapeParams;
LandscapeParams.ComponentBaseX = ComponentBase.X;
LandscapeParams.ComponentBaseY = ComponentBase.Y;
LandscapeParams.SubsectionSizeVerts = SubsectionSizeVerts;
LandscapeParams.NumSubsections = NumSubsections;
LandscapeParams.LastLOD = LastLOD;
LandscapeParams.HeightmapUVScaleBias = HeightmapScaleBias;
LandscapeParams.WeightmapUVScaleBias = WeightmapScaleBias;
}
RenderDoc 抓帧查看 Uniform Buff 数据
Primitive 数据如下:
Landscape 参数
最终计算得出的 Mesh 如下: