程序洞穴生成三(Procedural Cave Generation)

上一篇文章我们已经将地图的信息转换成了模型需要的Meh信息,但是生成的是一个平面的效果,所以接下来就要通过其他的方法生成3D的墙面,将上一篇生成的地图的边界衍生出垂直的墙面。

本篇本章的内容的视频链接https://www.youtube.com/watch?v=AsR0-wCTJl8

再接着上篇文章的内容之前,视频作者对上一个视频的一个错误的代码进行了修改,因为要通过边界来生成墙面,那么边界的点的顺序必然是需要有统一的规定,比如这个图(截自视频),如果此时我们的只有3的这个四方的顶点是active = true,或者说地图信息中的值是等于1的。那么如果只想生成一个面的话只要是顺时针的存储三角形的顶点都能够生成,但是如果AC是做为边界的话,ABC三角就是点A到点C,而BCA三角形的就是点C到点A,所以,为了统一边界的边的方向,视频作者对代码做了简单的修改,对TriangulateSquare函数中的Switch中的1,2,4的点的顺序做了修改。对于上一篇文章这个地方的代码就能发现区别。

            case 1:
                MeshFromPoints(square.centreLeft, square.centreBottom, square.bottomLeft);
                break;
            case 2:
                MeshFromPoints(square.bottomRight, square.centreBottom, square.centreRight);
                break;
            case 4:
                MeshFromPoints(square.topRight, square.centreRight, square.centreTop);

好了,接下来进入正题,将修改后生成的平面地图立体起来!

首先我们得知道一条边在满足什么条件的情况是一个边界。举个例子

 (截自视频)这个图中的白色的边,显然不是一个边界,边上边的那个点被AB两个三角形公用,下面的点被ABC三个三角形公用,此时有AB两个三角形是两个点都存在。换个例子

 (截自视频)这个途中的白色的边,显然是这个多边形的一个边界,左边的点只有A三角形使用,而右边的点由ABC三个三角形公用,此时只有A三角形是两个点都存在,此时得出的边界的定义:当边的两个点只公用一个三角形的时候,这条边就是多边形的边界。(我想网上应该有更多关于这个的解释,如果认为笔者讲的不好的可以上网搜搜)。

既然得出这个结论,那么我们就通过这个结论去实现我们需要的东西。

首先,我们先给之前我们的地图信息包个外框,修改MapGenerator脚本的GenerateMap函数:

    /// <summary>
    /// 生成地图
    /// </summary>
    void GenerateMap()
    {
        map = new int[width, height];
        RandomFillMap();

        for (int i = 0; i < 5; i++)
        {
            SmoothMap();
        }

        //创建出边界,这里的边界是地图的最外圈让其有宽度为borderSize的墙,这个值可以随便修改
        int borderSize = 1;
        int[,] borderedMap = new int[width + borderSize * 2, height + borderSize * 2];  //乘2的原因是边界有左右,上下

        for (int x = 0; x < borderedMap.GetLength(0); x++)
        {
            for (int y = 0; y < borderedMap.GetLength(1); y++)
            {
                //如果不是新添加的边界,那么就是原来的地图的信息,将原来的数组的数据映射到新的边界地图数据上,如果是边界就直接赋值1
                if (x >= borderSize && x < width + borderSize && y >= borderSize && y < height + borderSize)
                {
                    //减边界大小是为了让地图生成在中心
                    borderedMap[x, y] = map[x - borderSize, y - borderSize];
                }
                else
                {
                    borderedMap[x, y] = 1;
                }
            }
        }

        MeshGenerator meshGen = GetComponent<MeshGenerator>();
        //传进新的地图信息
        meshGen.GenerateMesh(borderedMap, 1);
    }

代码的解释,在注释中详细的说明了。

然后回到MeshGenerator脚本,为了能够对于顶点的三角形公用的关系,我们就需要为这些信息定义一个点与三角形的关系的结构体Triangle,其中只需要三个顶点的信息(在所有顶点列表中的索引)就可以了。然后为结构体写一个便于访问一些信息的函数,定义全局字典变量Dictionary<int, List<Triangle>> triangleDictionary,Key是顶点在所有顶点列表中的索引,Value是该顶点被公用的三角形的信息。这样我们就可以通过顶点的索引来获取三角形的关系,然后去对比两个顶点的三角形的公用数来判断是否是边界。所以在之前的CreateTriangle函数的基础上我们做出的修改就是讲传进来的三角面的三个点再拿去创建一个Triangle的结构(这里的Triangle出现的非常的频繁,希望大家不要混淆了,CreateTriangle在上一篇是服务于四方的信息,而这片文章中的Triangle结构体是本章新创建的结构体struct,服务于顶点和三角形的相对关系),并且将顶点和创建出来的三角形结构体存进triangleDictionary字典中。这里我们还需要两个变量:   

