1. 程式人生 > >計算幾何與圖形學有關的幾種常用演算法(二)

計算幾何與圖形學有關的幾種常用演算法(二)

3.6 用向量的叉積判斷直線段是否有交

        向量叉積計算的另一個常用用途是直線段求交。求交演算法是計算機圖形學的核心演算法,也是體現速度和穩定性的重要標誌,高效並且穩定的求交演算法是任何一個CAD軟體都必需要重點關注的。求交包含兩層概念,一個是判斷是否相交,另一個是求出交點。直線(段)的求交演算法相對來說是比較簡單的,首先來看看如何判斷兩直線段是否相交。

        常規的代數計算通常分三步,首先根據線段還原出兩條線段所在直線的方程,然後聯立方程組求出交點,最後再判斷交點是否線上段區間上。常規的代數方法非常繁瑣,每次都要解方程組求交點,特別是交點不線上段區間的情況,計算交點就是做無用功。計算幾何方法判斷直線段是否有交點通常分兩個步驟完成,這兩個步驟分別是快速排斥試驗和跨立試驗。假設要判斷線段P1P2和線段Q1Q2是否有交點,則:

(1)       快速排斥試驗

    設以線段 P1P2 為對角線的矩形為R1, 設以線段 Q1Q2 為對角線的矩形為R2,如果R1和R2不相交,則兩線段不會有交點;

(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 直線段跨立試驗示意圖

        根據向量叉積的幾何意義,跨立試驗只能證明線段的兩端點位於另一個線段所在直線的兩邊,但是不能保證是在另一直線段的兩端,因此,跨立試驗只是證明兩條線段有交點的必要條件,必需和快速排斥試驗一起才能組成直線段相交的充分必要條件。根據以上分析,兩條線段有交點的完整判斷依據就是:1)以兩條線段為對角線的兩個矩形有交集;2)兩條線段相互跨立。

        判斷直線段跨立用計算叉積演算法的CrossProduct()函式即可,還需要一個判斷兩個矩形是否有交的演算法。矩形求交也是最簡單的求交演算法之一,原理就是根據兩個矩形的最大、最小座標判斷。圖4展示了兩個矩形沒有交集的各種情況:

圖4 矩形沒有交集的幾種情況

圖5展示了兩個矩形有交集的各種情況:

圖5 矩形有交集的幾種情況

從圖4和圖5可以分析出來兩個矩形有交集的幾何座標規律,就是在x座標方向和y座標方向分別滿足最大值最小值法則,簡單解釋這個法則就是每個矩形在每個方向上的座標最大值都要大於另一個矩形在這個座標方向上的座標最小值,否則在這個方向上就不能保證一定有位置重疊。由以上分析,判斷兩個矩形是否相交的演算法就可以如下實現:

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.- ls2.ps.x, ls1.ps.- ls2.ps.y,

212                                ls2.pe.- ls2.ps.x, ls2.pe.- ls2.ps.y);

213     //( P2 - Q1 ) ×'a1?( Q2 - Q1 )

214     double p2xq = CrossProduct(ls1.pe.- ls2.ps.x, ls1.pe.- ls2.ps.y,

215                                ls2.pe.- ls2.ps.x, ls2.pe.- ls2.ps.y);

216 

217     //( Q1 - P1 ) ×'a1?( P2 - P1 )

218     double q1xp = CrossProduct(ls2.ps.- ls1.ps.x, ls2.ps.- ls1.ps.y,

219                                ls1.pe.- ls1.ps.x, ls1.pe.- ls1.ps.y);

220     //( Q2 - P1 ) ×'a1?( P2 - P1 )

