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

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

我的專業是計算機輔助設計(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.- rc.p1.x) * (p.- rc.p2.x);

  153     double yr = (p.- rc.p1.y) * (p.- 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.< 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.< 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.>= xl && p.<= xr) && (p.>= yb && p.<= 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的向量其實就是OP- 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.- ls.ps.x, ls.pe.- ls.ps.y,

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

  181 

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

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

  184 }

  185