算法系列之九:計算幾何與圖形學有關的幾種常用算法(一)

 

        我的專業是計算機輔助設計(CAD),算是一半機械一半軟件,《計算機圖形學》是必修課,也是我最喜歡的課程。熱衷於用代碼擺平一切的我幾乎將這本教科書上的每種算法都實現了一遍,這種重複勞動雖然意義不大,但是收穫很多,特別是丟棄了多年的數學又重新回到了腦袋中,算是最大的收穫吧。儘管已經畢業多年了,但是每次回顧這些算法的代碼,都覺得內心十分澎湃,如果換成現在的我,恐怕再也不會有動力去做這些事情了。

        在學習《計算機圖形學》之前,總覺得很多東西高深莫測,但實際掌握了之後,卻發現其中了無神祕可言,就如同被原始人像神一樣崇拜的火卻被現代人叼在嘴上玩弄一樣的感覺。圖形學的基礎之一就是計算幾何,但是沒有理論數學那麼高深莫測,它很有實踐性,有時候甚至可以簡單到匪夷所思。計算幾何是隨着計算機和CAD的應用而誕生的一門新興學科,在國外被稱爲“計算機輔助幾何設計(Computer Aided Geometric Design,CAGD)”。“算法系列”接下來的幾篇文章就會介紹一些圖形學中常見的計算幾何算法(順便曬曬我的舊代碼),都是一些圖形學中的基礎算法,需要一些圖形學的知識和數學知識,但是都不難。不信?那就來看看到底有多難。

        本文是第一篇,主要是一些圖形學常用的計算幾何方法,涉及到向量、點線關係以及點與多邊形關係求解等數學知識,還有一些平面幾何的基本原理。事先聲明一下,文中涉及的算法實現都是着眼於解釋原理以及揭示算法實質的目的,在算法效率和可讀性二者的考量上,更注重可讀性,有時候爲了提高可讀性會刻意採取“效率不高”的代碼形式,實際工程中使用的代碼肯定更緊湊更高效,但是算法原理都是一樣的,請讀者們對此有正確的認識。

 

一、        判斷點是否在矩形內

 

        計算機圖形學和數學到底有什麼關係?我們先來看幾個例子,增加一些感性認識。首先是判斷一個點是否在矩形內的算法,這是一個很簡單的算法,但是卻非常重要。比如你在一個按鈕上點擊鼠標,系統如何知道你要觸發這個按鈕對應的事件而不是另一個按鈕?對了,就是一個點是否在矩形內的判斷處理。Windows 的API提供了PtInRect()函數,實現方法其實就是判斷點的x座標和y座標是否同時落在矩形的x座標範圍和y座標範圍內,算法實現也很簡單:

  150 bool IsPointInRect(const Rect& rc, const Point& p)

  151 {

  152     double xr = (p.x - rc.p1.x) * (p.x - rc.p2.x);

  153     double yr = (p.y - rc.p1.y) * (p.y - rc.p2.y);

  154 

  155     return ( (xr <= 0.0) && (yr <= 0.0) );

  156 }

 看看IsPointInRect()函數的實現是否和你想象的不一樣?有時候硬件實現乘法有困難或受限制於CPU乘法指令的效率,可以考慮用下面的函數替換,代碼繁瑣了一點,但是避免了乘法運算:

  120 bool IsPointInRect(const Rect& rc, const Point& p)

  121 {

  122     double xl,xr,yt,yb;

  123 

  124     if(rc.p1.x < rc.p2.x)

  125     {

  126         xl = rc.p1.x;

  127         xr = rc.p2.x;

  128     }

  129     else

  130     {

  131         xl = rc.p2.x;

  132         xr = rc.p1.x;

  133     }

  134 

  135     if(rc.p1.y < rc.p2.y)

  136     {

  137         yt = rc.p2.y;

  138         yb = rc.p1.y;

  139     }

  140     else

  141     {

  142         yt = rc.p1.y;

  143         yb = rc.p2.y;

  144     }

  145 

  146     return ( (p.x >= xl && p.x <= xr) && (p.y >= yb && p.y <= yt) );

  147 }

