程序化生成开放世界地形二(Endless Terrain)


在这篇文章里,主要是实现Endless Terrain,以及我对这几个技术的学习、总结以及实践,这些东西的学习真的是花了大量的时间和精力,看了好几个外国大佬的博客,自己写了一个国庆,才终于实现了出来(其实是因为弄unity的多线程花了挺多的时间)。可能有点长,如果你也想做Procedural Terrain,看完这篇你一定能有大收获。

Chunks

unity对于一个mesh的绘制是有着顶点数上限限制,所以要实现大地形,需要随机生成很多块地形,每个地形称为一个chunk,生成多少个chunk则取决于玩家能看到的最远处,如果玩家位置发生了改变,那么在玩家目前MaxViewDst外的chunk就设置为不可见,生成的其它chunk就拼接在玩家目前所在chunk的四周,这样就能做出Endless Terrain。

1
2
3
4
5
6
7
public const float maxViewDst = 300;
public Transform viewer;

public static Vector2 viewerPosition;
static NoiseMapGenerator mapGenerator;
int chunkSize;//就是一个地图块的大小
int chunkVisibleInViewDst;

在一条视线方向上可见的chunk个数就为

1
2
chunkSize = NoiseMapGenerator.mapChunkSize - 1;
chunkVisibleInViewDst = Mathf.RoundToInt(maxViewDst / chunkSize);

我把整个世界划分成网格,每个网格就是一个chunk,玩家所能看到的chunkVisibleInViewDst就决定了这个chunk集的大小,假定能看到附近的一个chunk,那个一共要生成的chunk集就要有九个chunk:

我用一个TerrainChunk类去储存要生成的chunk的信息,它还要有一个方法去更新chunk的可见性,判断这个chunk是否在MaxViewDst内,如果在,那么让它可见

1
2
3
4
5
6
public void UpdateTerrainChunk()
{
float viewDstFromNearestEdge = Mathf.Sqrt(bounds.SqrDistance(viewerPosition));
bool visible = viewDstFromNearestEdge <= maxViewDst;
SetVisible(true);
}

对于生成的chunks,我用一个字典类型去存储,每当view的位置变化,都会计算出一个新的中心点,也就要基于这个点去生成新的chunk,如果已经有了这个chunk,那就直接把它setvisible就行了,而是否已经有了这个chunk,直接用key去查就行了,UpdateVisibleChunks函数如下

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
Dictionary<Vector2, TerrainChunk> terrainChunkDictionary = new Dictionary<Vector2, TerrainChunk>();//<位置,对应的地形>
List<TerrainChunk> terrainChunksVisibleLastUpdate = new List<TerrainChunk>();//viewer上一次移动时可见的chunk

for (int i = 0; i < terrainChunksVisibleLastUpdate.Count; i++)
{
terrainChunksVisibleLastUpdate[i].SetVisible(false);
}

int currentChunkCoordX = Mathf.RoundToInt(viewerPosition.x / chunkSize);
int currentChunkCoordY = Mathf.RoundToInt(viewerPosition.y / chunkSize);

for (int yOffset = -chunkVisibleInViewDst; yOffset <= chunkVisibleInViewDst; yOffset++)
{
for (int xOffset = -chunkVisibleInViewDst; xOffset <= chunkVisibleInViewDst; xOffset++)
{
Vector2 viewedChunkCoord = new Vector2(currentChunkCoordX + xOffset, currentChunkCoordY + yOffset);
if (terrainChunkDictionary.ContainsKey(viewedChunkCoord))
{
terrainChunkDictionary[viewedChunkCoord].UpdateTerrainChunk();
if (terrainChunkDictionary[viewedChunkCoord].IsVisible())
{ terrainChunksVisibleLastUpdate.Add(terrainChunkDictionary[viewedChunkCoord]);
}
}
else
{
terrainChunkDictionary.Add(viewedChunkCoord, new TerrainChunk(viewedChunkCoord, chunkSize, transform));
}
}
}

然后在update里面一直去更新chunks就可以了

