当前位置: 首页 > article >正文

耳切法简述

耳切法简述

将简单多边形分解成三角形称为多边形的三角剖分。对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)

  1. 凸顶点列表 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 中。
    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
    2

  3. 凸顶点的列表 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
    3

  4. 凸顶点的初始列表 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
    4

  5. 凸顶点的列表 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
    5

  6. 凸顶点的列表 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
    6

  7. 凸顶点的列表 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

7
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
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 empty
	int32 PolygonVertexCount = VertexPositions.Num();
	if (PolygonVertexCount < 3)
	{
		return;
	}


	// compute signed area of polygon
	double 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 cut
			EarVertexNumber = 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);
}

相关链接

  1. 多边形三角化
  2. TriangulationByEarClipping.pdf

http://www.kler.cn/a/461216.html

相关文章:

  • 用uniapp写一个播放视频首页页面代码
  • 办公 三之 Excel 数据限定录入与格式变换
  • 【GO基础学习】gin的使用
  • 低代码引擎插件开发:开启开发的便捷与创新之路
  • 15. 接雨水
  • 2024/12/29 黄冈师范学院计算机学院网络工程《路由期末复习作业一》
  • 矩阵的因子分解3-LU分解和LDU分解
  • WebSocket 入门详解
  • 【每日学点鸿蒙知识】Taro、native层获取文件宽度、位置变化callback、数据迁移、oh_modules说明等
  • QT--多线程
  • 深入浅出 Spring (二)| 依赖注入(DI)、自动装配
  • 课程思政元素收集系统|Java|SSM|JSP|
  • 计算机网络基础知识(7)中科大郑铨老师笔记
  • 【视觉SLAM:四、相机与图像】
  • 公交智眼 4G 录像机:开启安全运营新篇章
  • spring中常见的自动注入方式
  • 论文实现:Reactive Nonholonomic Trajectory Generation via Parametric Optimal Control
  • Vue3 简介
  • C++初步认识函数
  • @RestControllerAdvice注解
  • OneOS操作系统入门-驱动-03:I2C总线及驱动
  • java实现excel导入参考资料合集
  • Zookeeper在中间件的应用和在Spring Boot业务系统中实现分布式锁和注册中心的解决方案
  • CT 扫描显示 USB-C 电缆可能隐藏复杂的恶意硬件
  • 强化学习方法分类详解
  • 电脑cxcore.dll文件缺失怎么办?cxcore100.dll缺失问题解决办法