List<List<int>> outlines = new List<List<int>>();        HashSet<int> checkedVertices = new HashSet<int>(); 

outlines存的是顶点索引列表的列表(有点拗口),里面的List<int>存的点的索引是该边界的环上的所有的线所使用的到的点(比方说:ABCDEFGA这几个点形成了一个边界,那么就存有ABCDEFG这几个点,AB,BC...GA都是边界线)。

checkedVertices 存的是被检查过的顶点,如果一个顶点被判断过是边界上的点或者不是,都会被存进哈希表中,这样就不会重复的去判断一个点是不是边界上的点。

所以接下来我们就去遍历所的顶点,拿到没有被检查过的一个点A,获取到该点被公用的所有三角形的列表,然后拿到每个三角形的其他的几个点,这几个点和点A都能形成一条边,然后遍历这几个点拿到这几个点被公用的三角形,这样就可以知道相同三角形的数量,根据之前的结论,如果相同的数量是1那么就是边界。这样就能拿到形成边界的另外一个点,将这个点存进checkedVertices中,然后通过递归不断去获取下个与这个新的点形成边界的不存在checkedVertices中的点,直接找不到说明已经找到其中一个边界,直接遍历完所有的点,我们就能拿到所有的边界了。

拿到边界的点就容易了,新建一个子对象(“”Walls“”),挂上MeshRenderer和MeshFilter,脚本中创建新的Mesh,遍历outlines,拿到其中的一个边界的列表outline,两个两个点的去遍历outline中点,向下延伸出新的两个点,高度自定义,但是要注意的是,形成墙的mesh信息的triagles时要注意,因为墙是在内部生成的,所以triangles的添加顺序要变成是逆时针。

完整代码:

MapGenerator的修改上面已经修改了。

MeshGenerator:

public class MeshGenerator : MonoBehaviour {

    public SquareGrid squareGrid;   //四方网格
    public MeshFilter walls;        //墙壁的meshfilter组件,新建一个子对象挂在MeshRenderer和MeshFilter,拖拽给该变量赋值
    List<Vector3> vertces;          //地图信息的顶点的列表
    List<int> triangles;            //地图信息的所有三角面信息的列表

    Dictionary<int, List<Triangle>> triangleDictionary = new Dictionary<int, List<Triangle>>();     //包含顶点的三角形的列表和顶点的关系字典
    List<List<int>> outlines = new List<List<int>>();       //所有边界的列表,储存的是顶点的索引
    HashSet<int> checkedVertices = new HashSet<int>();      //已经检查过边界的顶点的哈希表,哈希表的特点可以去网上找到很多信息


    /// <summary>
    /// 创建生成网格
    /// </summary>
    /// <param name="map"></param>
    /// <param name="squareSize"></param>
    public void GenerateMesh(int[,] map, float squareSize)
    {
        triangleDictionary.Clear();
        outlines.Clear();
        checkedVertices.Clear();
        squareGrid = new SquareGrid(map, squareSize);

        vertces = new List<Vector3>();
        triangles = new List<int>();

        for (int x = 0; x < squareGrid.squares.GetLength(0); x++)
        {
            for (int y = 0; y < squareGrid.squares.GetLength(1); y++)
            {
                TriangulateSquare(squareGrid.squares[x, y]);
            }
        }

        Mesh mesh = new Mesh();
        GetComponent<MeshFilter>().mesh = mesh;

        mesh.vertices = vertces.ToArray();
        mesh.triangles = triangles.ToArray();
        mesh.RecalculateNormals();

        CreateWallMesh();
    }

