目前在网上能找到的关于利用高度图生成地形的文章很多,使用的高度图有的是通过各大地图服务获取,有的是使用各种图形软体自行绘制,也有些使用noise纹理来制作随机地形。

今天这一章来讲一点不一样的,使用mapbox提供的terrain-rgb图像来生成真实地形。

通过mapbox提供的API获取到的一张terrain-rgb,位置是death valley

一张黑白的高度图,来自https://en.wikipedia.org/wiki/Heightmap

与我们平时所熟悉的黑白高度图相比,mapbox提供的rgb图像通常看起来是蓝绿色,在一些临近像素的颜色可能会出现极大的差别。这是由于terrain-rgb使用了RGB三个通道来共同表示某个区域的海拔高度,比起只使用了一个有效通道的灰度图包含了更多信息,但可读性也变差了。

关于terrain-rgb的编码方式,阅读 : blog.mapbox.com/global-

在获取了高度图后,我们首先要做的是将它转换为高度数据。

首先准备好生成地形的各种参数:

[SerializeField]
Texture2D tex2d;//高度图
[SerializeField]
int sx=20;//mesh在x和y方向的分段数量
[SerializeField]
int sy=20;
[SerializeField]
float width=100;//生成的mesh的尺寸,这里表示的是正方向的边长
[SerializeField]
float heightscale=1; //添加一个系数来调整生成的地形y方向的缩放,如果要制作真实比例的地形,这个系数需要根据实际的距离和mesh的尺寸来计算。

以及生成一个mesh所需的一些数据:

Mesh mesh;
Vector3[] _vertices;
Vector2[] _uvs;
int[] _triangles;

把一张高度图案中sx*sy的块数切割,获得的顶点数量是(sx+1)*(sy+1)。要确保采样的点在一定在高度图的边缘像素上,每个顶点之间相差的距离(tex2d.width-1) / sx和(tex2d.height -1)/sy,之后按照顺序依次采样,并将获取的颜色换算成高度。

由于mesh的顶点数据保存在vertices中,所以在从高度图中取样的同时,顺便把vertices也准备好。地形的整体尺寸是width时,每个顶点在x和z方向的跨度就是width/sx和width/sy,而y值就是刚刚通过颜色换算出的高度。与heightscale相乘是为了让结果比较直观,与事后调整scale达到的是同样的效果,实际使用的时候可以酌情添加。

public void generateTerrain(){
float stepx = (tex2d.width-1) /(float) sx;
float stepy = (tex2d.height -1)/ (float)sy;
_vertices=new Vector3[(sx+1)*(sy+1)] ;
for (int yy = 0; yy <= sy; yy++) {
for (int xx = 0; xx <=sx; xx++) {
int self = xx + yy *(sx+1);
_vertices [self].x = xx * width/sx;
_vertices [self].z = yy * width /sy;
Color cc = tex2d.GetPixel (Mathf.FloorToInt( xx*stepx),Mathf.FloorToInt( yy*stepy));
float height = -10000 + (Mathf.Round (cc.r * 256) * 256 * 256 + Mathf.Round (cc.g * 256) * 256 + Mathf.Round (cc.b * 256)) / 10;
//https://blog.mapbox.com/global-elevation-data-6689f1d0ba65
height *= heightscale/width;
_vertices [self].y = height;
}
}
}

现在,每个顶点的信息按照从左下到右上的顺序存在了_vertices中。

接下来准备Triangles数组。根据官方文档中解释,Mesh.triangles储存的是vertices中顶点的排列顺序(The array is a list of triangles that contains indices into the vertex array. )

将顶点用横线和竖线连接起来后,整个地形就被划分成了sx*sy个小方块,接下来将每个小方块拆分为两个三角面,每个三角面对应三个顶点。

void setMeshTriangles()
{
int sum = Mathf.FloorToInt(sx * sy * 6);////每格两个三角形,6个顶点
_triangles = new int[sum];
int index = 0;
for (int yy = 0; yy < sy; yy++)
{
for (int xx = 0; xx < sx; xx++)
{
int self = xx + (yy * (sx + 1));
int next = xx + (yy + 1) * (sx + 1);
_triangles [index] = self;
_triangles [index + 1] = next + 1;
_triangles [index + 2] = self + 1;
_triangles [index + 3] = self;
_triangles [index + 4] = next;
_triangles [index + 5] = next + 1;
index += 6;
}
}
}

