只羊的博客

记录游戏开发历程

策略游戏地图制作(四)-地貌制作-上

1.地貌制作概述

本节先以一些策略游戏里的地貌为例,解释它们的地图地貌,之后提出一个简单的地貌模型,以下是该地貌模型应用到地形上的预览

    image    
图:让地形应用地貌图

1.1.一些游戏中的地貌

游戏《欧陆风云4》的地图相关资源暴露比较充分,在 Europa Universalis IV / map 文件夹下存放了大量与游戏地图构建相关的纹理资产、地貌图集、区域划分等文件。

在map\terrain文件中列出了所有的地形种类,所有地形都有对应的色彩表。常用的地形颜色有:平原(86 124 27),丘陵(0 86 6),海岸线(255 247 0),山脉(65 42 17),沼泽/湿地(75 147 174),草原(200 214 107),沙漠(200 214 107)。

这里采用的方式是地形以纯色对应,输出一张简单地貌图,即下图。在这张简单地貌图中已有大致的地貌样式,地图效果也勉强可以。但仅仅是简单的纯色图还是太单调了,可以使用更多的操作来提升地图效果。通常来讲有很多的做法,如叠加噪声让地表看起来起伏不平,叠加纹理贴图等。

    image    
图:地形图 Europa Universalis IV\map\terrain.bmp

下图是游戏地图里的秋季地貌,混合了地形纹理之后进行导出。可以从图中看出terrain.bmp即纯色地图的概貌,在纯色地图的基础上叠加地形纹理,再做colorgrading之类的操作即可得到下图。当然,按下图的精细度,仅有上面的步骤还是不行的,如果只考虑程序化生成,还需要叠加高度图、法线图等:

    image    
图:秋季地貌 Europa Universalis IV\map\terrain\colormap_autumn.dds

下图是游戏地貌的图集资产,这些纹理会叠加到地形图上。地形底色如上方所说是纯色对应。使用纯色作为有很多好处,如做季节、天气变化效果时,可以根据一张额外的混合贴图修改地图底色,省去很多操作。

    image    
图:地貌混合图集

当然,上面只是对EU4游戏的简单分析,要做出完整的EU4地图效果,仅有上面的是远不够的。如果你对欧陆风云4的自定义地图感兴趣,可以看看以下的wiki:
https://www.eu4cn.com/wiki/%E5%9C%B0%E5%9B%BE%E4%BF%AE%E6%94%B9

1.2.游戏地貌简单建模

如果只让美术/地编人员制作地图,那么地图效果毫无疑问会十分棒,但为了地图制作流程化与规范化考虑,PCG是必不可少的(毕竟你也不想看到美术累死吧)。程序化生成地图的方案在许多游戏里必不可少,而每类游戏又会有不同的地图需求,程序化生成方案需要具体定制。

继续以Paradox公司为例,P社有两种游戏引擎,Europa和Clausewitz(克劳塞维兹)。2000年的P社的第一款游戏欧陆风云1使用Europa引擎,2007年P社又推出了Clausewitz引擎,使用C++开发,目前支持DX11,该引擎以只用cpu0著称(祖宗之法不可变!)。P社引擎以构建3D视图的游戏大地图著称,适配其一直在开发的策略游戏。

扯的有些远了,但总的来讲,做地貌时应依据需求决定组件的轻重,以下提供一个简单的地貌建模方法。我们可以考虑基于真实地理知识,为游戏中的地貌建模。通常来讲,地貌受三个因素影响:海拔,湿度,温度。而海拔主要通过温度来影响地貌,所以也可以说只有温度、湿度两个要素

我们可以按照以下理念确定地形:
-温度越低的地方越偏冷色调,越高的地方偏暖色调;
-湿度越高的地方绿色越重,否则就越干旱,黄色更重;
-每种地形均是纯色,可以使用插值来丰富下表现;

基于此,我们简单地根据温度、湿度做个地形颜色对应图,如图所示:

    image    
图:简单地貌模型

一个地点的高度可以由高度图得出,湿度图、温度图可以由真实地理数据得出,或者手刷(通过绘图工具即可),使用时获取到数值即可,然后映射到地形纯色

    image    
图:一张湿度图范例

上面的地貌模型参考了hex的地貌构建思想,链接为(这位大佬的其他博客也很不错):
http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/

1.3.简单地貌模型应用

以下实践上述提出的模型:

建立湿度、温度-模型的映射关系(下方的颜色数组很混乱,读者可自行更改地形颜色):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
    public class LandformDataModel {
static Color Snow = new Color(248, 248, 248);
static Color Grass = new Color(195, 211, 169);
static Color GreenLand = new Color(116, 182, 88);
static Color DenceForest = new Color(71, 160, 71);
static Color Forest = new Color(155, 203, 149);
static Color Desert = new Color(235, 220, 194);

// temperature - humidity : Color dictionary
// humidity : low - high
// temp : low - high
static List<Color> LandformModel = new List<Color> {
new Color(153, 153, 153), new Color(187, 187, 187),new Color(221, 221, 187),
Snow, Snow, Snow,
new Color(187, 187, 187), new Color(220, 224, 195), new Color(196, 204, 187),
new Color(196, 204, 187), new Color(204, 212, 187),new Color(204, 212, 187),
new Color(220, 224, 195), Grass, Grass,
new Color(180, 201, 169), new Color(180, 201, 169), new Color(164, 196, 168),
new Color(220, 224, 195), Grass, new Color(180, 201, 169),
new Color(180, 201, 169), new Color(156, 187, 169), GreenLand,
Desert, Grass, new Color(193, 208, 174),new Color(156, 187, 169), GreenLand, GreenLand,
Desert, new Color(192, 214, 158), Forest, Forest, DenceForest, DenceForest
};

public static Color SampleColor(float humidity, float temperature) {
float HLevel = GetHumidityLevel(humidity);
float TLevel = GetTemperatureLevel(temperature);

int HLevel_l = Mathf.Clamp((int)HLevel, 0, 5);
int HLevel_r = Mathf.Clamp(HLevel_l + 1, 0, 5);

int TLevel_l = Mathf.Clamp((int)TLevel, 0, 5);
int TLevel_r = Mathf.Clamp(TLevel_l + 1, 0, 5);

float HRatio = HLevel - HLevel_l;
float TRatio = TLevel - TLevel_l;

// bilinear caculate
Color leftUp = LandformModel[TLevel_l * 6 + HLevel_l];
Color leftDown = LandformModel[TLevel_r * 6 + HLevel_l];
Color rightUp = LandformModel[TLevel_l * 6 + HLevel_r];
Color rightDown = LandformModel[TLevel_r * 6 + HLevel_r];

Color color1 = Color.Lerp(leftUp, rightUp, HRatio);
Color color2 = Color.Lerp(leftDown, rightDown, HRatio);
Color color = Color.Lerp(color1, color2, TRatio);
color.r /= 255.0f;
color.g /= 255.0f;
color.b /= 255.0f;
return color;
}

public static float GetHumidityLevel(float humidity) {
// level 1 : 0, level 6 : 100;
float level1H = 0;
float level6H = 100;
float level = Mathf.Lerp(level1H, level6H, humidity / level6H);
return level;
}

public static float GetTemperatureLevel(float temperature) {
// level 1 : 0, level 6 : 30
float maxT = 30;
float level1T = 0;
float level6T = 5;
float level = Mathf.Lerp(level1T, level6T, temperature / maxT);
return level;
}
public static float GetHumidity(Vector3 vertPos) {
// 使用湿度贴图进行采样
}
public static float GetTemperature(Vector3 vertPos) {
// 使用温度贴图进行采样
}
}

导出地貌图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public bool ExportFlipVertically = true;
public int ExportTexResolution = 1024;
public Texture2D curHandleTex;
public string landformTexImportPath;
public string curLandformTexPath;

private void ExportLandFormTex() {
HeightDataManager heightDataManager = new HeightDataManager();
heightDataManager.InitHeightDataManager(heightDataModels, MapTerrainEnum.ClusterSize);
curHandleTex = new Texture2D(ExportTexResolution, ExportTexResolution, TextureFormat.RGB24, false);
Color[] colors = curHandleTex.GetPixels();
for (int i = 0; i < ExportTexResolution; i++) {
for (int j = 0; j < ExportTexResolution; j++) {
Vector3 vertPos = new Vector3(i, 0, j);
vertPos.y = heightDataManager.SampleFromHeightData(startLongitudeLatitude, vertPos);
int idx = 0;
if (ExportFlipVertically) {
idx = j * ExportTexResolution + i;
} else {
idx = i * ExportTexResolution + j;
}
float humidity = LandformDataModel.GetHumidity(vertPos);
float temperature = LandformDataModel.GetTemperature(vertPos);
colors[idx] = GetColorByHeight(vertPos.y);
}
}
curHandleTex.SetPixels(colors);
curHandleTex.Apply();
Debug.Log(string.Format("successfully generate texture, resolution : {0}x{0}", ExportTexResolution));
}

执行上述的导出逻辑后,可以获得如下的地貌图:

    image    
图:导出后的地貌图

如果需要自定义地形颜色,更改颜色后,又可以得到不一样的地貌图

    image    
图:修改地形颜色后的地貌图

将该地貌贴图应用于地形上(因为只是应用一张贴图,shader逻辑并不复杂,此处不放),效果图如下:

    image    
图:让地形应用地貌图
    image    
图:拉近后的效果

2.混合纹理方案概述

本节不涉及项目内代码,简要讨论下混合纹理大地图的方案,它是经典的大世界地图解决方案,适合处理纹理量大时的地形。

通常来讲,当我们需要混合多个地形纹理时,有如下的方案
-直接使用一张BlendTexture存储权重,例如BlendTexture四个通道RGBA各自存储对应的纹理图的权重值。是比较直观又简单的方法,支持四张纹理混合
-使用纹理的A通道存储权重,即为每张贴图多添加一个A通道存权重(透明度),叠加得到最终颜色

上述的方案在地形纹理规模扩大的时候弊病会显得很严重,如果有n张纹理,就需要有n/4张索引贴图,采样n+n/4,如果算上法线则更加严重。所以目前的游戏制作主流方案会采用一张IndexTexture+纹理图集来减少采样次数,采样时先获取索引再去找到图集对应纹理(2次)。

    image    
图:一张纹理图集

如果要实践该方案,那么这个方案需要:

-4*4的纹理图集:对应16种地形
-indexTexture:RGBA四通道分别存储一种纹理的索引,对应blenderTexture
-blenderTexture:RGBA四通道存储纹理的混合权重值

计算过程:
Color = TexArray[indexTex.R]*blendTex.R + TexArray[indexTex.G]*blendTexG + TexArray[indexTex.B]*blendTex.B + TexArray[indexTex.A]*blendTex.A

目前没有代码,之后再来尝试该方案