    void CreateWallMesh()
    {
        //计算获得所有的边界顶点
        CalculateMeshOutlines();

        List<Vector3> wallVertices = new List<Vector3>();        //墙mesh的顶点列表
        List<int> wallTriangles = new List<int>();              //墙的mesh的三角列表
        Mesh mesh = new Mesh();
        float wallHeight = 5;                               //墙的高度

        foreach (List<int> outline in outlines)
        {
            for (int i = 0; i < outline.Count - 1; i++)
            {
                int startIndex = wallVertices.Count;
                wallVertices.Add(vertces[outline[i]]); //left
                wallVertices.Add(vertces[outline[i + 1]]); //right
                wallVertices.Add(vertces[outline[i]] - Vector3.up * wallHeight); //bottom left          ,墙向下延伸
                wallVertices.Add(vertces[outline[i + 1]] - Vector3.up * wallHeight); //bottom right      

                /*
                 * left     right
                 * 
                 * 
                 * 
                 * bottom   bottom
                 * left     right
                 * 
                 * left -> bottomLeft -> bottomRight ,  bottomRight -> right -> left
                 * 由于墙是要从内向外看,所以三角形的点的顺序要变成逆时针来存进去
                 */
                wallTriangles.Add(startIndex + 0);
                wallTriangles.Add(startIndex + 2);
                wallTriangles.Add(startIndex + 3);

                wallTriangles.Add(startIndex + 3);
                wallTriangles.Add(startIndex + 1);
                wallTriangles.Add(startIndex + 0);
            }
        }

        mesh.vertices = wallVertices.ToArray();
        mesh.triangles = wallTriangles.ToArray();
        walls.mesh = mesh;
    }

    /// <summary>
    /// 将方形的信息转换成三角信息
    /// </summary>
    /// <param name="square"></param>
    void TriangulateSquare(Square square)
    {
        switch (square.configuration)
        {
            case 0:
                break;

            // 只有一个点激活的情况
            case 1:
                MeshFromPoints(square.centreLeft, square.centreBottom, square.bottomLeft);
                break;
            case 2:
                MeshFromPoints(square.bottomRight, square.centreBottom, square.centreRight);
                break;
            case 4:
                MeshFromPoints(square.topRight, square.centreRight, square.centreTop);
                break;
            case 8:
                MeshFromPoints(square.topLeft, square.centreTop, square.centreLeft);
                break;

            // 两个点激活的情况
            case 3:
                MeshFromPoints(square.centreRight,square.bottomRight, square.bottomLeft, square.centreLeft);
                break;

            case 6:
                MeshFromPoints(square.centreTop, square.topRight, square.bottomRight, square.centreBottom);
                break;

            case 9:
                MeshFromPoints(square.topLeft, square.centreTop, square.centreBottom, square.bottomLeft);
                break;

            case 12:
                MeshFromPoints(square.topLeft, square.topRight, square.centreRight, square.centreLeft);
                break;

            case 5:
                MeshFromPoints(square.centreTop, square.topRight, square.centreRight, square.centreBottom, square.bottomLeft, square.centreLeft);
                break;

            case 10:
                MeshFromPoints(square.topLeft, square.centreTop, square.centreRight, square.bottomRight, square.centreBottom, square.centreLeft);
                break;

            // 三个点激活的情况
            case 7:
                MeshFromPoints(square.centreTop, square.topRight, square.bottomRight, square.bottomLeft, square.centreLeft);
                break;

            case 11:
                MeshFromPoints(square.topLeft, square.centreTop, square.centreRight, square.bottomRight, square.bottomLeft);
                break;

            case 13:
                MeshFromPoints(square.topLeft, square.topRight, square.centreRight, square.centreBottom, square.bottomLeft);
                break;

            case 14:
                MeshFromPoints(square.topLeft, square.topRight, square.bottomRight, square.centreBottom, square.centreLeft);
                break;
            // 四个点激活的情况

            case 15:
                MeshFromPoints(square.topLeft, square.topRight, square.bottomRight, square.bottomLeft);

                
                checkedVertices.Add(square.topLeft.vertexIndex);
                checkedVertices.Add(square.topRight.vertexIndex);
                checkedVertices.Add(square.bottomRight.vertexIndex);
                checkedVertices.Add(square.bottomLeft.vertexIndex);
                break;
        }
    }

    /// <summary>
    /// 通过顶点来生成Mesh的相关信息
    /// </summary>
    /// <param name="points"></param>
    void MeshFromPoints(params Node[] points)
    {
        AssignVertices(points);

        //这里我一开始也是看不明白为什么要这么写,然后发现这么写实在是太巧妙了。
        //从起始点出发,如果只有三个点就只需要画一个三角形,如果是四个点就会进入第二个判断,这两就能画出两个三角形,一次类推。
        //相当于三个点就就是012, 如果是四个点就是012,023, 五个点就是画012,023,034

        if (points.Length >= 3)
            CreateTriangle(points[0], points[1], points[2]);

        if (points.Length >= 4)
            CreateTriangle(points[0], points[2], points[3]);

        if (points.Length >= 5)
            CreateTriangle(points[0], points[3], points[4]);

        if (points.Length >= 6)
            CreateTriangle(points[0], points[4], points[5]);
    }

