接着上一遍的文章接着讲程序洞穴生成。
后来我去Unity的官网看到了一篇也是讲程序洞穴生成的,官网教程的链接https://unity3d.com/cn/learn/tutorials/s/procedural-cave-generation-tutorial,我想视频作者应该是参考了官网的文章然后制作了视频,本次的内容是视频的2,3两课。
进入正题,之前我们用方块点的形式生成了随机的洞穴,这次我们将地图的信息转换成网格的形式生成到Scene视图中去。新建一个脚本MeshGenetaror,挂载在MapGenerator同一个对象下,并且为对象添加MehsFilter和MeshRenderer组件,并且创建一个材质赋值给Renderer。
那么首先,我们上一篇中的Map的二维数组一怎么样的规则转换成Mesh所需要的顶点信息和三角信息呢。这里先定义四个类,用于转换和储存Map的信息。先贴代码,然后根据代码一点一点的讲。
/// <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);
}
}
先从Node类开始讲,Node类就是一个点的最基础的类,记录了点的位置和点作为mesh顶点的索引,初始值设置成-1,可以通过判断是否为-1来判断当前点是否一定被赋值过。(本人不会Ps所以很多图都是用画图工具画的,不好看请谅解)然后解释ControlNode,ControlNode也是继承自Node,因为它也需要最基本的点的信息,active用来记录这个点的激活状态,above和right两个点用来记录控制点上方和右边的点,每个控制点都伴随着两个普通点。
如上图所示,一个控制点伴随着一个位于控制点上方的above(above = 控制点的position + Vector3.forward * 方形的边长 / 2)和位于控制点右边的right(right = 控制点的position.right * 方形的边上 / 2)。每个控制点都是相同的规则,空间拓展一下,其他三个控制点也是如此。
然后就是Square类,类中定义了,四个ControlNode,四个普通的Node,他们的关系如下
SuqareGrid的作用就是将所有的控制点组成一个个Square,具体过程代码的注释写的还是比较的清楚的。
此时运行出来的结果是这样的:
放大的细节,黑色(active = false)和白色(active = true)就是ControlNode,灰色是Node
既然将所有的信息都转换到了Square上,那么就要将Square上点的active状态来绘制mesh,如果一个Square的active = true,那么就认为是二进制中的1,如果active = false,就认为是二进制中的0,我们自己定义topLeft是二进制的第四位,topRight是第三位,bottomRight是第二位,bottomLeft是第一位,这样四个点组合起来就会有16种情况,我们来举其中一情况来讲,其他的情况可以自己延伸的。
(该图截自视频)
此时topLeft和bottomRight的active = false,topRight和bottomLeft的active = true,二进制转换成十进制就是5,那么就要绘制中见灰色的mesh,unity中绘制mesh是要绘制三角形,并且是需要用到的是左手定则,也就是需要顺时针的画三角形,所以要画出三角形ABC,ACD,ADE,AEF,所以我们就要按当前ABCDEF的顺序记录。其实只要是顺时针的记录就行,这六个点无论哪个是起始点都无所谓。所以我们用二级制的方式来记录控制点的激活状况
if (topLeft.active)
configuration += 8;
if (topRight.active)
configuration += 4;
if (bottomRight.active)
configuration += 2;
if (bottomLeft.active)
configuration += 1;
根据configuration的值,就知道当前的点的激活状况,然后根据configuration来计算顶点和三角信息。
下面放出所有的代码:
public class MeshGenerator : MonoBehaviour {
public SquareGrid squareGrid;
List<Vector3> vertces;
List<int> triangles;
/// <summary>
/// 创建生成网格
/// </summary>
/// <param name="map"></param>
/// <param name="squareSize"></param>
public void GenerateMesh(int[,] map, float squareSize)
{
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();
}
/// <summary>
/// 将方形的信息转换成三角信息
/// </summary>
/// <param name="square"></param>
void TriangulateSquare(Square square)
{
switch (square.configuration)
{
case 0:
break;
// 只有一个点激活的情况
case 1:
MeshFromPoints(square.centreBottom, square.bottomLeft, square.centreLeft);
break;
case 2:
MeshFromPoints(square.centreRight, square.bottomRight, square.centreBottom);
break;
case 4:
MeshFromPoints(square.centreTop, square.topRight, square.centreRight);
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);
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);
}
/// <summary>
/// 用于观察生成的方形网格信息的绘制函数
/// </summary>
private void OnDrawGizmos()
{
//if (squareGrid != null)
//{
// for (int x = 0; x < squareGrid.squares.GetLength(0); x++)
// {
// for (int y = 0; y < squareGrid.squares.GetLength(1); y++)
// {
// Gizmos.color = (squareGrid.squares[x, y].topLeft.active) ? Color.black : Color.white;
// Gizmos.DrawCube(squareGrid.squares[x, y].topLeft.position, Vector3.one * .4f);
// Gizmos.color = (squareGrid.squares[x, y].topRight.active) ? Color.black : Color.white;
// Gizmos.DrawCube(squareGrid.squares[x, y].topRight.position, Vector3.one * .4f);
// Gizmos.color = (squareGrid.squares[x, y].bottomRight.active) ? Color.black : Color.white;
// Gizmos.DrawCube(squareGrid.squares[x, y].bottomRight.position, Vector3.one * .4f);
// Gizmos.color = (squareGrid.squares[x, y].bottomLeft.active) ? Color.black : Color.white;
// Gizmos.DrawCube(squareGrid.squares[x, y].bottomLeft.position, Vector3.one * .4f);
// Gizmos.color = Color.grey;
// Gizmos.DrawCube(squareGrid.squares[x, y].centreTop.position, Vector3.one * .15f);
// Gizmos.DrawCube(squareGrid.squares[x, y].centreRight.position, Vector3.one * .15f);
// Gizmos.DrawCube(squareGrid.squares[x, y].centreBottom.position, Vector3.one * .15f);
// Gizmos.DrawCube(squareGrid.squares[x, y].centreLeft.position, Vector3.one * .15f);
// }
// }
//}
}
/// <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);
}
}
}
只需要在之前的MapGenerator的GenerateMap函数下面去获取到MeshGenerator脚本然后调用GenerateMesh函数。下面再放效果图:
这次是两个视频的内容,其实将的内容还是比较多的,希望我写的注释和文章能对大家有帮助。