一个方块至少要由两个三角面来组成。只要把顶点按照顺时针方向排列,怎么划分三角面都能达到差不多的效果。

这里使用了0~7~1和0~6~7的方式来定义这两个三角面,顶点的排列顺序决定了三角面法线的方向。确定三角面的方式不唯一,只要三个顶点按照顺时针的顺序排列,用0~6~1,1~6~7的方式也是没有问题的。如果使用了逆时针的方法,比如0~7~6这样,法线就会变成向下的,在使用普通单面材质的情况下只能从下往上看才能看见正常的面。

用了0~7~1,0~7~6的方法,画出了分别能从正反面看见一部分的图形

接下来,如果需要给地形贴图的话,就需要设定一下每个顶点的uv,如果什么也不做的话,每个点都是默认值(0,0)。设定uv很简单,只要将0~1的值平均分配给每个顶点即可。

void setUV()
{
_uvs = new Vector2[(sx+1)*(sy+1)];
float u = 1.0F / sx;
float v = 1.0F / sy;
for (int yy = 0; yy <= sy; yy++)
{
for (int xx = 0; xx <= sx; xx++)
{
int self = xx + yy *(sx+1);
_uvs[self] = new Vector2(xx * u, yy * v);
}
}
}

最后,生成mesh,并用已经准备好的几个数组为其赋值。

void DrawMesh()
{
mesh = new Mesh ();
gameObject.AddComponent<MeshFilter> ().sharedMesh=mesh ;
////给mesh 赋值
mesh.Clear();
mesh.vertices = _vertices ;
mesh.uv = _uvs;
mesh.triangles = _triangles;
mesh.RecalculateNormals();////重置法线
mesh.RecalculateBounds();////重置范围
}

再添加meshrender,让生成的地形显示出来。

gameObject.AddComponent<MeshRenderer> ();

这样,一个根据真实地理数据制作的地形就生成出来了,将这块地形所对应的卫星图作为材质贴在上面,一块真实世界的地形就完成了。


这一章的内容出自另一个地形生成插件:

Real Terrain Maker - Asset Store?

assetstore.unity.com
图标

这一篇可能没有之前的文章那样有趣,这主要是由于被人打了两星之后,本月销量创了历史新低,作者产生了深深的焦虑,觉得有必要推广一下。

如果有兴趣可以买一份回去玩玩,提供了比较便宜的lite版本供大家试用,先买lite版本再升级可以享有一点微小的折扣。

建议在阅读使用说明或者看过demo之后理性购买,真的不要买后随便给差评啊,作者哭超凶的

这章所用到的资源在这里获取:

sakuraplus/make-terrain-with-google-elevation?

github.com

脚本挂在任意物体上即可使用。

如果出现Texture Trr00downloadHeightmap_143 is not readable这样的提示,将图片的import setting设为Read/Write Enabled即可。

这一章并没有讲将mesh转换为unity terrain的内容,但相关的文章很多,如有兴趣可以很容易的找到。


ps:

根据google给出的数据,在马里亚纳海沟的挑战者深渊处,海拔为-10166m,而terrain-rgb所能给出的最小值是-10000,这可能是在数据精度、计算难度和实际使用之间折中考虑所得出的结果。

google会给出xml或json格式的数据

另外,由于terrain-rgb所给出的是png格式的文件,所以理论上来说如果把alpha通道也一起利用上的话,就能让数据的精度更高。然而mapbox并没有这么做,而是使用a通道来表示数据缺失(通常发生在海里或较高纬度地区),果然人类对地球还需要更多的探索呢。(当然由于看图的时候a通道会对其他通道都造成影响,如果把a通道也拿来存高度数据的话,图的可读性就完全没有了:)


建了个客服群,方便一下有问题咨询的用户。店主会耐心解答大家的疑问,有需求可以加_(┐「ε:)_

关于Real Terrain Maker 的使用交流与答疑。关于Dynamic Flat Icon和Slideshow Effects 2D这两个没人买的插件的问题也可以聊。


推荐阅读:
相关文章