3.6ベクトルの外積を使用して、直線セグメントに交差があるかどうかを判断する
ベクトルの外積計算のもう1つの一般的な使用法は、直線セグメントの交差です。交差アルゴリズムは、コンピュータグラフィックスのコアアルゴリズムであり、速度と安定性の重要なシンボルでもあります。効率的で安定した交差アルゴリズムは、CADソフトウェアが注意しなければならないものです。交差点の検索には2つのレベルの概念が含まれます。1つは交差するかどうかを決定することであり、もう1つは交差点を見つけることです。直線(セグメント)の交差アルゴリズムは比較的単純です。最初に、2つの直線セグメントが交差するかどうかを判断する方法を見てみましょう。
従来の代数計算は通常3つのステップで構成されます。最初に、2つの線分が配置されている線の方程式を線分に従って復元し、次に連立方程式の交点を計算し、最後に交点が線分間隔上にあるかどうかを判断します。従来の代数的方法は非常に面倒であり、特に交点が線分区間内にない場合、交点を見つけるために連立方程式を解かなければならないたびに、交点を計算するのは無意味です。直線セグメントの交差があるかどうかを判断するための計算幾何学的手法は、通常、高速除去テストとストラドルテストという2つのステップで完了します。線分P1P2と線分Q1Q2の間に交差があるかどうかを決定すると仮定すると、次のようになります。
(1)迅速な拒絶反応試験
線分P1P2を対角線とする長方形がR1であり、線分Q1Q2を対角線とする長方形がR2であるとします。R1とR2が交差しない場合、2つの線分セグメント間に交差はありません。
(2)ストラドルテスト。
2つの線分が交差する場合、2つの線分が互いにまたがる必要があります。いわゆるストラドルとは、線分の両端が、もう一方の線分のある直線の両側にあることを意味します。ストラドルするか、ベクトル外積の幾何学的意味を使用するかを決定します。図3を例にとると、P1P2がQ1Q2にまたがる場合、ベクトル(P1-Q1)および(P2-Q1)はベクトル(Q2-Q1)の両側に配置されます。
(P1-Q1)×(Q2-Q1)*(P2-Q1)×(Q2-Q1)<0
上記の式は、次のように書き換えることができます。
(P1-Q1)×(Q2-Q1)*(Q2-Q1)×(P2-Q1)> 0
(P1-Q1)×(Q2-Q1)= 0の場合、線分P1P2とQ1Q2が同一線上にあることを意味します(ただし、必ずしも交点はありません)。同様に、Q1Q2がP1P2をまたぐと判断する基準は次のとおりです。
(Q1-P1)×(P2-P1)*(Q2-P1)×(P2-P1)<0
特定の状況を次の図に示します。
図3直線断面ストラドルテストの概略図
ベクトル外積の幾何学的な意味によると、ストラドルテストでは、線分の2つの端点が他の線分が配置されている線の両側にあることのみを証明できますが、他の線分の両端にあることは保証できません。したがって、ストラドルテストでは、2つの線分が交差に必要な条件を迅速な拒否テストと組み合わせて、直線セグメントの交差に必要かつ十分な条件を形成する必要があります。上記の分析によれば、2つの線分の交点を判断するための完全な根拠は、1)2つの線分が対角線としてある2つの長方形に交点があること、2)2つの線分が互いにまたがることです。
クロス積アルゴリズムを計算するCrossProduct()関数は、直線セグメントのストラドルを決定するために使用できます。2つの長方形が交差するかどうかを決定するアルゴリズムも必要です。長方形の交差も最も単純な交差アルゴリズムの1つであり、2つの長方形の最大座標と最小座標に基づいて判断することが原則です。図4は、2つの長方形が交差しないさまざまな状況を示しています。
図4長方形に交差がないいくつかの状況
図5は、2つの長方形が交差しているさまざまな状況を示しています。
図5長方形に交差があるいくつかの状況
図4と図5から、2つの長方形の交差の幾何座標の法則は、x座標方向とy座標方向で最大値と最小値が満たされるということが分析できます。このルールの簡単な説明は、各長方形が各方向にあるということです。座標の最大値は、この座標方向の他の長方形の座標の最小値よりも大きくなければなりません。そうでない場合、この方向に位置が重なる必要があるという保証はありません。上記の分析に基づいて、2つの長方形が交差するかどうかを判断するアルゴリズムは、次のように実装できます。
186 bool IsRectIntersect (const Rect & rc1 、 const Rect & rc2 ) 187 { 188 return ( (std::max(rc1.p1.x, rc1.p2.x) >= std::min(rc2.p1.x, rc2.p2.x)) 189 && (std::max(rc2.p1.x, rc2.p2.x) >= std::min(rc1.p1.x, rc1.p2.x)) 190 && (std::max(rc1.p1.y, rc1.p2.y) >= std::min(rc2.p1.y, rc2.p2.y)) 191 && (std::max(rc2.p1.y, rc2.p2.y) >= std::min(rc1.p1.y, rc1.p2.y)) ); 192 } |
完成了排斥试验和跨立试验的算法,最后判断直线段是否有交点的算法就水到渠成了:
204 bool IsLineSegmentIntersect(const LineSeg& ls1, const LineSeg& ls2) 205 { 206 if(IsLineSegmentExclusive(ls1, ls2)) //排斥实验 207 { 208 return false; 209 } 210 //( P1 - Q1 ) ×'a1?( Q2 - Q1 ) 211 double p1xq = CrossProduct(ls1.ps.x - ls2.ps.x, ls1.ps.y - ls2.ps.y, 212 ls2.pe.x - ls2.ps.x, ls2.pe.y - ls2.ps.y); 213 //( P2 - Q1 ) ×'a1?( Q2 - Q1 ) 214 double p2xq = CrossProduct(ls1.pe.x - ls2.ps.x, ls1.pe.y - ls2.ps.y, 215 ls2.pe.x - ls2.ps.x, ls2.pe.y - ls2.ps.y); 216 217 //( Q1 - P1 ) ×'a1?( P2 - P1 ) 218 double q1xp = CrossProduct(ls2.ps.x - ls1.ps.x, ls2.ps.y - ls1.ps.y, 219 ls1.pe.x - ls1.ps.x, ls1.pe.y - ls1.ps.y); 220 //( Q2 - P1 ) ×'a1?( P2 - P1 ) 221 double q2xp = CrossProduct(ls2.pe.x - ls1.ps.x, ls2.pe.y - ls1.ps.y, 222 ls1.pe.x - ls1.ps.x, ls1.pe.y - ls1.ps.y); 223 224 //跨立实验 225 return ( (p1xq * p2xq <= 0.0) && (q1xp * q2xp <= 0.0) ); 226 } |
IsLineSegmentExclusive()函数就是调用IsRectIntersect()函数根据结果做排斥判断,此处不再列出代码。
3.7 点和多边形关系的算法实现
好了,现在我们已经了解了矢量叉积的意义,以及判断直线段是否有交点的算法,现在回过头看看文章开始部分的讨论的问题:如何判断一个点是否在多边形内部?根据射线法的描述,其核心是求解从P点发出的射线与多边形的边是否有交点。注意,这里说的是射线,而我们前面讨论的都是线段,好像不适用吧?没错,确实是不适用,但是我要介绍一种用计算机解决问题时常用的建模思想,应用了这种思想之后,我们前面讨论的方法就适用了。什么思想呢?就是根据问题域的规模和性质抽象和简化模型的思想,这可不是故弄玄虚,说说具体的思路吧。
计算机是不能表示无穷大和无穷小,计算机处理的每一个数都有确定的值,而且必须有确定的值。我们面临的问题域是整个实数空间的坐标系,在每个维度上都是从负无穷到正无穷,比如射线,就是从坐标系中一个明确的点到无穷远处的连线。这就有点为难计算机了,为此我们需要简化问题的规模。假设问题中多边形的每个点的坐标都不会超过(-10000.0, +10000.0)区间(比如我们常见的图形输出设备都有大小的限制),我们就可以将问题域简化为(-10000.0, +10000.0)区间内的一小块区域,对于这块区域来说,>= 10000.0就意味着无穷远。你肯定已经明白了,数学模型经过简化后,算法中提到的射线就可以理解为从模型边界到内部点P之间的线段,前面讨论的关于线段的算法就可以使用了。
射线法的基本原理是判断由P点发出的射线与多边形的交点个数,交点个数是奇数表示P点在多边形内(在多边形的边上也视为在多边形内部的特殊情况),正常情况下经过点P的射线应该如图6(a)所示那样,但是也可能碰到多种非正常情况,比如刚好经过多边形一个定点的情况,如图6 (b),这会被误认为和两条边都有交点,还可能与某一条边共线如图6 (c)和(d),共线就有无穷多的交点,导致判断规则失效。还要考虑凹多边形的情况,如图6(e)。
图6 射线法可能遇到的各种交点情况
针对这些特殊情况,在对多边形的每条边进行判断时,要考虑以下这些特殊情况,假设当前处理的边是P1P2,则有以下原则:
1)如果点P在边P1P2上,则直接判定点P在多边形内;
2)如果从P发出的射线正好穿过P1或者P2,那么这个交点会被算作2次(因为在处理以P1或P2为端点的其它边时可能已经计算过这个点了),对这种情况的处理原则是:如果P的y坐标与P1、P2中较小的y坐标相同,则忽略这个交点;
3)如果从P发出的射线与P1P2平行,则忽略这条边;
对于第三个原则,需要判断两条直线是否平行,通常的方法是计算两条直线的斜率,但是本算法因为只涉及到直线段(射线也被模型简化为长线段了),就简化了很多,判断直线是否水平,只要比较一下线段起始点的y坐标是否相等就行了,而判断直线是否垂直,也只要比较一下线段起始点的x坐标是否相等就行了。
应用以上原则后,扫描线法判断点是否在多边形内的算法流程就完整了,图7就是算法的流程图:
最终扫描线法判断点是否在多边形内的算法实现如下:
228 bool IsPointInPolygon(const Polygon& py, const Point& pt) 229 { 230 assert(py.IsValid()); /*只考虑正常的多边形,边数>=3*/ 231 232 int count = 0; 233 LineSeg ll = LineSeg(pt, Point(-INFINITE, pt.y)); /*射线L*/ 234 for(int i = 0; i < py.GetPolyCount(); i++) 235 { 236 /*当前点和下一个点组成线段P1P2*/ 237 LineSeg pp = LineSeg(py.pts[i], py.pts[(i + 1) % py.GetPolyCount()]); 238 if(IsPointOnLineSegment(pp, pt)) 239 { 240 return true; 241 } 242 243 if(!pp.IsHorizontal()) 244 { 245 if((IsSameFloatValue(pp.ps.y, pt.y)) && (pp.ps.y > pp.pe.y)) 246 { 247 count++; 248 } 249 else if((IsSameFloatValue(pp.pe.y, pt.y)) && (pp.pe.y > pp.ps.y)) 250 { 251 count++; 252 } 253 else 254 { 255 if(IsLineSegmentIntersect(pp, ll)) 256 { 257 count++; 258 } 259 } 260 } 261 } 262 263 return ((count % 2) == 1); 264 } |
在图形学领域实施的真正工程代码,通常还会增加一个多边形的外包矩形快速判断,对点根本就不在多边形周围的情况做快速排除,提高算法效率。这又涉及到求多边形外包矩形的算法,这个算法也很简单,就是遍历多边形的所有节点,找出各个坐标方向上的最大最小值。以下就是求多边形外包矩形的算法:
266 void GetPolygonEnvelopRect(const Polygon& py, Rect& rc) 267 { 268 assert(py.IsValid()); /*只考虑正常的多边形,边数>=3*/ 269 270 double minx = py.pts[0].x; 271 double maxx = py.pts[0].x; 272 double miny = py.pts[0].y; 273 double maxy = py.pts[0].y; 274 for(int i = 1; i < py.GetPolyCount(); i++) 275 { 276 if(py.pts[i].x < minx) 277 minx = py.pts[i].x; 278 if(py.pts[i].x > maxx) 279 maxx = py.pts[i].x; 280 if(py.pts[i].y < miny) 281 miny = py.pts[i].y; 282 if(py.pts[i].y > maxy) 283 maxy = py.pts[i].y; 284 } 285 286 rc = Rect(minx, miny, maxx, maxy); 287 } |
除了扫描线法,还可以通过多边形边的法矢量方向、多边形面积以及角度和等方法判断点与多边形的关系。但是这些算法要么只支持凸多边形,要么需要复杂的三角函数运算(多边形边数小于44时,可采用近似公式计算夹角和,避免三角函数运算),使用的范围有限,只有扫描线法被广泛应用。
至此,本文的内容已经完结,以上通过对点与矩形、点与圆、点与直线以及点与多边形位置关系判断算法的讲解,向大家展示了几种常见的计算几何算法实现,用简短而易懂的代码剖析了这些算法的实质。下一篇将介绍计算机图形学中最基本的直线生成算法。
参考资料:
【1】计算几何:算法设计与分析 周培德 清华大学出版社 2005年
【2】计算几何:算法与应用 德贝尔赫(邓俊辉译) 清华大学出版社 2005年
【3】算法导论 Thomas H.Cormen等(潘金贵等译) 机械工业出版社 2006年
【4】计算机图形学 孙家广、杨常贵 清华大学出版社 1995年