由於IsPointInRect()函數並不假設矩形的兩個定點是按照座標軸升序排列的,所以算法實現時就考慮了所有可能的座標範圍。IsPointInRect()函數使用的是平面直角座標系,如果不特別說明,本文所有的算法都是基於平面直角座標系設計的。另外,IsPointInRect()函數沒有指定特別的浮點數精度範圍,默認是系統浮點數的最大精度,只在某些必須要與0比較的情況下,採用10-8次方精度,如無特別說明,本文的所有算法都這樣處理。

一、        判斷點是否在圓內

 

        現在考慮複雜一點,如果圖形界面的按鈕不是矩形而是圓形的怎麼辦呢?當然就是判斷點是否在圓內部。判斷算法的原理就是計算點到圓心的距離d,然後與圓半徑r進行比較,若d < r則說明點在圓內,若d = r則說明點在圓上,若d > r則說明點在圓外。這就要提到計算平面上兩點距離的算法。以下圖爲例,計算平面上任意兩點之間的距離主要依據著名的勾股定理:

圖1 平面兩點距離計算示意圖

  113 //計算歐氏幾何空間內平面兩點的距離

  114 double PointDistance(const Point& p1, const Point& p2)

  115 {

  116     return std::sqrt( (p1.x-p2.x)*(p1.x-p2.x)

  117                         + (p1.y-p2.y)*(p1.y-p2.y) );

  118 }

 

一、        判斷點是否在多邊形內

 

        現在再考慮複雜一點的,如果按鈕是個不規則的多邊形區域怎麼辦?別以爲這個考慮沒有意義,很多多媒體軟件和遊戲,通常都是用各種形狀的不規則圖案作爲熱點(Hot Spot),Windows也提供了PtInRegion() API,用於判斷點是否在一個不規則區域中。我們對問題做一個簡化,就判斷一個點是否在多邊形內?判斷點P是否在多邊形內是計算幾何中一個非常基本的算法,最常用的方法是射線法。以P點爲端點,向左方做射線L,然後沿着L從無窮遠處開始向P點移動,當遇到多邊形的某一條邊時,記爲與多邊形的第一個交點,表示進入多邊形內部,繼續移動,當遇到另一個交點時,表示離開多邊形內部。由此可知,當L與多邊形的交點個數是偶數時,表示P點在多邊形外,當L與多邊形交點個數是奇數時,表示P點在多邊形內部。

        由此可見,要實現判斷點是否在多邊形內的算法,需要知道直線段求交算法,而求交算法又涉及到矢量的一些基本概念,因此在實現這個算法之前,先講一下矢量的基本概念以及線段求交算法。

 

3.1 矢量的基礎知識

         什麼是矢量?簡單地講,就是既有大小又有方向的量,數學中又常被稱爲向量。矢量有幾何表示、代數表示和座標表示等多種表現形式,本文討論的是幾何表示。如果一條線段的端點是有次序之分的,我們把這種線段成爲有向線段(Directed Segment),比如線段P1P2,如果起始端點P1就是座標原點(0, 0),P2的座標是(x, y),則線段P1P2的二維矢量座標表示就是P= (x, y)。

 

3.2 矢量的加法和減法

         現在來看幾個與矢量有關的重要概念,首先是矢量的加減法。假設有二維矢量P = ( x1, y1 ),Q = ( x2 , y2 ),則矢量加法定義爲:

 

P + Q = ( x1 + x2 , y1 + y2 )

 

同樣的,矢量減法定義爲:

 

P - Q = ( x1 - x2 , y1 - y2 )

 

根據矢量加減法的定義,矢量的加減法滿足以下性質:


P + Q = Q + P

P - Q = - ( Q - P )

 

圖2 演示了矢量加法和減法的幾何意義,由於幾何中直線段的兩個點不可能剛好在原點,因此線段P1P2的矢量其實就是OP2 - OP1的結果,如圖2 (b)所示:

