在根據四叉樹節點創建了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 = https://www.cnblogs.com/yaocenji/p/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 = https://www.cnblogs.com/yaocenji/p/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等級的地塊的連接處的露縫問題,應該在后續中改進
這里我僅僅實作了最基本的網格動態顯示,沒有考慮渲染,在以后的改進中,我會嘗試在地形渲染方面做出更多改進,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/549304.html
標籤:其他
