程序化生成开放世界地形一


Procedural Generation

之前研究了一下程序化生成,这是个很复杂的技术,开放世界游戏最需要的,就是可以很方便快速且随机地去生成地形,然后美术可以用很多接口去控制地形的细节,然后给地形上植被、建筑等等。这个系列文章会总结一下我学到的相关技术,会基于噪声去生成高度图,自己去储存mesh的三角面然后给Mesh Filter,这样我们可以控制地形的细分程度,然后控制地形的细节,然后会用LOD的思想去做无尽的地形,进行地形分块。

这篇文章我会使用噪声去生成一张高度图,然后储存mesh,根据高度图去生成高低起伏的地图。

生成Noise Map

首先要通过噪声生成高度图,我用曲线表示地形,不同高度的曲线叠加,去增加地形的细节。将单独的一个波形称为Octave,将多个Octave叠加起来,就可以得到我想要的效果。然后另外用两个变量去控制叠加的波形的波峰高度和频率。

具体代码如下:

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
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float amplitude = 1;
float frequency = 1;
float noiseHeight = 0;

for (int i = 0; i < octaves; i++)
{
float samplerX = (x - halfWidth) / scale * frequency + octaveOffsets[i].x;
float samplerY = (y - halfHeight) / scale * frequency + octaveOffsets[i].y;
//转换到-1~1
float perlinValue = Mathf.PerlinNoise(samplerX, samplerY) * 2 - 1;

noiseHeight += perlinValue * amplitude;
amplitude *= persistance;
frequency *= lacunarity;
}

if (noiseHeight > maxNoiseHeight) maxNoiseHeight = noiseHeight;
else if (noiseHeight < minNoiseHeight) minNoiseHeight = noiseHeight;

noiseMap[x, y] = noiseHeight;
}
}

其中,Lacunarity表示频率的增加量,Persistence是一个0-1的数,它表示增幅的减小量,也就是后面叠加的波峰的最大高度,octave的个数就代表着地形细节的多少。

然后再根据这个噪点图给一个texture上色,每一个点的颜色则取决于相同大小的噪点图的这个点对应的值,那我就从黑到白做一个lerp,lerp的值就是noiseMap[x, y]存的值。然后我们把这个texture通过renderer组件的sharedMaterial给平面材质的texture。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int width = heightMap.GetLength(0);
int height = heightMap.GetLength(1);

Color[] colorMap = new Color[width * height];
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
colorMap[y * width + x] = Color.Lerp(Color.black, Color.white, heightMap[x, y]);
}
}

Texture2D texture = new Texture2D(width, height);
texture.filterMode = FilterMode.Point;
texture.wrapMode = TextureWrapMode.Clamp;
texture.SetPixels(colorMap);
texture.Apply();

textureRenderer.sharedMaterial.mainTexture = texture;
textureRenderer.transform.localScale = new Vector3(texture.width, 1, texture.height);

这样就可以生成一个可以调整细节的噪声图:

根据高度划分地形

首先建立一个地形数组,根据高度给地形赋予不同的颜色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Color[] colorMap = new Color[mapHeight * mapWidth];

for (int x = 0; x < mapHeight; x++)
{
for (int y = 0; y < mapWidth; y++)
{
float currentHeight = generatedNoise[x, y];
for (int i = 0; i < regions.Length; i++)
{
if (currentHeight<=regions[i].height)
{
colorMap[y * mapWidth + x] = regions[i].color;
break;
}
}
}
}

然后用同样的方法画到一个texture上:

可以看到已经得到了一个类似的2D的地形图片

储存Mesh,LOD,赋予高度

3D地形的生成主要是生成mesh,然后对每一个vertex做高度的变化,对于一个mesh我们需要存储它的uv、vetices和normals。而具体来说就是存储组成这个mash的三角形的过程,每个三角形都包含了每个顶点的坐标,顶点顺序,法线,uv坐标,顶点颜色。现在每一个像素点就是一个三角形的顶点,每三年个顶点就决定了一个三角形,就要想办法把三角形存储起来。

