三角剖分的种类不少, 根据不一样需求有各类各样的算法, 这里讲的是按顺序给出边缘顶点以后, 如何对这个顶点集合进行三角剖分.html
好比下面的图示:算法
图一dom
给出了边缘点并按顺序排列, 将它剖分红三角形, 虽然有多种方法, 不过咱们只须要得到正确的三角形就好了, 不用关心它剖成怎么样.ide
对于凸多边形(Convex Polygon), 只须要像图中同样, 找一个起始点, 而后按顺序取点跟起始点组成三角形就好了, 很是简单. 代码都懒得写了.测试
而对于凹多边形(Concave Polygon)就不能简单划分三角形了, 由于会超出原有多边形的范围 :spa
图二3d
HAB组成的三角形明显超出了边界, 不能简单剖分.code
PS: 不要把这个和 delaunay triangulation 搞混了, delaunay作的是离散点的三角剖分, 得到的是这些离散点组成的凸多边形.orm
我是最近才有这个功能要作, 因此找了一下相关的实现方法, 发现不少人的实现基于很早之前的某篇论文<<三维模型重建中的凹多边形三角剖分>> : 解放军测绘学院学报 1999年9月刊.htm
并且看了下基本都是抄来抄去, 代码很多, 错误很多, 基本都没有实现这个剖分逻辑的...... 论文也是颇有问题, 给出了几个定义, 几个性质, 而后就给出了算法, 至于算法的正确性证实也没有, 我就
直接相信它吧.
把论文的算法精简出来就有那么几步 ( 好比顶点列表为 List<Vector3> points, 排列顺序就是在List中的顺序 ) :
一. 寻找出一个凸顶点
1.1 定义 : 临时从列表中去掉某个顶点, 而后列表剩下的顶点构成新的的多边形, 这个被去掉的顶点不在新多边形内部, 就是凸顶点 ( 好比图二中的B点是凸顶点. 点D被CE组成的多边形包在里面, 因此不是凸顶点 )
1.2 功能 : 若是不是凸顶点, 把这个顶点插回列表原来位置. 找下一个顶点, 若是是凸顶点, 测试是否可划分顶点.
二. 肯定该凸顶点是可划分顶点
2.1 定义 : 凸顶点和在列表中相邻的两点组成的三角形, 若是不包含列表中的其它顶点, 就是可划分点 ( 好比图二中的ABC组成的三角形里面没有其它点, B点就是可划分点. HAB 中有D点, A点就不可划分 )
2.2 功能 : 若是不是可划分顶点, 把这个顶点插回列表原来位置. 找下一个顶点
三. 可划分就把顶点从列表中删除
3.1 功能 : 把以前该点组成的三角形记录到返回值列表中, 它就是一个剖分出来的三角形. 不插回去就等于删除了.
四. 一直重复直到只剩最后3个顶点, 最后3个顶点就是最后一个三角形.
4.1 功能 : 把最后三个顶点做为一个三角形加到返回值列表中, 剖分就完成了.
看似简单的逻辑, 拆分出来的功能也不简单, 先把各个大项中的算法逻辑整理一下:
一. 寻找出一个凸顶点
最重要的是测试一个点(P)是否在另外一个多边形内, 这里按照论文逻辑, 在P点处按某个方向引一条射线, 穿过这个多边形, 而后看射线与多边形的边的交点个数,
若是是奇数就说明点在多边形内部, 是偶数就说明在多边形外部. 看看下面一些情形:
PS : 射线与线段的交点计算方法在 : 功能实现的数学模型选择 -- (射线与线段交点的例子)
图三
图四
上图的状况在射线通过线段端点的时候(图四-1, 图四-2), 会有歧义, 由于无论怎样计算, 射线都是跟两条线段相交, 偶数相交点就判断在多边形外部的话是不对的.
还有在射线跟线段平行的状况(图四-3), 理论上来讲不该该有这样的状况, 由于在建模时三个点在一条直线上还不如去掉中间的点, 节省资源, 然而这种情形确实存在, 也会影响判断.
因此按照论文要给这些歧义的地方添加特殊判断 :
1. 在交点为线段端点的时候, 按照射线方向计算出线段位于射线的左边仍是右边, 只对在左边或右边的线段进行计数. 论文中没有给出证实来讲明这个方法的正确性, 若是有时间我会进行一下数学证实, 在附录补上.
计算端点与射线相交时, 线段在射线左右边的方法 : 只要求出相交端点到另外一个端点的向量, 而后跟射线方向叉乘一下, 左右判断就看大于0或者小于0来判断便可. 大于0在左边, 小于0在右边.
2. 在射线与某条线段重合的时候, 不进行计数, 也就是说该点的状况不受这条线段影响. 这个结论也没有通过证实, 不过根据个人直觉它是对的, 由于像上面说的建模时原本就应该去掉的顶点, 不参与计算也对.(2019.06.19)
图五 -- 反对上面的直觉, 射线跟线段重叠并不必定是多余的点!(2019.06.20) 若是有时间我会进行一下数学证实为什么不进行计数, 在附录补上.
PS : 这些理论在数学上即便可以证实是正确的, 但是用在计算机系统里面就不必定正确, 由于小数点的精度问题, 在这些计算中确定会出现错误, 好比射线跟线段端点相交, 若是有一点误差就是不相交或是相交在线段内了.
3. 怎样肯定点P的射线方向, 让它穿过多边形? 最简单的方法就在多边形上随便选一个点, 而后点P到该点的方向便可, 为了减小计算偏差, 我会选择随机某条边的中间点来做为射线方向.
图六 图七
对图2的多边形顶点进行凸顶点测试, A点与B点都在新多边形以外, 都是凸顶点.
二. 肯定该凸顶点是可划分顶点
1. 当一个顶点是凸顶点的时候, 还须要确认它是可划分顶点, 可划分顶点就是剖出的三角形不包含剩下的各个顶点的状况. 看下图:
图八
这是对图二的多边形进行的一次三角剖分, 这里A, B顶点都是是凸顶点, 用HAB组成了一个新三角形, 而后剩下的顶点是C,D,E,F,G显然D点在HAB里面, 因此如今A不是一个可划分点, 而后下一个点B, ABC没有包含其它顶点, B点就是可划分点.
核心算法就是怎样测试其他点是否在要剖分的三角形以内.
2. 怎样计算一个点是否在三角形以内, 这里使用的是一个相似几何的方法, 看下图:
图九
若是点P在一个三角形以内, 能够得出 : 角A, 角B, 角C 任意角都小于180度.
数学证实 : 角A + 角B + 角C = 360度, P与各个端点构成的三角形好比BPA, 最大的角度就是P点在AB的线段上, 那么角BPA最大就是180度, 最小就是角BCA, 因此 角BPA >= 角BCA && 角BPA <= 180度, 那么其他两角的和就大于等于180度.
计算逻辑 : 使用叉乘来计算 ( 用⊙ 表明叉乘符号 ) =>
cross1 = (向量PA ⊙ 向量PB)
cross2 = (向量PB ⊙ 向量PC)
cross3 = (向量PC ⊙ 向量PA)
这里用的是X-Z平面做为二维平面, 那么叉积获得的就是y轴的值, 用Vector3向量的话很容易计算, 那么若是点在三角形内, 能够得出它们的叉积在y轴的值必定同时都是负的或者同时都是正的. 只有点在三角形以外的时候才会有超过180度的角,
才会有同时有正负的状况出现. 因此断定就能够很简单: (cross1.y * cross2.y) >= 0 && (cross2.y * cross3.y) >= 0; 判断他们正负号都相同就好了.
三. 可划分就把顶点从列表中删除
当可划分顶点找到后, 就能够把该顶点从原有列表中删除了. 按照图七的划分, 记录下剖分三角形ABC, 顶点列表删除B点, 剩下的列表若是大于三个顶点, 继续重复这个过程便可. 直到最后三个点, 把三个点也加入返回列表就完成了.
理论过程就到这里, 下来就是代码实现了.
说明一下我用的是Unity3D, 原始顶点列表做为输入, 返回值输出的是剖分后的三角形顶点, 而不是组成三角形的Index, 由于Mesh须要normal的数量跟顶点数量一致, 若是Mesh使用原始顶点列表做为顶点的话(好比正方体只有8个顶点的话),
它自动计算出来的normal也是只有8个, 也就是说它把各个共享顶点的向量进行了插值, 光照会出错的.
public static class Triangulation { #region Main Funcs /// <summary> /// 泛用的三角剖分 /// </summary> /// <param name="vertices"> 按照顺序排列的边界点 </param> /// <returns></returns> public static List<Vector3> GenericTriangulate(List<Vector3> vertices) { if(vertices.Count < 3) { throw new System.Exception("No Enough Points!!!"); } List<Vector3> tempPoints = new List<Vector3>(vertices); List<Vector3> points = new List<Vector3>(); int convexIndex = -1; while(tempPoints.Count > 3 && ((convexIndex = SearchConvexIndex(tempPoints)) != -1)) { var p0 = convexIndex == 0 ? tempPoints[tempPoints.Count - 1] : tempPoints[convexIndex - 1]; var p1 = tempPoints[convexIndex]; var p2 = (convexIndex == tempPoints.Count - 1) ? tempPoints[0] : tempPoints[convexIndex + 1]; points.Add(p0); points.Add(p1); points.Add(p2); tempPoints.RemoveAt(convexIndex); Debug.Log("被剔除顶点 : " + convexIndex); } points.AddRange(tempPoints); return points; } #endregion #region Help Funcs /// <summary> /// Search a Convex point from vertices /// </summary> /// <param name="vertices"></param> /// <returns></returns> public static int SearchConvexIndex(List<Vector3> vertices) { if(vertices.Count > 3) { for(int i = 0; i < vertices.Count; i++) { if(IsPointInsideGeometry(ref vertices, i) == false) { int index0 = i == 0 ? vertices.Count - 1 : i - 1; int index1 = i; int index2 = i == vertices.Count - 1 ? 0 : i + 1; if(IsAnyPointInsideGeometryTriangle(vertices, index0, index1, index2)) { Debug.LogWarning("有其它顶点在剔除三角形内, 没法剔除:" + i); continue; } return i; } } } return -1; } /// <summary> /// 检查是否相等, 解决计算机计算偏差 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> public static bool IsTheSameValue(float a, float b) { const double Epsilon = 1e-5; var val = System.Math.Abs(a - b); return val <= Epsilon; } /// <summary> /// 射线与线段相交性判断 /// </summary> /// <param name="ray"></param> /// <param name="p1"></param> /// <param name="p2"></param> /// <param name="point"></param> /// <param name="isParallel"></param> /// <returns></returns> public static bool RayAndLineIntersection(Ray ray, Vector3 p1, Vector3 p2, out Vector3 point, out bool isParallel) { point = Vector3.zero; isParallel = false; Vector3 p3 = new Vector3(ray.origin.x, 0, ray.origin.z); Vector3 p4 = ray.GetPoint(1.0f); p4.y = 0; float a1, b1, c1; float[] variables1; GeometryMath.MultivariateLinearEquation(new float[2] { p1.x, p1.z }, new float[2] { p2.x, p2.z }, out variables1); a1 = variables1[0]; b1 = variables1[1]; c1 = variables1[2]; float a2, b2, c2; float[] variables2; GeometryMath.MultivariateLinearEquation(new float[2] { p3.x, p3.z }, new float[2] { p4.x, p4.z }, out variables2); a2 = variables2[0]; b2 = variables2[1]; c2 = variables2[2]; DeterminantEquation determinantEquation = new DeterminantEquation(2); determinantEquation.determinant.SetRow(a1, b1); determinantEquation.determinant.SetRow(a2, b2); var variables = determinantEquation.CaculateVariables(-c1, -c2); if(variables == null) { // no result -- check is Parallel line with same float ea, eb; ea = a1 * c2 - a2 * c1; eb = b1 * c2 - b2 * c1; if((ea == 0) && (eb == 0)) { point = p1; isParallel = true; Debug.Log("Determinant No Result, it is Parallel Line."); return true; } return false; } point = new Vector3(variables[0], 0, variables[1]); var rayDir = ray.direction; rayDir.y = 0; bool intersect = Vector3.Dot(point - p3, rayDir) > 0; if(intersect) { bool xClamp = IsTheSameValue(point.x, p1.x) || IsTheSameValue(point.x, p2.x) || (point.x >= Mathf.Min(p1.x, p2.x) && point.x <= Mathf.Max(p1.x, p2.x)); bool zClamp = IsTheSameValue(point.z, p1.z) || IsTheSameValue(point.z, p2.z) || (point.z >= Mathf.Min(p1.z, p2.z) && point.z <= Mathf.Max(p1.z, p2.z)); intersect = xClamp && zClamp; } return intersect; } /// <summary> /// 判断点是否在多边形内 /// </summary> /// <param name="points"></param> /// <param name="checkPointIndex"></param> /// <returns></returns> private static bool IsPointInsideGeometry(ref List<Vector3> points, int checkPointIndex) { var p = points[checkPointIndex]; var point = new Vector3(p.x, 0, p.z); points.RemoveAt(checkPointIndex); int interCount = 0; int randIndex = UnityEngine.Random.Range(0, points.Count - 1); var randPoint = (points[randIndex] + points[randIndex + 1]) * 0.5f; randPoint.y = 0.0f; Vector3 randDir = (randPoint - point).normalized; Ray ray = new Ray(point, randDir); // random direction for(int i = 0; i < points.Count; i++) { var p1 = points[i]; var p2 = (i + 1) >= points.Count ? points[0] : points[i + 1]; Vector3 intersectPoint; bool isParallel; if(RayAndLineIntersection(ray, p1, p2, out intersectPoint, out isParallel)) { if(isParallel == false) { if(p1 == intersectPoint || p2 == intersectPoint) { var dir = (p1 == intersectPoint) ? (p2 - p1) : (p1 - p2); var cross = Vector3.Cross(dir, randDir); if(cross.y < 0) { continue; } } interCount++; } } } points.Insert(checkPointIndex, p); bool inside = ((interCount % 2) == 1); if(inside == false) { Debug.Log("Index 不在多边形内, 尝试剔除此顶点 : " + checkPointIndex + " 测试顶点:" + randPoint); } return inside; } /// <summary> /// 判断点是否在三角形内 -- 快速 /// </summary> /// <param name="points"></param> /// <param name="triangleIndex0"></param> /// <param name="triangleIndex1"></param> /// <param name="triangleIndex2"></param> /// <returns></returns> private static bool IsAnyPointInsideGeometryTriangle(List<Vector3> points, int triangleIndex0, int triangleIndex1, int triangleIndex2) { var p0 = points[triangleIndex0]; var p1 = points[triangleIndex1]; var p2 = points[triangleIndex2]; for(int i = 0; i < points.Count; i++) { if(i != triangleIndex0 && i != triangleIndex1 && i != triangleIndex2) { var point = points[i]; if(IsPointInsideTriangle(p0, p1, p2, point)) { return true; } } } return false; } /// <summary> /// a fast way to check a point in triangle /// </summary> /// <param name="triangleP0"></param> /// <param name="triangleP1"></param> /// <param name="triangleP2"></param> /// <param name="point"></param> /// <returns></returns> public static bool IsPointInsideTriangle(Vector3 triangleP0, Vector3 triangleP1, Vector3 triangleP2, Vector3 point) { point.y = 0.0f; triangleP0.y = 0.0f; triangleP1.y = 0.0f; triangleP2.y = 0.0f; var dir1 = triangleP0 - point; var dir2 = triangleP1 - point; var dir3 = triangleP2 - point; var cross1 = Vector3.Cross(dir1, dir2); var cross2 = Vector3.Cross(dir2, dir3); var cross3 = Vector3.Cross(dir3, dir1); bool inside = (cross1.y * cross2.y) >= 0 && (cross2.y * cross3.y) >= 0; return inside; } #endregion }
最后直接测试一下生成一个Mesh
List<Vector3> points = new List<Vector3>(); points.Add(new Vector3(0.5f, 0, 0.5f)); points.Add(new Vector3(0.5f, 0, 1.5f)); points.Add(new Vector3(1.5f, 0, 1.5f)); points.Add(new Vector3(1.5f, 0, -1f)); points.Add(new Vector3(-1.5f, 0, -1f)); points.Add(new Vector3(-1.5f, 0, 1.5f)); points.Add(new Vector3(-0.5f, 0, 1.5f)); points.Add(new Vector3(-0.5f, 0, 0.5f));