多边形分割
钢板建模的思路是,通过已经获得的钢板轮廓点集,生成钢板的两个底面,再将两个底面通过侧面闭合成三维模型:
由于侧面三角面片的生成我们已经在骨骼建模中应用,代码完全可以复用。可以说现在钢板建模的关键点,就在于如何生成两个底面模型(其实只需要生成一个底面,因为顶部可以由底部平移得到)。难点一,底部面片轮廓点组成的多边形可能是凹多边形,不利于面片模型的生成。难点二,如何才能让底部面片模型完全贴合骨骼模型的外表面。
本章将重点讨论难点一,凹多边形的处理,或者说如何将凹多边形切割为多个凸多边形。假设我们可以为凸多边形建立面片模型(事实上确实可以),那么将凹多边形切割为多个凸多边形,就可以为每个凸多边形分别计算面片模型,最后再将这些面片合并,便得到了凹多边形的面片模型。
多边形的基本信息主要由类Polygon
负责。Polygon
对象记录了钢板面片的所有轮廓点,以及哪些轮廓点是凹点。凹点的判断非常重要,它是多边形凸分解算法所倚仗的关键信息。在Polygon
中,凹点判断函数为MarkConvex
,该函数会将每个轮廓点标记为凹点(1)或是凸点(0)。凹点的判断思路有点类似于凸包检测的思路,首先需要将所有的轮廓点按顺时针方向排序,这样对于一点B
,假设前一个点为A
,后一个点为C
,如果C
位于向量AB
的左侧,那么B
就是凹点。方向判断可以通过矢量叉积运算得到,如果P x Q > 0
,则向量P位于向量Q的顺时针方向。
public void MarkConvex() // 调用前确保轮廓点按顺时针方向排序
{
int nCount = 0;
nCount = listPolyNode.Count;
if (nCount < 4) return; // 三角形不需要处理,直接返回
for (int i = 0; i < nCount; i++)
{
Vector2 prev, mid, next;
prev = listPolyNode[(nCount + i - 1) % nCount].point;
mid = listPolyNode[(nCount + i + 0) % nCount].point;
next = listPolyNode[(nCount + i + 1) % nCount].point;
float nMul = Multiply(mid, next, prev); // 向量叉乘
bool bIsConvex = nMul < 0 ? true : false;
if (!bIsConvex)
listPolyNode[(i + 0) % nCount].nMark = 1; // 标记为凹点
else
listPolyNode[(i + 0) % nCount].nMark = 0;
}
}
public float Multiply(Vector2 op, Vector2 sp, Vector2 ep)
{
return ((sp.x - op.x) * (ep.y - op.y) - (ep.x - op.x) * (sp.y - op.y));
}
Rogers
类专门负责多边形的凸分解。凸分解函数RogersDecompose
输入一个多边形Polygon
对象作为参数,输出一个Polygon
链表,其中的Polygon
对象均为凸多边形。
//////////////////////////////////////////////////////////////////////////
//----简单多边形凸分解算法,大致分为以下几个步骤:
//1. 判断多边形顺序
//2. 查找第一个凹点
//3. 选取分割点(最为关键的一步)
//4. 根据交点分割多边形
//5. 递归分解,直到所有多边形都为凸多边形
//////////////////////////////////////////////////////////////////////////
// 分区域分解算法:不考虑角度和顶点数
public void RogersDecompose(ref Polygon poly)
{
List<PolyNode> arrPoly = poly.listPolyNode; // 获取顶点列表
int nCount = arrPoly.Count;
if (nCount < 4)
{ // 三角形不需要处理,直接返回
listPolygon.Add(poly);
return;
}
// 0. 判断多边形顺序
poly.ProcAntic();
if (poly.isAntic)
{ // 保证多边形轮廓点按顺时针方向排序
poly.ConversPoly();
poly.ProcAntic();
}
// 1. 标记凹点
poly.ClearMark();
// 2. 查找第一个凹点
int nCur = 0;
nCur = poly.GetNotMarkConvex(); // 获取第一个凹点,不对原轮廓点做标记
if (nCur < 0)
{
listPolygon.Add(poly);
return; // 表示没有凹点,不需要继续分割
}
// 3. 选取分割点(最为关键的一步)
Vector2 splitPoint = Vector2.zero; // 交点
int nIntersectionIndex = -1;
if (GetSplitPointMeans(ref poly, nCur, ref nIntersectionIndex, ref splitPoint))
{ // 分区域法寻找分割点
Vector2 p1 = arrPoly[nIntersectionIndex].point;
Polygon lPoly = new Polygon();
Polygon rPoly = new Polygon();
// 4. 根据交点分割多边形
poly.Split(nCur, nIntersectionIndex, lPoly, rPoly);
poly.Clear();
lPoly.AddPoint(splitPoint);
if (splitPoint != p1)
{
rPoly.AddPoint(splitPoint);
}
// 5. 递归调用
RogersDecompose(ref lPoly);
RogersDecompose(ref rPoly);
}
else
{
listPolygon.Add(poly);
return; // 求不出交点,默认已经是凸多边形
}
}
如何选取分割点是凸分解算法的一个关键点。上述代码中,函数GetSplitPointMeans
输入一个多边形和凹点,返回一个分割点序号和分割点对象。分割点的选取有多种方法,我们认为,好的分割点可以使分割后的多边形消除更多的凹点,并且分割后的两个多边形拥有相似的形态。
经典的分割点选取算法是Rogers
算法。
设简单多边形顶点按逆时针方向排序,且以单向链表表示连接,如图(a)。算法可以从顶点a开始延链表方向寻找到第一个凹点c,从凹点c沿有向边bc(称为凹边)作射线,与多边形其余边求交,取离点c最近的交点,如图(b)所示的点j,沿线段cj将多边形一分为二,得到两个多边形abjhia
和cdefgj'c
,然后对这两个多边形再分别作同样的处理,这是一个递归的过程,直到所有新产生的多边形均为凸多边形。如图(c),原凹多边形被分解为4个凸多边形abjka
、hik'h
、cdlj'c
和efgl'e
。
Rogers算法的时间复杂度为O(mn),其中m为凹点个数,n为多边形顶点个数。Rogers算法简单清晰易实现,但其存在如下的缺陷:
(1) 计算量大。分割点需要凹边与其它所有线段求交得到。
(2) 剖分产生的凸多边形数量较多。如果分割点不是已有的凹点,那么凸多边形的数量为(m+1)个,这是绝大多数情况。而这往往是现有其它算法的最坏结果。
(3) 有可能产生非常细小或非常微小的凸多边形。这种情况出现在凹边与相邻边夹角较小时。
因此,通常我们不直接使用Rogers算法,而是使用基于顶部可见性的局部剖分算法,夏旸师兄在这里也是使用了该算法的变形。该算法类似Rogers算法,需要对多边形的顶点进行排序,并找到第一个凹点作为分割线的一个端点。不同的是,分割点的选取不再是凹边延长线的交点。
假设有如下多边形,其第一个凹点为vi,设M为与有向线段vi-1,vi方向一致的射线,设N为与有向线段vi+1,vi方向一致的射线。M和N所在直线将平面分为4个区域:A、B、C、D。
分别计算A、B、C区域中vi的可见点,即与vi连线间没有相交点的顶点,用SA、SB、SC表示这3个点集:SA=(s2,s3,s4,s5); SB=(s0,s1); SC=(s6,s7,s8,s9)。从可见点的几何特性可得到结论:如果SA为空,则SB和SC必不为空。证明不再给出。
基于顶点可见性的凹多边形局部剖分算法步骤如下:
1. 搜索凹点,如果多边形没有凹点,算法结束。
2. 搜索当前凹点的可见点集SA、SB、SC。
3. 如果SA不为空,则:
(1) 将SA中的顶点划分为凸点集SetA和凹点集SetB。
(2) 如果SetB不为空,选择SetB中与射线MN角平分线投影最长的凹点作为分割点。
(3) 如果SetB为空,选择SetA中与射线MN角平分线投影最长的凸点作为分割点。
4. 如果SA为空,由结论可知SB、SC必不为空,连接SB的最后一个顶点和SC的第一个顶点,做射线MN的角平分线与连线相交,即得到分割点。
5. 从凹点至分割点引分割线,将多边形切割成两个多边形。
6. 对新产生的两个多边形按上述步骤递归地进行凹多边形凸分解,直到所有多边形均为凸多边形为止。
在步骤3中,我们优先选取区域A中的凹点作为分割点,旨在一次消除两个凹点,减少多边形的数量。另外,为了保证分割后的两个多边形拥有相似的形态,我们取在角平分线上有最长投影的点作为分割点。设射线M的单位向量为m,射线N的单位向量为n,那么MN的角平分线有mid = m + n
。选取分割点时,作向量(vi,si)
,其中si为当前顶点,计算(vi,si)
在角平分线mid上的投影长度,取长度最长的顶点作为分割点。
这部分的代码位于Rogers.cs
中:
//分区域分割点选取方法
//poly: 简单多边形
//nCur: 凹点索引
//nIntersectionIndex:交点索引(如果有效)
//splitPoint:交点坐标
public bool GetSplitPointByRgnBCInter(ref Polygon poly, int nCur, ref int nIntersectionIndex, ref Vector2 splitPoint)
{
List<PolyNode> arrPoly = poly.listPolyNode; // 获取顶点列表
int nCount = arrPoly.Count;
if (nCount < 4) return false;
nIntersectionIndex = -1; // 如果存在交点,则为交点的索引值
// 1. 找Pi前后两个点Pi-1和Pi+1
int nPrev, nNext; // 前一个点、后一个点的索引
nPrev = (nCount + nCur - 1) % nCount;
nNext = (nCount + nCur + 1) % nCount;
Vector2 prevPoint, curPoint, nextPoint; // 前一个点坐标、交点坐标
prevPoint = arrPoly[nPrev].point;
curPoint = arrPoly[nCur].point;
nextPoint = arrPoly[nNext].point;
// 2. 建立存储四个分区的结构A、B、C、D
List<int> A = new List<int>();
List<int> B = new List<int>();
List<int> C = new List<int>();
List<int> D = new List<int>();
// 3. 循环判断每个点所属区域
int nMax = 0;
nMax = nNext <= nPrev ? nPrev : nPrev + nCount;
for (int i = nNext; i <= nMax; i++)
{
Vector2 dPoint = arrPoly[i % nCount].point;
float f1, f2;
f1 = FuncVal(prevPoint, curPoint, dPoint); // 计算dPoint相对于直线M的位置
f2 = FuncVal(nextPoint, curPoint, dPoint); // 计算dPoint相对于直线N的位置
if (f1 > 0 && f2 < 0)
{
A.Add(i % nCount);
}
else if (f1 <= 0 && f2 <= 0)
{
B.Add(i % nCount);
}
else if (f1 > 0 && f2 > 0)
{
C.Add(i % nCount);
}
else if (f1 < 0 && f2 > 0)
{
D.Add(i % nCount);
}
}
// 求ABCD可见点串A1B1C1D1
List<int> A1 = new List<int>();
List<int> B1 = new List<int>();
List<int> C1 = new List<int>();
List<int> D1 = new List<int>();
VisibleSets(ref poly, nCur, A, A1);
CopyArray(B, B1);
CopyArray(D, D1);
VisibleSets(ref poly, nCur, C, C1);
// 4. 如果A1不为空,则处理A
if (A1.Count > 0)
{
List<int> SetA = new List<int>();
List<int> SetB = new List<int>();
SetsSplit(ref poly, A1, SetA, SetB); // SetA:凸点集合,SetB凹点集合
if (SetB.Count > 0) // 凹点集合不为空,则优先处理凹点集合
{
nIntersectionIndex = GetBestPoint(ref poly, nCur, SetB);
}
else
{
nIntersectionIndex = GetBestPoint(ref poly, nCur, SetA);
}
if (nIntersectionIndex < 0 && nIntersectionIndex >= nCount) return false;
splitPoint = poly.listPolyNode[nIntersectionIndex].point;
return true;
}
else
{
int left = B1[B1.Count - 1]; // 最后一个
int right = C1[0]; // 第一个
Vector2 lPoint = poly.listPolyNode[left].point;
Vector2 rPoint = poly.listPolyNode[right].point;
// 线段LR
Vector2 d1 = rPoint - lPoint;
// 射线pL,端点是Pi
Vector2 d0, d00, d01;
d00 = (curPoint - prevPoint);
d01 = (curPoint - nextPoint);
Normalize(d00);
Normalize(d01);
d0 = (d00 + d01); // 角平分线
d0.x /= 2; d0.y /= 2;
// 求射线与线段的交点
if (Calculator.Intersection(curPoint, d0, lPoint, d1, ref splitPoint) != 1)
{
return false;
}
nIntersectionIndex = left; // 交点索引
return true;
}
}
通过计算,我们获得了一组凸多边形数组,这些凸多边形由原来的多边形分割所得。接下来,可以对这些凸多边形进行网格面片的生成了。