我使用了一个数组去存所有的三角形,采用顺时针的方式

所有三角形的数量为 (width - 1)* (h - 1)* 2,所以需要储存的三角形顶点为(width - 1)* (h - 1)* 6,意味着需要开一个这么大的数组去存这些有序顶点。

我用一个MeshData类去存储一个Mesh需要的数据:

1
2
3
4
5
public Vector3[] vertices;
public int[] triangles;
public Vector2[] uvs;

int triangleIndex;//存储的三角形的序号

这个类包含了创建一个Mesh的初始化数据,以及储存三角形顶点的函数:

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
public MeshData(int meshWidth, int meshHeight)
{
//顶点个数、uv、三角面个数
vertices = new Vector3[meshWidth * meshHeight];
uvs = new Vector2[meshWidth * meshHeight];
triangles = new int[(meshWidth - 1) * (meshHeight - 1) * 6];
}

public void StoreTriangles(int a, int b,int c)
{//abc:mesh顶点的编号,对应一个三角形的三个顶点
//存进数组
triangles[triangleIndex] = a;
triangles[triangleIndex + 1] = b;
triangles[triangleIndex + 2] = c;
triangleIndex += 3;
}
public Mesh CreateMesh()
{
Mesh mesh = new Mesh();
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.uv = uvs;
mesh.RecalculateNormals();
return mesh;
}

储存三角形的方法为:

也就是:

1
2
meshData.StoreTriangles(vertexIndex, vertexIndex + verticesPerLine + 1, 			vertexIndex + verticesPerLine);
meshData.StoreTriangles(vertexIndex + verticesPerLine + 1, vertexIndex, vertexIndex + 1);

然后实现高低起伏的地形,我们使用heightMultiplier这个参数去乘每个顶点的y值,大小就由噪点的值确定,为了更好地控制地形,避免低的海面也被乘上这个数值,我是用了一个curve去进行控制,curve的参数就是一个0~1的数,某个值以下就置0,这样y就不会被heightMultiplier影响。

然后我们去实现mesh的细分程度,我们要可以自己去调变三角面的个数,也就是地形的细分程度。正常情况下在遍历所有顶点时,是用++,如果我们每次加2,或者说每次跨度是宽度的一个因数,那么这个跨度越大,存下来的三角面就越大,也就是说它的level of detail越低。而受限于unity支持的顶点数65000,使用的plane最大为254×254,我选择使用241大小的正方形,因为它的因数较多,能有更多的跨度,也即是level数:

核心代码:

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
int width = heightMap.GetLength(0);
int height = heightMap.GetLength(1);
float leftmostX = (width - 1) / -2f;
float rightmostZ = (height - 1) / 2f;

//levelOfDetail:细分程度
int simplificationIncreament = (levelOfDetail == 0) ? 1 : levelOfDetail * 2;//跨度
int verticesPerLine = (width - 1) / simplificationIncreament + 1;

MeshData meshData = new MeshData(width, height);
int vertexIndex = 0;

for (int y = 0; y < height; y += simplificationIncreament)
{
for (int x = 0; x < width; x += simplificationIncreament)
{
meshData.vertices[vertexIndex] = new Vector3(leftmostX + x, heightCurve.Evaluate(heightMap[x, y]) * heightMultiplier, rightmostZ - y);
meshData.uvs[vertexIndex] = new Vector2(x / (float)width, y / (float)height);

if (x < width - 1 && y < height - 1)
{
meshData.StoreTriangles(vertexIndex, vertexIndex + verticesPerLine + 1, vertexIndex + verticesPerLine);
meshData.StoreTriangles(vertexIndex + verticesPerLine + 1, vertexIndex, vertexIndex + 1);
}
vertexIndex++;
}
}

创建mesh之后通过meshFilter.sharedMesh = meshData.CreateMesh()把mesh赋给目标就ok。

不同的level of detail:


Author: cdc
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source cdc !
  TOC