在工作中有用到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;
}