    /// <summary>
    /// 分配顶点
    /// </summary>
    /// <param name="points"></param>
    void AssignVertices(Node[] points)
    {
        for (int i = 0; i < points.Length; i++)
        {
            if (points[i].vertexIndex == -1)
            {
                //让index = count 就能够做到从0到最大值-1
                points[i].vertexIndex = vertces.Count;
                vertces.Add(points[i].position);
            }
        }
    }

    /// <summary>
    /// 根据三点创建三角面
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    /// <param name="c"></param>
    void CreateTriangle(Node a, Node b, Node c)
    {
        ///将顶点的关系存进三角信息列表中
        triangles.Add(a.vertexIndex);
        triangles.Add(b.vertexIndex);
        triangles.Add(c.vertexIndex);

        //创建顶点与三角形的关系的结构体
        Triangle triangle = new Triangle(a.vertexIndex, b.vertexIndex, c.vertexIndex);
        AddTrianleToDictionary(triangle.vertexIndexA, triangle);
        AddTrianleToDictionary(triangle.vertexIndexB, triangle);
        AddTrianleToDictionary(triangle.vertexIndexC, triangle);
    }

    /// <summary>
    /// 将三角信息存进字典中
    /// </summary>
    /// <param name="vertexIndex"></param>
    /// <param name="triangle"></param>
    void AddTrianleToDictionary(int vertexIndex, Triangle triangle)
    {
        if (triangleDictionary.ContainsKey(vertexIndex))
        {
            triangleDictionary[vertexIndex].Add(triangle);
        }
        else
        {
            triangleDictionary.Add(vertexIndex, new List<Triangle>() { triangle });
        }
    }

    /// <summary>
    /// 获取到相连接的能形成边界的一条边上的另一个店
    /// </summary>
    /// <param name="vertexIndex"></param>
    /// <returns></returns>
    int GetConnectedOutlineVertex(int vertexIndex)
    {
        List<Triangle> triangleContainingVertex = triangleDictionary[vertexIndex];

        for (int i = 0; i < triangleContainingVertex.Count; i++)
        {
            Triangle triangle = triangleContainingVertex[i];

            for (int j = 0; j < 3; j++)
            {
                int vertexB = triangle[j];

                if (vertexB != vertexIndex && !checkedVertices.Contains(vertexB))
                {
                    if (IsOutLineEdge(vertexIndex, vertexB))
                    {
                        return vertexB;
                    }
                }
            }
        }

        return -1;
    }

    /// <summary>
    /// 计算出所有的边界
    /// </summary>
    void CalculateMeshOutlines()
    {
        for (int vertexIndex = 0; vertexIndex < vertces.Count; vertexIndex++)
        {
            if (!checkedVertices.Contains(vertexIndex))
            {
                int newOutlineVertex = GetConnectedOutlineVertex(vertexIndex);
                if (newOutlineVertex != -1)
                {
                    //未检查过的点存在边界点
                    checkedVertices.Add(vertexIndex);

                    //创建新的边界列表然后通过递归将所有的边界点拿到
                    List<int> newOutline = new List<int>();
                    newOutline.Add(vertexIndex);
                    outlines.Add(newOutline);
                    FollowOutline(newOutlineVertex, outlines.Count - 1);
                    outlines[outlines.Count - 1].Add(vertexIndex);
                }
            }
        }
    }

    /// <summary>
    /// 递归获取到同一个边界上所有边界点的信息
    /// </summary>
    /// <param name="vertexIndex"></param>
    /// <param name="outlineIndex"></param>
    void FollowOutline(int vertexIndex, int outlineIndex)
    {
        outlines[outlineIndex].Add(vertexIndex);
        checkedVertices.Add(vertexIndex);
        int nextVertexIndex = GetConnectedOutlineVertex(vertexIndex);

        if (nextVertexIndex != -1)
        {
            FollowOutline(nextVertexIndex, outlineIndex);
        }
    }

    /// <summary>
    /// 通过一条边两个顶点判断该边是否是边界
    /// </summary>
    /// <param name="vertexA"></param>
    /// <param name="vertexB"></param>
    /// <returns></returns>
    bool IsOutLineEdge(int vertexA, int vertexB)
    {
        List<Triangle> triangleContianingVertexA = triangleDictionary[vertexA];
        int sharedTriangleCount = 0;
        for (int i = 0; i < triangleContianingVertexA.Count; i++)
        {
            if (triangleContianingVertexA[i].Contains(vertexB))
            {
                sharedTriangleCount++;
                if (sharedTriangleCount > 1)
                {
                    break;
                }
            }
        }

        return sharedTriangleCount == 1;
    }

    /// <summary>
    /// 三角信息结构体
    /// </summary>
    struct Triangle
    {
        public int vertexIndexA;
        public int vertexIndexB;
        public int vertexIndexC;

        int[] verteces;

        public Triangle(int a, int b, int c)
        {
            vertexIndexA = a;
            vertexIndexB = b;
            vertexIndexC = c;

            verteces = new int[] { vertexIndexA, vertexIndexB, vertexIndexC };
        }

        //让结构体能像索引一直通过index访问三角中的顶点
        public int this[int i]
        {
            get { return verteces[i]; }
        }

        public bool Contains(int vertexIndex)
        {
            return vertexIndex == vertexIndexA || vertexIndex == vertexIndexB || vertexIndex == vertexIndexC;
        }
    }

    /// <summary>
    /// 方形网格
    /// </summary>
    public class SquareGrid
    {
        public Square[,] squares;

        public SquareGrid(int[,] map, float squareSize)
        {
            //计算出控制点的x方向的数量和y方向的数量,其实就是二维数组的长宽
            int nodeCountX = map.GetLength(0);
            int nodeCountY = map.GetLength(1);

            //根据传进来的方形的边长计算出所有的点构成的面长和高(这里的长指的是x方向的长度,高指的是z方向的纵深)
            float mapWidth = nodeCountX * squareSize;
            float mapHeight = nodeCountY * squareSize;

            //创建控制点的二维数组
            ControlNode[,] controlNodes = new ControlNode[nodeCountX, nodeCountY];

            for (int x = 0; x < nodeCountX; x++)
            {
                for (int y = 0; y < nodeCountY; y++)
                {
                    //计算控制点的位置,并且做了偏移,让整体的中心在原点
                    Vector3 pos = new Vector3(-mapWidth * .5f + x * squareSize + squareSize * .5f, 0, -mapHeight * .5f  + y * squareSize + squareSize * .5f);
                    //如果是1说明是墙,就将active设置成true,不是则为false
                    controlNodes[x, y] = new ControlNode(pos, map[x, y] == 1, squareSize);
                }
            }

            //由于一个四边是有四个点构成,square的总量是nodeCountX - 1 * nodeCoungY - 1, 这里在纸上点出几个点再画四边形就能理解啦。
            squares = new Square[nodeCountX - 1, nodeCountY - 1];

            for (int x = 0; x < nodeCountX -1; x++)
            {
                for (int y = 0; y < nodeCountY - 1; y++)
                {
                    //初始化要按顺序传入topLeft, topRight, _bottomLeft, _bottomRight;
                    squares[x, y] = new Square(controlNodes[x, y + 1], controlNodes[x + 1, y + 1], controlNodes[x, y], controlNodes[x + 1, y]);
                }
            }
        }
    }

    /// <summary>
    /// 方形区域,记录包块四个角的控制点,和每条边的中心的点
    /// </summary>
    public class Square
    {
        public ControlNode topLeft, topRight, bottomRight, bottomLeft;
        public Node centreTop, centreRight, centreBottom, centreLeft;
        public int configuration;

        public Square(ControlNode _topLeft, ControlNode _topRight, ControlNode _bottomLeft, ControlNode _bottomRight)
        {
            topLeft = _topLeft;
            topRight = _topRight;
            bottomLeft = _bottomLeft;
            bottomRight = _bottomRight;

            centreTop = topLeft.right;
            centreLeft = bottomLeft.above;
            centreBottom = bottomLeft.right;
            centreRight = bottomRight.above;

            if (topLeft.active)
                configuration += 8;

            if (topRight.active)
                configuration += 4;

            if (bottomRight.active)
                configuration += 2;

            if (bottomLeft.active)
                configuration += 1;
        }
    }

    /// <summary>
    /// 点的基础类
    /// </summary>
    public class Node
    {
        public Vector3 position;
        public int vertexIndex = -1;

        public Node(Vector3 _position)
        {
            position = _position;
        }
    }

    /// <summary>
    /// 控制点(一个方形区域的四角)
    /// </summary>
    public class ControlNode : Node
    {
        public bool active;
        public Node above, right;

        public ControlNode(Vector3 _position, bool _active, float squareSize) : base(_position)
        {
            active = _active;
            above = new Node(position + Vector3.forward * squareSize * .5f);
            right = new Node(position + Vector3.right * squareSize * .5f);
        }
    }
}

代码的注释写的应该还是比较清楚的吧,希望有所帮助~。

最后再贴一张效果图

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值