耳切法简述
将简单多边形分解成三角形称为多边形的三角剖分
。对n个顶点的简单多边形的任何三角剖分都有n-2个三角形。其中最简单的算法,称为耳切法(EarClipping)
。
耳的定义
多边形的一个 “耳” 是由 V i 0 V_{i_{0}} Vi0、 V i 1 V_{i_{1}} Vi1和 V i 2 V_{i_{2}} Vi2三个连续顶点形成的三角形,其中 V i 1 V_{i_{1}} Vi1是一个凸顶点,该顶点的内角
小于π弧度,从 V i 0 V_{i_{0}} Vi0到 V i 2 V_{i_{2}} Vi2的线段完全位于多边形内部,该三角形内部除自身顶点外无其他多边形顶点。线段< V i 0 V_{i_{0}} Vi0, V i 2 V_{i_{2}} Vi2>称为多边形的对角线。顶点 V i 1 V_{i_{1}} Vi1称为耳尖。
算法步骤
第一步是将多边形存储为双向链表,以便可以快速删除耳尖。构建此列表是一个 O ( n ) O(n) O(n)过程。
第二步是迭代顶点并找到耳朵。对于每个顶点和相应的三角形,测试所有其他顶点,看看是否有任何顶点在三角形内部;如果没有一个在里面,就有耳朵。如果至少有一个在里面,就没有耳朵。实际上,在三角形测试中只考虑反射顶点就足够了。反射顶点
是由共享它的两条边形成的内角
大于 π 弧度的顶点。
多边形的数据结构使用数组同时维护四个双链表进行存储,而不是在标准列表数据结构中动态分配和释放内存。
多边形的顶点存储在循环列表 L L L中,凸顶点存储在线性列表 C C C中,反射顶点存储在线性列表 R R R中,耳尖存储在循环列表 E E E中。
上图的简单多边形,其顶点位置为:
V 0 = ( 3 , 48 ) , V 1 = ( 52 , 8 ) , V 2 = ( 99 , 50 ) , V 3 = ( 138 , 25 ) , V 4 = ( 175 , 77 ) , V 5 = ( 131 , 72 ) , V 6 = ( 111 , 113 ) , V 7 = ( 72 , 43 ) , V 8 = ( 26 , 55 ) , V 9 = ( 29 , 100 ) V_{0}=(3,48), V_{1}=(52,8), V_{2}=(99,50), V_{3}=(138,25), V_{4}=(175,77), V_{5}=(131,72), V_{6}=(111,113), V_{7}=(72,43), V_{8}=(26,55), V_{9}=(29,100) V0=(3,48),V1=(52,8),V2=(99,50),V3=(138,25),V4=(175,77),V5=(131,72),V6=(111,113),V7=(72,43),V8=(26,55),V9=(29,100)。
-
凸顶点列表 C = 0 , 1 , 3 , 4 , 6 , 9 C=0,1,3,4,6,9 C=0,1,3,4,6,9;
反射顶点的初始列表 R = 2 , 5 , 7 , 8 R=2,5,7,8 R=2,5,7,8;
耳朵的列表 E = 3 , 4 , 6 , 9 E=3,4,6,9 E=3,4,6,9,顶点 3 3 3 的耳朵去掉,所以三角剖分中的第1个三角形 T 2 , 3 , 4 T_{2,3,4} T2,3,4
注意:顶点 8 8 8在三角形 T 9 , 0 , 1 T_{9,0,1} T9,0,1中,顶点 7 7 7在三角形 T 0 , 1 , 2 T_{0,1,2} T0,1,2 中。
-
凸顶点的列表 C = 0 , 1 , 4 , 6 , 9 C=0,1,4,6,9 C=0,1,4,6,9;
反射顶点的列表 R = 2 , 5 , 7 , 8 R=2,5,7,8 R=2,5,7,8;
耳朵的列表 E = 4 , 6 , 9 E=4,6,9 E=4,6,9,顶点 4 4 4 的耳朵去掉,所以三角剖分中的第2个三角形 T 2 , 4 , 5 T_{2,4,5} T2,4,5
-
凸顶点的列表 C = 0 , 1 , 5 , 6 , 9 C=0,1,5,6,9 C=0,1,5,6,9;
反射顶点的列表 R = 2 , 7 , 8 R=2,7,8 R=2,7,8;
耳朵的列表 E = 5 , 6 , 9 E=5,6,9 E=5,6,9,顶点 5 5 5 的耳朵去掉,所以三角剖分中的第3个三角形 T 2 , 5 , 6 T_{2,5,6} T2,5,6
-
凸顶点的初始列表 C = 0 , 1 , 2 , 6 , 9 C=0,1,2,6,9 C=0,1,2,6,9;
反射顶点的初始列表 R = 7 , 8 R=7,8 R=7,8;
耳朵的列表 E = 6 , 9 E=6,9 E=6,9,顶点 6 6 6 的耳朵去掉,所以三角剖分中的第4个三角形 T 2 , 6 , 7 T_{2,6,7} T2,6,7
-
凸顶点的列表 C = 0 , 1 , 2 , 9 C=0,1,2,9 C=0,1,2,9;
反射顶点的列表 R = 7 , 8 R=7,8 R=7,8;
耳朵的列表 E = 9 , 2 E=9,2 E=9,2,顶点 9 9 9 的耳朵去掉,所以三角剖分中的第5个三角形 T 0 , 8 , 9 T_{0,8,9} T0,8,9
-
凸顶点的列表 C = 0 , 1 , 2 , 8 C=0,1,2,8 C=0,1,2,8;
反射顶点的列表 R = 7 R=7 R=7;
耳朵的列表 E = 0 , 2 , 8 E=0,2,8 E=0,2,8,顶点 0 0 0 的耳朵去掉,所以三角剖分中的第6个三角形 T 0 , 1 , 8 T_{0,1,8} T0,1,8
-
凸顶点的列表 C = 1 , 2 , 8 C=1,2,8 C=1,2,8;
反射顶点的列表 R = 7 R=7 R=7;
耳朵的列表 E = 2 , 8 E=2,8 E=2,8,顶点 2 2 2 的耳朵去掉,所以三角剖分中的第7个三角形 T 1 , 2 , 7 T_{1,2,7} T1,2,7,同时最后一个三角形 T 1 , 7 , 8 T_{1,7,8} T1,7,8
8. 剖分后的三角列表
T 2 , 3 , 4 T_{2,3,4} T2,3,4, T 2 , 4 , 5 T_{2,4,5} T2,4,5, T 2 , 5 , 6 T_{2,5,6} T2,5,6, T 2 , 6 , 7 T_{2,6,7} T2,6,7, T 0 , 8 , 9 T_{0,8,9} T0,8,9, T 0 , 1 , 8 T_{0,1,8} T0,1,8, T 1 , 2 , 7 T_{1,2,7} T1,2,7, T 1 , 7 , 8 T_{1,7,8} T1,7,8
代码示例
虚幻引擎的实现源码:UE\Engine\Source\Runtime\GeometryCore\Private\CompGeom\PolygonTriangulation.cpp
//
// Triangulate using ear clipping
// This is based on the 3D triangulation code from MeshDescription.cpp, simplified for 2D polygons
//
template<typename T>
void PolygonTriangulation::TriangulateSimplePolygon(const TArray<TVector2<T>>& VertexPositions, TArray<FIndex3i>& OutTriangles, bool bOrientAsHoleFill)
{struct Local{static inline bool IsTriangleFlipped(T OrientationSign, const TVector2<T>& VertexPositionA, const TVector2<T>& VertexPositionB, const TVector2<T>& VertexPositionC){T TriSignedArea = TTriangle2<T>::SignedArea(VertexPositionA, VertexPositionB, VertexPositionC);return TriSignedArea * OrientationSign < 0;}};OutTriangles.Reset();// Polygon must have at least three vertices/edges, or result is emptyint32 PolygonVertexCount = VertexPositions.Num();if (PolygonVertexCount < 3){return;}// compute signed area of polygondouble PolySignedArea2 = 0;for (int i = 0; i < PolygonVertexCount; ++i){const TVector2<T>& v1 = VertexPositions[i];const TVector2<T>& v2 = VertexPositions[(i + 1) % PolygonVertexCount];PolySignedArea2 += v1.X*v2.Y - v1.Y*v2.X;}bool bIsClockwise = PolySignedArea2 < 0;T OrientationSign = (bIsClockwise) ? -T(1) : T(1);// If perimeter has 3 vertices, just copy content of perimeter out if (PolygonVertexCount == 3){OutTriangles.Add(bOrientAsHoleFill ? FIndex3i(0, 2, 1) : FIndex3i(0, 1, 2));return;}// Make a simple linked list array of the previous and next vertex numbers, for each vertex number// in the polygon. This will just save us having to iterate later on.TArray<int32> PrevVertexNumbers, NextVertexNumbers;PrevVertexNumbers.SetNumUninitialized(PolygonVertexCount, false);NextVertexNumbers.SetNumUninitialized(PolygonVertexCount, false);for (int32 VertexNumber = 0; VertexNumber < PolygonVertexCount; ++VertexNumber){PrevVertexNumbers[VertexNumber] = VertexNumber - 1;NextVertexNumbers[VertexNumber] = VertexNumber + 1;}PrevVertexNumbers[0] = PolygonVertexCount - 1;NextVertexNumbers[PolygonVertexCount - 1] = 0;int32 EarVertexNumber = 0;int32 EarTestCount = 0;for (int32 RemainingVertexCount = PolygonVertexCount; RemainingVertexCount >= 3; ){bool bIsEar = true;// If we're down to only a triangle, just treat it as an ear. Also, if we've tried every possible candidate// vertex looking for an ear, go ahead and just treat the current vertex as an ear. This can happen when // vertices are collinear or other degenerate cases.if (RemainingVertexCount > 3 && EarTestCount < RemainingVertexCount){const TVector2<T>& PrevVertexPosition = VertexPositions[PrevVertexNumbers[EarVertexNumber]];const TVector2<T>& EarVertexPosition = VertexPositions[EarVertexNumber];const TVector2<T>& NextVertexPosition = VertexPositions[NextVertexNumbers[EarVertexNumber]];// Figure out whether the potential ear triangle is facing the same direction as the polygon// itself. If it's facing the opposite direction, then we're dealing with a concave triangle// and we'll skip it for now.if (!Local::IsTriangleFlipped(OrientationSign, PrevVertexPosition, EarVertexPosition, NextVertexPosition)){int32 TestVertexNumber = NextVertexNumbers[NextVertexNumbers[EarVertexNumber]];do{// Test every other remaining vertex to make sure that it doesn't lie inside our potential ear// triangle. If we find a vertex that's inside the triangle, then it cannot actually be an ear.const TVector2<T>& TestVertexPosition = VertexPositions[TestVertexNumber];if (TTriangle2<T>::IsInside(PrevVertexPosition, EarVertexPosition, NextVertexPosition, TestVertexPosition)){bIsEar = false;break;}TestVertexNumber = NextVertexNumbers[TestVertexNumber];} while (TestVertexNumber != PrevVertexNumbers[EarVertexNumber]);}else{bIsEar = false;}}if (bIsEar){// OK, we found an ear! Let's save this triangle in our output buffer.{int32 A = PrevVertexNumbers[EarVertexNumber], B = EarVertexNumber, C = NextVertexNumbers[EarVertexNumber];OutTriangles.Add(bOrientAsHoleFill ? FIndex3i(A, C, B) : FIndex3i(A, B, C));}// Update our linked list. We're effectively cutting off the ear by pointing the ear vertex's neighbors to// point at their next sequential neighbor, and reducing the remaining vertex count by one.{NextVertexNumbers[PrevVertexNumbers[EarVertexNumber]] = NextVertexNumbers[EarVertexNumber];PrevVertexNumbers[NextVertexNumbers[EarVertexNumber]] = PrevVertexNumbers[EarVertexNumber];--RemainingVertexCount;}// Move on to the previous vertex in the list, now that this vertex was cutEarVertexNumber = PrevVertexNumbers[EarVertexNumber];EarTestCount = 0;}else{// The vertex is not the ear vertex, because it formed a triangle that either had a normal which pointed in the opposite direction// of the polygon, or at least one of the other polygon vertices was found to be inside the triangle. Move on to the next vertex.EarVertexNumber = NextVertexNumbers[EarVertexNumber];// Keep track of how many ear vertices we've tested, so that if we exhaust all remaining vertices, we can// fall back to clipping the triangle and adding it to our mesh anyway. This is important for degenerate cases.++EarTestCount;}}check(OutTriangles.Num() > 0);
}
相关链接
- 多边形三角化
- TriangulationByEarClipping.pdf