3.3 矢量的叉積(外積)

         另一個比較重要的概念是矢量的叉積(外積)。計算矢量的叉積是判斷直線和線段、線段和線段以及線段和點的位置關係的核心算法。假設有二維矢量P = ( x1, y1 ),Q = ( x2 , y2 ),則矢量的叉積定義爲:

 

P × Q = x1*y2 - x2*y1

 

向量叉積的幾何意義可以描述爲由座標原點(0,0)、P、Q和P + Q所組成的平行四邊形的面積,而且是個帶符號的面積,由此可知,矢量的叉積具有以下性質:

 

P × Q = - ( Q × P )

 

叉積的結果P × Q是P和Q所在平面的法矢量,它的方向是垂直與P和Q所在的平面,並且按照P、Q和P × Q的次序構成右手系,所以叉積的另一個非常重要性質是可以通過它的符號可以判斷兩矢量相互之間位置關係是順時針還是逆時針關係,具體說明如下:

 

1) 如果 P × Q > 0 , 則Q在P的逆時針方向;

2) 如果 P × Q < 0 , 則Q在P的順時針方向;

3) 如果 P × Q = 0 , 則Q與P共線(但可能方向是反的);

 

3.4 矢量的點積(內積)

         最後要介紹的概念是矢量的點積(內積)。假設有二維矢量P = ( x1, y1 ),Q = ( x2 , y2 ),則矢量的點積定義爲:

 

P·Q = x1*x2 + y1*y2

 

向量點積的結果是一個標量,它的代數表示是:

 

P·Q = |P| |Q| cos(P, Q)

 

(P, Q) 表示向量P和Q的夾角,如果P和Q不共線,則根據上式可以得到向量點積的一個非常重要的性質,具體說明如下:

 

1) 如果 P · Q > 0 , 則P和Q的夾角是鈍角(大於90度);

2) 如果 P · Q < 0 , 則P和Q的夾角是銳角(小於90度);

3) 如果 P · Q = 0 , 則P和Q的夾角是90度;

 

        瞭解了矢量的概念以及矢量的各種運算的幾何意義和代數意義後,就可以開始解決各種計算幾何的簡單問題了,回想本文開始提到的點與多邊形的關係問題,首先要解決的就是判斷點和直線段的位置關係問題。

 

3.5 用矢量的叉積判斷點和直線的關係

 

        根據矢量叉積的幾何意義,如果線段所表示的矢量和點的矢量的叉積是0,就說明點在線段所在的直線上,相對於座標原點O來說,線段的矢量其實就是線段終點P2=[x2, y2]的矢量OP2減線段起點P1=[x1, y1]的矢量OP1的結果,因此線段P1P2的矢量可以表示爲P1P2=(x2 – x1, y2 – y1)。如果要判斷點P是否在線段P1P2上,就要判斷矢量P1P2和矢量OP的叉積是否是0。需要注意的是,叉積爲0只能說明點P與線段P1P2所在的直線共線,並不能說明點P一定會落在P1P2區間上,因此只是一個必要條件。要正確判斷P在線段P1P2上,還需要做一個排斥試驗,就是檢查點P是否在以直線段爲對角線的矩形空間內,如果以上兩個條件都爲真,即可判定點在線段上。有了上述原理,算法實現就比較簡單了,以下就是判斷點是否在線段上的算法:

  174 bool IsPointOnLineSegment(const LineSeg& ls, const Point& pt)

  175 {

  176     Rect rc;

  177 

  178     GetLineSegmentRect(ls, rc);

  179     double cp = CrossProduct(ls.pe.x - ls.ps.x, ls.pe.y - ls.ps.y,

  180                              pt.x - ls.ps.x, pt.y - ls.ps.y); //計算叉積

  181 

  182     return ( (IsPointInRect(rc, pt)) //排除實驗

  183              && IsZeroFloatValue(cp) ); //1E-8 精度

  184 }

  185 

 

【未完,下篇繼續介紹用矢量叉積判斷直線和直線段的位置關係,以及判斷點與多邊形關係的完整算法】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章