1
2
3
4
5
private void Update()
{
viewerPosition = new Vector2(viewer.position.x, viewer.position.z);
UpdateVisibleChunks();
}

接下来就是用Threading去优化整个过程,避免在生成太多地形的时候导致卡顿。我需要在生成map的脚本里面得到Endless Terrain脚本里生成的所有chunks的数据,它不需要立即得到所有生成的数据。所以在mapGenerator脚本里面我们需要一个RequestMapData函数,它的参数是一个callback function,在Endless Terrain里面当我们计算好了数据,就是用这个对应的callback function。

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
Queue<MapThreadInfo<MapData>> mapdataThreadInfoQueue = new Queue<MapThreadInfo<MapData>>();
Queue<MapThreadInfo<MeshData>> meshDataThreadInfoQueue = new Queue<MapThreadInfo<MeshData>>();

struct MapThreadInfo<T>
{
public readonly Action<T> callback;
public readonly T parameter;

public MapThreadInfo(Action<T> callback, T parameter)
{
this.callback = callback;
this.parameter = parameter;
}
}

public void RequestMapData(Action<MapData> callback)
{
ThreadStart threadStart = delegate
{
MapDataThread(callback);
};

new Thread(threadStart).Start();
}

void MapDataThread(Action<MapData> callback)
{
MapData mapData = GenerateMapData();
lock (mapdataThreadInfoQueue)
{
mapdataThreadInfoQueue.Enqueue(new MapThreadInfo<MapData>(callback, mapData));
}
}

public void RequestMeshData(MapData mapData, Action<MeshData> callback)
{
ThreadStart threadStart = delegate
{
MeshDataThread(mapData, callback);
};
new Thread(threadStart).Start();
}

void MeshDataThread(MapData mapData, Action<MeshData> callback)
{
MeshData meshData = MeshGenerator.GenerateTerrainMesh(mapData.heightMap, meshHeightMultiplier, meshHeightCurve, levelOfDetail);
lock (meshDataThreadInfoQueue)
{
meshDataThreadInfoQueue.Enqueue(new MapThreadInfo<MeshData>(callback, meshData));
}
}

在Update里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (mapdataThreadInfoQueue.Count>0)
{
for (int i = 0; i < mapdataThreadInfoQueue.Count; i++)
{
MapThreadInfo<MapData> threadInfo = mapdataThreadInfoQueue.Dequeue();
threadInfo.callback(threadInfo.parameter);
}
}

if (meshDataThreadInfoQueue.Count>0)
{
for (int i = 0; i < meshDataThreadInfoQueue.Count; i++)
{
MapThreadInfo<MeshData> threadInfo = meshDataThreadInfoQueue.Dequeue();
threadInfo.callback(threadInfo.parameter);
}
}

然后在Endless Terrain里去实做Received Function

1
2
3
4
5
6
7
8
9
void OnMapDataReceived(MapData mapData)
{
mapGenerator.RequestMeshData(mapData, OnMeshDataRecieved);
}

void OnMeshDataRecieved(MeshData meshData)
{
meshFilter.mesh = meshData.CreateMesh();
}

然后我们就可以得到多个chunk拼接起来的地形

LOD Switching

在真实的游戏世界中,对于远处的物体,可以不需要太多细节,因此在做地形时,我们也让远处地形的detail少一些,去做一个根据距离的LOD Switching。我使用了一个LODMesh类,它包含了一个回调函数,可以在创建这个地形时告诉目前这个chunk的LOD是多少。

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
public class LODMesh
{
public Mesh mesh;
public bool hasRequestedMesh;
public bool hasMesh;
int lod;

System.Action updateCallback;

public LODMesh(int lod, System.Action updateCallback)
{
this.lod = lod;
this.updateCallback = updateCallback;
}

void OnMeshDataReceived(MeshData meshData)
{
mesh = meshData.CreateMesh();
hasMesh = true;

updateCallback();
}

public void RequestMesh(MapData mapData)
{
hasRequestedMesh = true;
mapGenerator.RequestMeshData(mapData, lod, OnMeshDataReceived);
}
}

最终的效果是:

离玩家的位置越远,地形的细节就越低。


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