Unity 2D 任意多边形切割算法

在工作中有用到Recast导航工具,寻路过程中,角色经常会从水中移动到目录位置,是因为并没有设置水区域的寻路权重。为了设置水区域且设置多边形又有条件需求(单个多边形要求:不大于12个顶点的凸多边形),需要将策划围出水区域切割为N个符合要求的多边形区域。为了以后方法查看,特此记录一下。

需求:将任意多边形切割为小于等于指定顶点的凸多边形。
流程:
1.如果多边形为凹多边形则,进行凹多边形分割为凸多边形
2.如果为凸多边形且顶点数量小于12,则不继续进行切割
3.计算凸多边形顶点数量大于12,则采用2分的方式进行切割

注意:所有顶点y轴均一致或都为0,提供的多边形在XZ平面上

首先,需要几个判断方法:
1.2D叉乘算法

	/// <summary>
	/// 2D叉乘
	/// </summary>
	/// <param name="v1">点1</param>
	/// <param name="v2">点2</param>
	/// <returns></returns>
	public static float CrossProduct2D(Vector3 v1, Vector3 v2)
	{
		//叉乘运算公式 x1*y2 - x2*y1
		float value = v1.x * v2.z - v2.x * v1.z;
		return value;
	}

2.凹多边形检测
采用叉积计算方式,顶点以逆时针方法存放,如果叉积大于0则表示该多边形是凹多边形,该点为凹点

	/// <summary>
	/// 凹多边形检测 并返回凹点下标
	/// </summary>
	/// <param name="points"></param>
	/// <returns></returns>
	private bool IsConcavePolygon(List<Vector3> points,out int pitIndex)
	{
		pitIndex = -1;
		if (points.Count <= 3)
			return false;
		//Debug.Log("凹边形判断");
		//检测凹点
		for (int i = 0; i < points.Count; i++)
		{
			int preIndex = i - 1;
			if (i == 0)
				preIndex = points.Count - 1;

			int nextIndex = i +1;
			if (nextIndex >= points.Count)
				nextIndex = 0;
			
			Vector3 v1 = points[preIndex] - points[i];
			Vector3 v2 = points[nextIndex] - points[i];
			//计算叉积,根据三维叉积公式计算,z轴为0 
			float corss = CrossProduct2D(v1,v2);
			corss = ((int) (corss * 100)) / 100.0f;
			if ( corss > 0)
			{
				//Debug.Log(corss);
				pitIndex = i;
				return true;
			}
		}
		return false;
	}

3.求直线的交点

	/// <summary>
	/// 求直线的交点
	/// </summary>
	/// <param name="line1Start"></param>
	/// <param name="line2End"></param>
	/// <param name="line2Start"></param>
	/// <param name="line2End"></param>
	/// <returns></returns>
	public static bool LineIntersectPoint(Vector3 line1Start, Vector3 line1End, Vector3 line2Start,
					Vector3 line2End,out Vector3 point)
	{
		point = Vector3.zero;
		//判断两条直线平行的情况
		float value = CrossProduct2D(line1End - line1Start, line2End - line2Start);
		if (Mathf.Approximately(Mathf.Abs(value),0))
			return false;
        
		//两点式公式
		//x0 = ((x3-x4) * (x2*y1 - x1*y2) - (x1-x2) * (x4*y3 - x3*y4)) / ((x3-x4) * (y1-y2) - (x1-x2) * (y3-y4));
		//y0 = ((y3-y4) * (y2*x1 - y1*x2) - (y1-y2) * (y4*x3 - y3*x4)) / ((y3-y4) * (x1-x2) - (y1-y2) * (x3-x4));
		float x1 = line1Start.x, x2 = line1End.x, x3 = line2Start.x, x4 = line2End.x;
		float y1 = line1Start.z, y2 = line1End.z, y3 = line2Start.z, y4 = line2End.z;
		point.x = ((x3-x4) * (x2*y1 - x1*y2) - (x1-x2) * (x4*y3 - x3*y4)) / ((x3-x4) * (y1-y2) - (x1-x2) * (y3-y4));
		point.z = ((y3-y4) * (y2*x1 - y1*x2) - (y1-y2) * (y4*x3 - y3*x4)) / ((y3-y4) * (x1-x2) - (y1-y2) * (x3-x4));
		//获取点在线上的投影位置
		point.y = line2End.y;
		return true;
	}

4.点是否在线段上
Dis(lineStart,lineEnd) == Dis(lineStart,point) + Dis(lineEnd,point)

