在根据四叉树节点创建了1365个地形分块网格并保存到本地后,我们接下来要在游戏运行的过程中动态地显示所需的网格,这是最关键的一步。
如何根据摄像机位置动态地选择地形块?这其中体现了由整体到局部,从简单到复杂的原则。
0、 我们首先创建三个缓存列表。
1、 我们先将索引为0的地形分块(即最高LOD等级)的分块放入BufferA;
2、 然后遍历BufferA,判断BufferA中的每一个元素是否符合“无需更加详细”的条件,如果是,将它放入BufferFinal,否则放入BufferB;
3、 在遍历完BufferA中的元素后,清空BufferA,将BufferB的元素全部复制到BufferA中,清空BufferB;
4、 重复2-3步骤的操作,直到BufferA、BufferB列表均空。
此时BufferFinal中存储的索引即是我们最终所需要的地形网格分块的索引。
我们把以上的操作封装成函数,在游戏开始运行时调用一次。这一部分的代码如下:
private void TerrainGen() // 生成(更新)地形网格 { // 使用了子物体网格的方法,而不是网格合并的方法 // 因此需要首先清除所有子物体 for (int i = 0; i < transform.childCount; i++) { Destroy(transform.GetChild(i).gameObject); } // 四叉树计算 // 三个buffer计算方法 List<int> BufferA = new List<int>(); List<int> BufferB = new List<int>(); List<int> FinalBuffer = new List<int>(); Vector3 ppos = player.transform.position; // 迭代计算 BufferA.Add(0); // bufferA初始化,加入根节点 while (BufferA.Count != 0) // 当bufferA不为空时 { while (BufferA.Count != 0) // 遍历buffera每个值 { int i = BufferA[0]; float dist = Mathf.Sqrt(Mathf.Pow((qTree[i].begin_Pos.x + 64 * qTree[i].interval) * 5 - ppos.z, 2) + Mathf.Pow((qTree[i].begin_Pos.y + 64 * qTree[i].interval) * 5 - ppos.x, 2)); // 计算瓦片中心距离玩家位置的水平距离 BufferA.Remove(i); if (dist >= 0.8 * 128 * qTree[i].interval * 5) // 1表示1倍瓦片边长;128表示瓦片行列数;5时xzbias(此处其实应该参数化) { // 那么这张瓦片距离玩家太远,可以直接使用当前lod,不用细化 FinalBuffer.Add(i); } else { // 否则这张瓦片距离玩家很近,如果有更小的lod,应该细化; if(qTree[i].LodLeval == 0) // 如果已经是最细节的瓦片了,那没办法了,直接显示 { FinalBuffer.Add(i); } else // 否则把它的四个更细节的子节点加入到待计算的b buffer { for(int j = 1; j <= 4; j++) { BufferB.Add(i * 4 + j); } } } } // 将ab buffer交换 foreach(int i in BufferB) { BufferA.Add(i); } BufferB.Clear(); } Mesh[] tiles = new Mesh[FinalBuffer.Count]; Material mat = GetComponent<Renderer>().material; for (int i = 0; i < FinalBuffer.Count; i++) { tiles[i] = new Mesh(); tiles[i].indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; string path = @"Assets\LodMeshes\LodMesh_" + FinalBuffer[i].ToString() + ".asset"; tiles[i] = (Mesh)AssetDatabase.LoadAssetAtPath(path, typeof(Mesh)); GameObject theTile = new GameObject(); theTile.transform.parent = this.transform; theTile.name = "lod_" + i.ToString(); theTile.AddComponent<MeshFilter>(); theTile.AddComponent<MeshRenderer>(); theTile.AddComponent<Renderer>(); theTile.GetComponent<MeshFilter>().mesh = new Mesh(); theTile.GetComponent<MeshFilter>().mesh = tiles[i]; theTile.GetComponent<Renderer>().material = mat; } }
这一部分我选择将需要的网格实例化到子对象中而没有合并,如果需要优化的话,应该将网格合并成一个,可以减少DrawCall的数量。
同时要注意的是:网格的实例化与显示并非在每一帧进行,可以维护一个数对来表示玩家摄像机所在的区域,如果玩家摄像机离开了原本的区域进入到新的区域中,那我们便执行一此地形网格更新操作:
void Update() { if((int)player.transform.position.x / (64 * 5) != x_area || (int)player.transform.position.z / (64 * 5) != z_area) // 玩家区域改变 { TerrainGen(); x_area = (int)player.transform.position.x / (64 * 5); z_area = (int)player.transform.position.z / (64 * 5); } }
补充一点,在此之前我们可以从本地读取,在上一节生成网格时保存的四叉树信息,这部分代码很简单,如下所示:
private void ReadQTree() { using(StreamReader RawTerrainData = new StreamReader(@"E:\Unity\MyProjects\Desert_01\Assets\TerrainTree\MyQTree.txt")) { string line; for (int i = 0; i < qTree.Length; i++) // 读取所有顶点 并且给原始uv数据赋值 { line = RawTerrainData.ReadLine(); string[] stringdata = line.Split(' '); // 读取并写入 float.TryParse(stringdata[0], out qTree[i].begin_Pos.x); float.TryParse(stringdata[1], out qTree[i].begin_Pos.y); int.TryParse(stringdata[2], out qTree[i].interval); int.TryParse(stringdata[3], out qTree[i].LodLeval); float.TryParse(stringdata[4], out qTree[i].Center.x); float.TryParse(stringdata[5], out qTree[i].Center.y); } } }
最终的效果如下:
至此,我们大致完成了一个非常基础的一个四叉树网格地形系统,这其中还有很多问题,我大致思考了一下改进的方向:
性能优化方面的问题问题,比如显示的网格应该合并成一个而非保持多个对象;明显超出视线范围的地形网格分块应该直接剔除掉而非继续显示等;
代码复用性的方面的问题,有许多数据直接写死在代码里面,导致耦合度过高。在改进的时候,应该将这些数据参数化,将算法更优化,来降低耦合度,增强代码对不同大小的地形的复用能力;
效果实现方面的问题,没有考虑不同LOD等级的地块的连接处的露缝问题,应该在后续中改进
这里我仅仅实现了最基本的网格动态显示,没有考虑渲染,在以后的改进中,我会尝试在地形渲染方面做出更多改进。