221     double q2xp = CrossProduct(ls2.pe.- ls1.ps.x, ls2.pe.- ls1.ps.y,

222                                ls1.pe.- ls1.ps.x, ls1.pe.- 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[(+ 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.> pp.pe.y))

246             {

247                 count++;

248             }

249             else if((IsSameFloatValue(pp.pe.y, pt.y)) && (pp.pe.> 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].< minx)

277             minx = py.pts[i].x;

278         if(py.pts[i].> maxx)

279             maxx = py.pts[i].x;

280         if(py.pts[i].< miny)

281             miny = py.pts[i].y;

282         if(py.pts[i].> 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年

相關推薦

計算幾何圖形有關常用演算法

3.6 用向量的叉積判斷直線段是否有交         向量叉積計算的另一個常用用途是直線段求交。求交演算法是計算機圖形學的核心演算法,也是體現速度和穩定性的重要標誌,高效並且穩定的求交演算法是任何一個CAD軟體都必需要重點關注的。求交包含兩層概念,一個是判斷是否相

算法系列之九:計算幾何圖形有關常用演算法

3.6 用向量的叉積判斷直線段是否有交        向量叉積計算的另一個常用用途是直線段求交。求交演算法是計算機圖形學的核心演算法,也是體現速度和穩定性的重要標誌,高效並且穩定的求交演算法是任何一個CAD軟體都必需要重點關注的。求交包含兩層概念,一個是判斷是否相交,另一個是

計算幾何圖形有關常用演算法

我的專業是計算機輔助設計(CAD),算是一半機械一半軟體,《計算機圖形學》是必修課,也是我最喜歡的課程。熱衷於用程式碼擺平一切的我幾乎將這本教科書上的每種演算法都實現了一遍,這種重複勞動雖然意義不大,但是收穫很多,特別是丟棄了多年的數學又重新回到了腦袋中,算是最大的收

計算幾何圖形有關常用演算法

我的專業是計算機輔助設計(CAD),算是一半機械一半軟體,《計算機圖形學》是必修課,也是我最喜歡的課程。熱衷於用程式碼擺平一切的我幾乎將這本教科書上的每種演算法都實現了一遍,這種重複勞動雖然意義不大,但是收穫很多,特別是丟棄了多年的數學又重新回到了腦袋中,算是最大的收穫吧

JDBC 資料庫的查詢方式

1.資料庫中的查詢jdbc.queryForObject( )和jdbc.query( );我的理解就是一個返回的物件,一個返回集合(集合可能不貼切) String sql = "select username from user where username

協議比較

以下是上述協議的簡單介紹:BSD開源協議 BSD開源協議是一個給於使用者很大自由的協議。基本上使用者可以”為所欲為”,可以自由的使用,修改原始碼,也可以將修改後的程式碼作為開源或者專有軟體再發布。 但”為所欲為”的前提當你釋出使用了BSD協議的程式碼,或則以BSD協議程式碼為基礎做二次開發自己的產品時,需要滿

圖形道基礎練習計算機圖形

整理資料夾時發現曾經做的幾個作業練習,發出來和大家一起學習吧。 ================================(題目①)================================= 在一個黑色的視窗中央畫出矩形、三角形、點和線。 踩坑記錄:一

對於單向連結串列的10常用演算法

list.c檔案如下 #include "list.h" /*返回head連結串列POS節點的位置*/ LINK list_moov_pos(LINK head,int pos){ LINK node = head; while(--pos) node = node->pNe

scikit-learn常用演算法的比較code

from sklearn import datasets import numpy as np iris =datasets.load_iris() X = iris.data[:,[2,3]] y = iris.target np.unique(y) #np.unique(y)返回儲

在C#中,Json的序列化和反序列化的方式總結轉載

在這篇文章中,我們將會學到如何使用C#,來序列化物件成為Json格式的資料,以及如何反序列化Json資料到物件。 什麼是JSON? JSON (JavaScript Object Notation) is a lightweight data-interchange format.

javascript 面向物件的常見寫法

//定義Circle類,擁有成員變數r,常量PI和計算面積的成員函式area()  文章轉自:http://www.iteye.com/topic/434462 1.工廠方式 var Circle = function() { var obj = new Object

java開發過程中常用演算法

排序演算法 排序演算法中包括:簡單排序、高階排序   簡單排序  簡單排序常用的有:氣泡排序、選擇排序、插入排序 氣泡排序程式碼如下: 1 private static void bubbleSrot(int[] arr) { 2 3 for (int

python建立子程序的常用方式fork, Process,程序池

linux下使用fork()建立子程序 Linux 作業系統提供了一個 fork() 函式用來建立子程序,這個函式很特殊,呼叫一次,返回兩次,因為作業系統是將當前的程序(父程序)複製了一份(子程序),然後分別在父程序和子程序內返回。子程序永遠返回0,而父程

Hbase資料入庫load方式比較

1. 預先生成HFile入庫 2. 通過MapReduce入庫 /* MapReduce 讀取hdfs上的檔案,以HTable.put(put)的方式在map中完成資料寫入,無reduce過程*/ import java.io.IOException; import or

Linux入門學習,怎麼使用Vim編輯器編輯儲存檔案?以及檢視瀏覽檔案的方式介紹——

引言:上期回顧(想要學習的童鞋可以點選看看) 上一章講述了在linux系統當中怎麼建立檔案以及對檔案的基本操作,這章主要是說檢視檔案的幾種方式,順便簡單簡單說下vim編輯器的使用。我們不管在使用什麼系

Unity中UGUI人物血條跟隨的實現方式

昨天在群裡有人在做遊戲的時候遇到了一個坑,就是用UGUI做人物血條跟隨遇到了大坑,今天就來說說如何用UGUI來做人物血條跟隨。 第一種: 把Canvas畫布作為Player的子物體。 首先:佈置一下場

php常用演算法

寫一下幾種常見排序演算法,清一下腦子 歸併排序 /** * Created by PhpStorm. * User: leon * Date: 2018/3/14 * Time: 下午2:37 */ function mergeSort(&am

學習筆記---PHP中加密演算法MD5,shal,base64_encode等

PHP中有多種加密演算法。 1.md5()—–不可逆,無解密演算法 md5(string $str[,bool $raw_output = false|true]); 引數: $str:待加密的字串 $raw_output:可選引數為true或

排序總結——堆排序

堆排序 這幾天看了演算法導論的排序部分,作一下總結。 堆排序的優點        1)最壞情況下o(nlgn)的時間複雜度        2)就地排序,不用輔助陣列 幾種操作(以最大堆為例) 1.保持堆性質         這是主要操作,對於節點A[i],前提是以LEFT(

常見的排序演算法java和C++版參考《演算法

博主這裡要講的幾種排序演算法包括(從難到易):1.氣泡排序(最low的演算法) 2.插入排序 3.希爾排序 4.歸併排序 5.快速排序 6.快速排序的三項切分 氣泡排序: (1)簡介:這是最原始,最簡單的排序,幾乎不需要額外的空間 (2)基本原理:通過迴圈將最大的元素移到