Dis:距离  
lineStart:线段起点 
LineEnd:线段终点  
point:目标点
	/// <summary>
	/// 点是否在线段上
	/// </summary>
	/// <param name="point"></param>
	/// <param name="lineStart"></param>
	/// <param name="lineEnd"></param>
	/// <returns></returns>
	private bool IsPointOnSegment2(Vector3 point, Vector3 lineStart, Vector3 lineEnd)
	{
		//使所有点在一个平面内
		Vector3 startTemp = lineStart, endTemp = lineEnd, pointTemp = point;
		//startTemp.y = endTemp.y = pointTemp.y = 0;
		
		return Mathf.Approximately(Mathf.Abs((startTemp - pointTemp).magnitude) + Mathf.Abs((endTemp - pointTemp).magnitude),
						Mathf.Abs((endTemp - startTemp).magnitude));
	}

5.点与线的位置关系
也是通过叉乘计算

	/// <summary>
	/// 点与线的位置关系
	/// </summary>
	/// <param name="point"></param>
	/// <param name="lineStart"></param>
	/// <param name="lineEnd"></param>
	/// <returns>==0:点在线上 <0:点在线的左侧 >0:点在线的右侧</returns>
	private int IsPointToLinePosition(Vector3 point, Vector3 lineStart, Vector3 lineEnd)
	{
		float crossValue = CrossProduct2D(point - lineStart, lineEnd - lineStart);
		if (crossValue < 0) return -1;
		if (crossValue > 0) return 1;
		return 0;
	}

6.线段相交判断

	/// <summary>
	/// 线段是否相交
	/// </summary>
	/// <param name="segment1Start"></param>
	/// <param name="segment1End"></param>
	/// <param name="segment2Start"></param>
	/// <param name="segment2End"></param>
	/// <returns></returns>
	private bool IsSegmentIntersect(Vector3 segment1Start, Vector3 segment1End, Vector3 segment2Start,
					Vector3 segment2End)
	{
		//快速排斥实验
		if (Mathf.Min(segment1Start.x,segment1End.x) <= Mathf.Max(segment2Start.x,segment2End.x) 
		    && Mathf.Min(segment2Start.x,segment2End.x) <= Mathf.Max(segment1Start.x,segment1End.x)
		    && Mathf.Min(segment1Start.z,segment1End.z)<=Mathf.Max(segment2Start.z,segment2End.z)
		    && Mathf.Min(segment2Start.z,segment2End.z) <= Mathf.Max(segment1Start.z,segment1End.z))
		{
			//互为跨立的判断
			int state = IsPointToLinePosition(segment1Start, segment2Start, segment2End) +
			            IsPointToLinePosition(segment1End, segment2Start, segment2End) +
			            IsPointToLinePosition(segment2Start, segment1Start, segment1End) +
			            IsPointToLinePosition(segment2End, segment1Start, segment1End);
			if (state==0) return true;
		}
		return false;
	}

有上面的方法支撑后就可以开始切割多边形

这个方法是入口,需要传入逆时针顶点列表,遵循以下流程:
1.从待检测队列中获取需要检测的多边形
2.检测多边形是否为凹多变形(并获取凹点),如果是则调用凹多边形切割方法,将结果放入待检测队列中
3.检测是否顶点数量是否符合要求,符合要求则方法多边形列表中
4.如果顶点数量过多,则调用凸多必行切割方法,将结果放入待检测列表中

	/// <summary>
	/// 切割多边形
	/// </summary>
	/// <param name="points"></param>
	private List<List<Vector3>> CutPolygon(List<Vector3> points,int maxPoint)
	{
		//用于存放已切割正常的多边形
		List<List<Vector3>> result = new List<List<Vector3>>();
		if (points==null || points.Count<=0)
		{
			return result;
		}
	
		List<List<Vector3>> cutResult;
		//等待检测队列
		List<List<Vector3>> waitQueue = new List<List<Vector3>>();
		waitQueue.Add(points);
		//等待队列如果大于0 则持续进行切割
		while (waitQueue.Count>0)
		{
			if (result.Count>1000)
			{
				waitQueue.Clear();
				break;
			}
			List<Vector3> polygons = waitQueue[0];
			waitQueue.RemoveAt(0);
			//多边形是凹多边形 则以凹点进行多边形切割
			if (IsConcavePolygon(polygons,out int pitIndex))
			{
				cutResult = CutConcavePolygon(polygons,pitIndex);
				waitQueue.AddRange(cutResult);
				continue;
			}
			
			//检测多边形是否合格
			if (polygons.Count<=maxPoint)
			{
				result.Add(polygons);
				continue;
			}
			//是凸多边形,但是顶点数量超了,则分割凸多边形
			cutResult = CutRaisedPolygon(points);
			waitQueue.AddRange(cutResult);
		}
		return result;

这个方法是切割凹多边形:
凹多边形的切割比较复杂,遵循以下逻辑
1.获取凹点(在凹多边形判断时已获取)
2.构建凹点射线(凹点减去上一个顶点)
3.新建两个顶点列表用于存储切割的两个多边形,多边形1指射线左侧区域起点为凹点,结束点为切割点,多边形2指射线右侧区域,起点为凹点,切割点为第二个顶点。
4.从凹点开始依次与后面的线段做相交检测,找到切割点,如果该线段不是切割线段则将线段终点插入到多边形1的顶点列表中。
5.获取到切割线段与切割点后,可判断下凹点与切割线段的终点,两个间无其他线段则该点可作为切割点
6.生成多边形2的顶点列表(注意插入顺序以及环形处理)
7.返回两个多变形,插入待检测队列中

	/// <summary>
	/// 切割凹多边形 采用凹点延迟线切割多边形方式
	/// </summary>
	/// <param name="points"></param>
	/// <param name="pitIndex">凹点</param>
	/// <returns></returns>
	private List<List<Vector3>> CutConcavePolygon(List<Vector3> points,int pitIndex)
	{
		//Debug.Log(pitIndex);
		List<List<Vector3>> result = new List<List<Vector3>>();

		int preIndex = pitIndex - 1;
		if (preIndex<0)
			preIndex = points.Count - 1;

		//射线
		Vector3 rayStart = points[preIndex],rayDir = ( points[pitIndex] - rayStart).normalized;
		// polygon1 包含:凹点、切割点、射线线段相交检测未通过的点 polygon2包含:凹点,切割点,polygon1不包含的点
		List<Vector3> polygon1 = new List<Vector3>(){points[pitIndex],points[pitIndex+1]}, polygon2 = new List<Vector3>(){points[pitIndex]};

		int cutIndex = -1;
		
		//Debug.Log("Wait Polygon:"+Log(points));
		
		//射线与线段相交检测,找到切割点
		for (int i = pitIndex+1;i<points.Count+pitIndex-1; i++)
		{
			int index = i % points.Count;
			int nextIndex = (i + 1) % points.Count;

			Vector3 segmentStart = points[index],segmentEnd = points[nextIndex];
			//切割点
			Vector3 iPoint = Vector3.zero;
			//线段不与射线相交,则将线段加入polygon1中
			if (!IsRaySegmentIntersect(rayStart, rayDir, segmentStart, segmentEnd, out iPoint))
			{
				polygon1.Add(segmentEnd);
				continue;
			}
			
			//凹点与射线相交的线段结束点之间没有任何线段 则线段结束点为切割点
			if (!IsHasSegmentIntersect(points[pitIndex],points[nextIndex],points))
			{
				cutIndex = nextIndex;
				polygon1.Add(points[nextIndex]);
				polygon2.Add(points[nextIndex]);
				break;
			}

			cutIndex = index;
			polygon1.Add(iPoint);
			polygon2.Add(iPoint);
			break;
		}

		result.Add(polygon1);
		//生成polygon2的顶点集合
		for (int i = cutIndex+1; i < (cutIndex<pitIndex ? pitIndex : points.Count + pitIndex); i++)
		{
			int index = i % points.Count;
			polygon2.Add(points[index]);
		}
		result.Add(polygon2);

		//Debug.Log("Polygon 1:"+Log(polygon1));
		//Debug.Log("Polygon 1:"+Log(polygon2));
		return result;
	}
	
	/// <summary>
	/// 是否有线段与目标线段相交
	/// </summary>
	/// <param name="segmentStart"></param>
	/// <param name="segmentEnd"></param>
	/// <param name="points"></param>
	/// <returns></returns>
	private bool IsHasSegmentIntersect(Vector3 segmentStart, Vector3 segmentEnd, List<Vector3> points)
	{
		for (int i = 0; i < points.Count; i++)
		{
			int nextIndex = (i + 1) % points.Count;
			if(segmentStart==points[i] || segmentStart == points[nextIndex] || segmentEnd == points[i] || segmentEnd == points[nextIndex]) continue;
			if (IsSegmentIntersect(segmentStart, segmentEnd, points[i], points[nextIndex]))
			{
				//线段相交合格
				Debug.Log(string.Format("线段:{0},{1} 与{2},{3}相交:",segmentStart,segmentEnd,points[i],points[nextIndex]) );
				return true;
			}
		}
		return false;
	}
	

这个方法是凸多边形切割方法(使用二分法切割)

	/// <summary>
	/// 切割凸多边形
	/// </summary>
	/// <param name="points"></param>
	/// <returns></returns>
	private List<List<Vector3>> CutRaisedPolygon(List<Vector3> points)
	{
		List<List<Vector3>> result = new List<List<Vector3>>();
		//二分切割
		List<Vector3> polygon1 = new List<Vector3>(), polygon2 = new List<Vector3>();
		int centerIndex = points.Count / 2;
		for (int i = 0; i < points.Count; i++)
		{
			if (i==0 || i==centerIndex)
			{
				polygon1.Add(points[i]);
				polygon2.Add(points[i]);
				continue;
			}
			
			if (i<centerIndex)
			{
				polygon1.Add(points[i]);
				continue;
			}

			if (i>centerIndex)
				polygon2.Add(points[i]);
		}
		result.Add(polygon1);
		result.Add(polygon2);
		return result;
	}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘建宁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值