如何在unity中手写一个四叉树地形lod系统(二)

发布时间 2023-04-06 09:42:27作者: 遥岑寄

  在根据四叉树节点创建了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等级的地块的连接处的露缝问题,应该在后续中改进

       这里我仅仅实现了最基本的网格动态显示,没有考虑渲染,在以后的改进中,我会尝试在地形渲染方面做出更多改进。