前言
随着电脑和手机硬件性能越来越高,游戏越来越追求大世界,而大世界非常核心的一环是地形系统,地形系统两大构成因素:HeightMap地形网格和多材质混合,此篇文章介绍下UE4/UE5 地形的材质混合方案----基于WeightMap混合。
材质层
地形着色的基础组成,比如雪, 沙漠,岩石,绿地等等。在UE5创建材质层是材质的节点LandscapeLayerBlend节点中创建

LandscapeLayerBlend节点每个输入上游就是一个材质基础层


这里我用简单的例子,假定材质基础层都是一个float3常量颜色,比如float3(1.0, 0.2, 0.1)。
WeightMap
各个材质基础层按照一定权重混合得到最终的效果,

WeightMap就是材质权重图,里面存储了各个基础层材质混合的权重值。在UE5里,WeightMap的格式是RGBA8,可以存储四层材质层的权重值,精度为0 - 255 (对应 0.0 - 1.0的精度)。
WeightMap数量和通道使用
从之前我UE地形系列文章,我们知道UE地形是由一个个地形块(LandscapeComponent)组成,一个LandscapeComponent可以存在多个WeightMap, 假设存在N个WeightMap.

那么最终地块的基础材质层总数:4 * (N - 1) < MaterialLayerNum<= 4 *N.
这里之所以用"<=" 和 ">", 而不是"= 4 * N",是因为一张WeightMap不一定会用完所有通道。比如当前地形块刷了5种材质基础层,则该地形块就存在两张WeightMap, 第一张WeightMap RGBA都用来存储四种材质基础层的权重,第二张WeightMap的R通道用来存储第五种材质基础层的权重,剩余的GBA通道都是零权重。


材质基础层用了哪张WeightMap的哪个通道是记录在ULandscapeComponent里

WeightMap存储权重总和
不管地形块刷了多少层材质基础层,最终地形某个点的各层权重总和为1.0(255).比如说上面的地形刷了五层材质基础层,有两张WeightMap,分别为W1和W2, W1[n][n].R 代表访问W1权重图的N行N列的像素R通道值。
则存在公式:
W1[n][n].R + W1[n][n].G + W1[n][n].B + W1[n][n].A + W2[n][n].R = 255(byte)
(注意:W2[n][n].G = W2[n][n].B = W2[n][n].A = 0)
UE5地形权重混合的HLSL代码分析
生成的具体HLSL代码
以上面的5层材质的为案例, 抓帧得到的HLSL代码(位于Material.ush文件)

大致能看到采样了WeightMap1和WeightMap2, 然后大致能看到颜色混合了5次。
为更清楚的表示, 手写HLSL代码表示,大致如下:
cpp
float4 Weigh1 = Texture2DSample(WeightMap1, UV);
float4 Weigh2 = Texture2DSample(WeightMap2, UV);
float4 Layer1Mask = float(1.0, 0.0, 0.0, 0.0);
float4 Layer2Mask = float(0.0, 1.0, 0.0, 0.0);
float4 Layer3Mask = float(1.0, 0.0, 1.0, 0.0);
float4 Layer5Mask = float(0.0, 0.0, 0.0, 1.0);
float4 Layer6Mask = float(1.0, 0.0, 0.0, 0.0);
float3 L1 = float3(0.78437978,0.95937508,0.50223249);
float3 L2 = float3(0.98749989,0.06583356,0.17528065);
float3 L3 = float3(0.45752281,0.53502721,0.88749981);
float4 L5 = float3(0.22312574,0.97916698,0.28475177);
float4 L6 = float3(0.88749981,0.25128645,0.26541224);
float3 BaseColor = float3(0.0, 0.0, 0.0);
// five layer blend
BaseColor += dot(Layer1Mask, Weigh1) * L1;
BaseColor += dot(Layer2Mask, Weigh1) * L2;
BaseColor += dot(Layer3Mask, Weigh1) * L3;
BaseColor += dot(Layer5Mask, Weigh1) * L5;
BaseColor += dot(Layer6Mask, Weigh2) * L6;
这里dot(float4(1, 0, 0,0),A),其实就是取A的R通道, 其他类似道理。
地形材质Shader代码编译流程
上面可以更清楚整个材质权重混合的流程. 代码是动态生成的,和地块现在使用到的实际材质基础层数量相关,所以在刷UE地形的时候,刷一种未出现的新材质会引起材质编译

编译生成流程的最终在UMaterialExpressionLandscapeLayerBlend::Compile编译Shader代码,

这里判断是否需要编译发生在ULandscapeComponent::GetCombinationMaterial里
首先ULandscapeComponent 的所属ALandscapeProxy 里存在一个 MaterialInstanceConstantMap 材质实例管理表

这个表会缓存出现过的各种地形材质实例,MaterialInstanceConstantMap的key代表使用了哪些材质基础,并且哪个基础材质使用了哪张权重纹理,如下红圈所示:
代表了LandscapeMaterialInstanceConstant_10 材质实例是在 L1. L3, L3,L7 使用WeightMap0, L6 使用了WeightMap0的情况下编译出来的。

如果在MaterialInstanceConstantMap 找到同个Key的, 就使用已有的MaterialInstance, 如果找不到就以ALandscape的母材质为基础创建新的材质实例,并且设TerrainLayerWeightParameters材质静态编译信息, 触发编译。


上面WeightMap%d和LayerMask_%s 指在Shader代码中生成了每个Layer对应的权重图和纹理通道遮罩索引,在上面的HLSL实现地形材质混合的简化代码大致相符合。
当然最终这个材质实例是保存到ULandscapeComponent的MaterialInstances,被UPROPERTY序列化下来。
当然最终材质编译完后,得更新地形的材质实例参数值: WeightMap和LayerMask遮罩

参考资料
UE5.2的地形系统实现