二维区域中Delaunay三角剖分:
1.将一个有限的二维平面空间划分为若干个三角形单元,每个三角形相邻且互不重叠。
2.每个三角形单元的顶点只能是相邻单元的顶点,每个三角形的外接圆内不包含其他三角形单元的顶点。
先上效果图(图为Unity中用Gizmos将每个三角形单元顶点连线画出来的预览图):
目录
一、要求
输入:
/// <summary>
/// 二维空间区域中的点集
/// </summary>
public List<Vector3> _vertices = new List<Vector3>();
输出:
/// <summary>
/// 三角形列表
/// </summary>
List<Triangle> _triangle = new List<Triangle>();
二、所需要自定义的类:
1.Edge类:存放三角形每条边的数据
Edge类的结构很简单,只需要两个Vector3来定义一条线段。
public class Edge
{
public Vector3 P1;
public Vector3 P2;
public Edge(Vector3 P1, Vector3 P2)
{
this.P1 = P1;
this.P2 = P2;
}
}
2.Triangle类:存放每个三角形单元的数据
Triangle类的结构的参数较多:
首先是三个顶点(P1,P2,P3)和三条边(E,E2,E3),外接圆的圆心Center和半径R,和一个bool值isSame(用来判断该三角形是否多余)。
public class Triangle
{
public Vector3 P1;
public Vector3 P2;
public Vector3 P3;
public Edge E1;
public Edge E2;
public Edge E3;
public Vector3 Center;
public double R;
public bool isSame;
}
其次是一些函数:
(1).计算该三角形的外接圆:
/// <summary>
/// 计算三角形的外接圆
/// </summary>
public void GetTriangleExcenterRadius()
{
//same point
if (P1 == P2 && P1 == P3)
{
R = 0;
Center = P1;
return;
}
float x1 = P1.x, x2 = P2.x, x3 = P3.x, y1 = P1.z, y2 = P2.z, y3 = P3.z;
float C1 = (float)(Math.Pow(x1, 2) + Math.Pow(y1, 2) - Math.Pow(x2, 2) - Math.Pow(y2, 2));
float C2 = (float)(Math.Pow(x2, 2) + Math.Pow(y2, 2) - Math.Pow(x3, 2) - Math.Pow(y3, 2));
float centery = (C1 * (x2 - x3) - C2 * (x1 - x2)) / (2 * (y1 - y2) * (x2 - x3) - 2 * (y2 - y3) * (x1 - x2));
float centerx = (C1 - 2 * centery * (y1 - y2)) / (2 * (x1 - x2));
Center = new Vector3(centerx, 0, centery);
R = GetDistance(P1, Center);
}
(2).判断该三角形与点之间的空间关系:
public bool isComtain(Vector3 node)
{
GetTriangleExcenterRadius();
if ((node - Center).sqrMagnitude <= R*R)
return true;
else
return false;
}
(3).判断是否存在包含超级三角形顶点的三角形:
public void isSame(Triangle s)
{
Vector3[] super = new Vector3[3];
super [0] = s.P1;
super [1] = s.P2;
super [2] = s.P3;
if (super .Contains(P1) || super .Contains(P2) || super .Contains(P3))
isBad = true;
}
三、Delaunay 二维三角剖分过程
1.变量
/// <summary>
/// 输入点集
/// </summary>
public List<Vector3> _vertices = new List<Vector3>();
/// <summary>
/// 超级三角形
/// </summary>
Triangle SuperTriangle;
/// <summary>
/// 边列表
/// </summary>
List<Edge> _edges = new List<Edge>();
/// <summary>
/// 输出三角形列表
/// </summary>
List<Triangle> _triangle = new List<Triangle>();
2.剖分过程:
(1)第一步:对输入的点集进行排序(由于是二维,y均为0,只根据x,z进行排序):
_vertices = _vertices.OrderBy(o => o.x).ThenBy(o => o.z).ToList();
排序后的点集按照x值、z值从小到大排序(x权重大于z,这个权重调换也可以不影响),
(2)第二步:根据排序好的点集构建超级三角形(即包含点集中所有节点的三角形):
/// <summary>
/// 找到超级三角形
/// </summary>
/// <returns></returns>
public Triangle FindSuper_Triangle(List<Vector3> _vertices)
{
Triangle super_Triangle=new Triangle(new Vector3(0,0,0),new Edge(new Vector3(1, 0, 1), new Vector3(-1, 0, 1)));
Vector3 Circle;
float Radius;
float xmin = _vertices[0].x;
float xmax = _vertices[_vertices.Count - 1].x;
float zmin = _vertices[0].z;
float zmax = _vertices[_vertices.Count - 1].z;
foreach (var dot in _vertices)
{
if (zmin > dot.z)
zmin = dot.z;
if (zmax <= dot.z)
zmax = dot.z;
//zmin = Math.Min(zmin, dot.z);
//zmax = Math.Max(zmax, dot.z);
}
Vector3 P1 = new Vector3(xmin, 0, zmin);
Vector3 P2 = new Vector3(xmin, 0, zmax);
Vector3 P3 = new Vector3(xmax, 0, zmax);
Vector3 P4 = new Vector3(xmax, 0, zmin);
Circle = (P1 + P3) / 2;
Radius = Mathf.Sqrt((xmax - xmin) * (xmax - xmin) + (zmax - zmin) * (zmax - zmin));
Vector3 sP1 = new Vector3(Circle.x, 0, Circle.y + Radius * 2);
Vector3 sP2 = new Vector3(Circle.x - Radius * 2, 0, Circle.y - Radius);
Vector3 sP3 = new Vector3(Circle.x + Radius * 2, 0, Circle.y - Radius);
super_Triangle = new Triangle(sP1, new Edge(sP2, sP3));
return super_Triangle;
}
这里用到的方法比较笨,构造出的超级三角形非常大,但准确率能有一定的保证。
(3)第三步:终于要开始剖分啦!
剖分的过程就是从超级三角形开始,根据三角形内的点对三角形进行解构(这部分原理在网上已经有许多详细的图文讲解,这里就不多赘述了,上代码!)
先将超级三角形加入三角形列表,其三个顶点加入点集:
_vertices.Add(SuperTriangle.P1);
_vertices.Add(SuperTriangle.P2);
_vertices.Add(SuperTriangle.P3);
_triangle.Add(SuperTriangle);
开始遍历点集,对于每一个节点:
判断三角形列表中的每个三角形是否包含该点,若包含,将该三角形的三条边加入边列表(AddEdge函数),然后从三角形列表中移除该三角形:
int index = 0;
while (index<_triangle.Count)
{
if (_triangle[index].isComtain(_vertices[i]))
{
AddEdge(_edges, _triangle[index].E1);
AddEdge(_edges, _triangle[index].E2);
AddEdge(_edges, _triangle[index].E3);
_triangle.Remove(_triangle[index]);
}
else
{
index++;
}
}
(关于AddEdge函数:加入列表中不存在的边,特别要注意的一点就是若是要加入的边已经存在时,需将已存在的这条边移除,并在移除之后也不能加入该边:
/// <summary>
/// 添加边
/// </summary>
/// <param name="_edges"></param>
/// <param name="E"></param>
public void AddEdge(List<Edge> _edges,Edge E)
{
bool isAdd = true;
int index = 0;
while (index<_edges.Count)
{
if (_edges[index].P1 == E.P1 && _edges[index].P2 == E.P2 || _edges[index].P1 == E.P2 && _edges[index].P2 == E.P1)
{
_edges.Remove(_edges[index]);
isAdd = false;
}
else
{
index++;
}
}
if(isAdd)
{
_edges.Add(E);
}
}
接着遍历边列表,对于边列表中的每条边,都与该点构成新的三角形单元存放入三角形列表中:
foreach (var e in _edges)
{
Triangle Ttemp = new Triangle(_vertices[i], e);
_triangle.Add(Ttemp);
}
按以上过程遍历完点集之后,开始处理三角形列表和点集——记得我们一开始把超级三角形的三个顶点加入这两个列表里了吧,把它移除!
移除三角形列表中包含超级三角形顶点的三角形:
int Tindex = 0;
while (Tindex < _triangle.Count)
{
_triangle[Tindex].Bad(SuperTriangle);
if (_triangle[Tindex].isBad)
_triangle.Remove(_triangle[Tindex]);
else
Tindex++;
}
最后,移除点集中的三个超级三角形顶点:
_vertices.Remove(SuperTriangle.P1);
_vertices.Remove(SuperTriangle.P2);
_vertices.Remove(SuperTriangle.P3);
大功告成!遍历输出的三角形列表,就可以根据每个三角形的顶点将其画出来啦: