在这篇文章里,主要是实现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>(); 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); } }
最终的效果是:
离玩家的位置越远,地形的细节就越低。