使用OpenGL實現三維座標的滑鼠揀選

來源:互聯網
上載者:User

一、簡介(Introduction)

       OpenGL是一種比較“純粹”的3D圖形API,一般僅用於三維圖形的渲染,對於特定領域的開發人員(如遊戲開發人員)而言,如果選擇使用 OpenGL進行開發,類似碰撞檢測的機制就都需要自行編寫了。但是由於滑鼠在圖形程式中的應用非常非常之廣泛(例如現在已經很少有PC遊戲能完全地脫離滑鼠),OpenGL在圖形庫的基礎上添加了選擇與反饋機制(Select & Feedback)來滿足使用者使用滑鼠即時操作三維圖形的需要。但由於種種原因,我們需要更為特殊的選擇機制以滿足特定需求,在這裡我們提出了一種簡單迅速的RIP(Ray-Intersection-Penetration)方法,可以滿足絕大多數典型應用的需要。

二、相關研究(Related Work)

       用過OpenGL選擇與反饋機制的開發人員,或多或少可能都會覺得它難以令人滿意。大致表現在下面幾個方面:

一、編寫程式比較繁瑣。

想要使用選擇反饋機制就需要切換渲染模式,操作命名堆棧,計算揀選矩陣,檢查選中記錄,這些繁瑣的步驟很容易出錯,而且非常不便於調試,只會降低工作效率和熱情。

二、只能做基於圖元的選定。

如(1 - a),使用GL_TRIANGLES繪製了一個三角形,三個頂點分別為 P1、P2和P3。若使用該機制,你將只能判斷是否在三維情境中選中了這個三角形(使用者點擊處是否在P1、P2和P3的範圍內),而無法判斷使用者是點擊了這個三角形哪一部分(是左邊的m地區內還是右邊的n地區內),因為所繪製的P1、P2和P3本身構成的三角形就是一個基本圖元,對於揀選機制而言是不可分的。當然,把這個三角形拆成兩個三角形再分別進行測試也是一個可行的方案,可是看看圖(1 - b),這可怎麼拆呢?還有圖(1 – c)呢?另外,如果n和m兩個平面不共面呢?對於使用者而言,OpenGL提供的揀選機制功能的確有限。

      

       三、降低了渲染效率。

OpenGL中的選擇和反饋是與普通渲染方式不同的一種特殊的渲染方式。我們使用時一般是先在幀緩衝中渲染普通情境,然後進入選擇模式重繪情境,此時幀緩衝的內容並無變化。也就是說,為了選擇某些物體,我們需要在一幀中使用不同的渲染方式將其渲染兩遍。我們知道對對象進行渲染是比較耗時的操作,當情境中需要選擇的對象多而雜的時候,採用這個機制是非常影響速度的。

       另外在OpenGL紅寶書中介紹了一種簡便易行的辦法:在後緩衝中使用不同的顏色重繪所有對象,每個對象用一個單色來標示其顏色,這樣畫好之後我們讀取滑鼠所在點的顏色,就能夠確定我們揀選了哪個物體。這種方法有一個缺陷,當情境中需要選擇的對象的數目超出一定限度時,可能會出現標識數的溢出。對於這個問題,紅寶書給出的解決辦法就是多次掃描。實踐證明這種方法的確簡便易行,但仍有不少局限性,而且做起來並不比第一種機制方便多少。限於篇幅,不再贅述。

三、具體描述(Related Work)

       看過了上面兩種方法,我們會發現這兩種方法都不是十分的方便,而且使用者不能對其進行完全的控制,不能精確地判定滑鼠定位與實際的世界空間中三維座標的關係。那麼有什麼更好的辦法能夠更簡單更精確地對其加以控制呢?

       實際上此處給出的解決方案十分簡單,就是一個很普通也很有用的 GLU 函數 gluUnProject()。

此函數的具體用途是將一個OpenGL視區內的二維點轉換為與其對應的情境中的三維座標。

轉換過程如所示(由點P在視窗中的XY座標得到其在三維空間中的全局座標):

       這個函數在glu.h中的原型定義如下:

int APIENTRY gluUnProject (

    GLdouble       winx,

    GLdouble       winy,

    GLdouble       winz,

    const GLdouble modelMatrix[16],

    const GLdouble projMatrix[16],

    const GLint    viewport[4],

    GLdouble       *objx,

    GLdouble       *objy,

    GLdouble       *objz);

其中前三個值表示視窗座標,中間三個分別為模型視圖矩陣(Model/View Matrix),投影矩陣(Projection Matrix)和視區(ViewPort),最後三個為輸出的全局座標值。

可能你會問:視窗座標不是只有X軸和Y軸兩個值麼,怎麼這裡還有Z值?這就要從二維空間與三維空間的關係說起了。

眾所周知,我們通過一個放置在三維世界中的攝像機,來觀察當前情境中的對象。通過使用諸如gluPerspective() 這樣的OpenGL函數,我們可以設定這個攝像機所能看到的視野的大小範圍。這個視野的邊界所圍成的幾何體是一個標準的平截頭體(Frustum),可以看做是金字塔狀的幾何體削去金字塔的上半部分後形成的一個台狀物,如果還原成金字塔狀,就得到了通常我們所說的視錐(View Frustum)這個視錐的錐頂就是視點(View Point)也就是攝像機所在的位置。平截頭體,視錐以及視點之間的關係,如所示:

在上面的圖中,遠裁剪面ABCD和近裁剪面A’B’C’D’構成了平截頭體,加上虛線部分就是視錐,頂點O就是攝像機所在的視點。我們在視窗中所能看到的東東,全部都在此平截頭體內。這跟前面的視窗座標Z值有什麼關係呢?看:

如此圖所示,點P和點P’分別在遠裁剪面ABCD和近裁剪面A’B’C’D’上。我們點擊螢幕上的點P,反映到視錐中,就是選中了所有的從點P到點P’的點。舉個形象的例子,這就像是我們挽弓放箭,如果射出去的箭近乎筆直地飛出(假設力量非常之大近乎無窮),從挽弓的地點直至擊中目標,在這條直線的軌跡上任何物體都將被一穿而過。對應這裡的情況,使用者單擊滑鼠獲得螢幕上的某一點,即是指定了從視點指向螢幕深處的某一方向,也就確定了螢幕上某條從O點出發的射線(在圖中即為OP)。在這裡,我們稱呼其為揀選射線。

因此,從視窗的XY座標,我們僅僅只能獲得一條出發自O點的揀選射線,並不能得到使用者想要的點在這條射線上的確切位置。

這時候視窗座標的Z值就能派上用場了。我們通過Z值,來指定我們想要的點在射線上的位置。假如使用者點擊了螢幕上的點(100,100)得到了這條射線OP,那麼我們傳入值1.0f就表示近裁剪面上的P點,而值0.0f則對應遠裁剪面上的P’點。

這樣,我們通過引入一個視窗座標的Z值,就能指定視錐內任意點的三維座標。與此同時,我們還解決了前面紅寶書給出的方法中存在的缺陷——同一位置上重疊物體的選擇問題。解決辦法是:從螢幕座標得到射線之後,分別讓重疊的物體與該射線求交,得到的交點,然後根據這些與視點的遠近確定選擇的對象。如此我們就不必受“僅僅只能選取螢幕中離觀察者最近的物體”的限制了。這樣一來,如果需要的話,我們甚至可以用代碼來作一定的限定,通過判斷交點與視點的距離,使得與該揀選射線相交的物體中,離視點遠的對象才能被選取,這樣就能夠對那些暫時被其他對象遮住的物體進行選取。

       至於如何求揀選射線與對象的交點,在各種圖形學的書中的數學部分均有講述,在此不再贅述。

      

四、常式(Sample Code Fragment)

      

       前面講述了RIP方法,現在我們來看如何編寫代碼以實現之,以及一些需要注意的問題。

       由於揀選射線以線段形式儲存更加便於後面的計算,況且我們可以直接得到縱跨整個平截頭體的線段(即前面圖中的線段PP’),故我們直接計算出這條串連遠近裁剪面的線段。我們將揀選射線的線段形式稱之為揀選線段。     

       在下面的代碼前方聲明有兩個類Point3f和LineSegment這分別表示由三個浮點數構成的三維空間中的點,以及由兩個點構成的空間中的一條線段。

       應注意代碼中用到了類Point3f的一個需要三個浮點參數的建構函式,以及類LineSegment的一個需要兩個點參數的建構函式。

       擷取揀選射線的常式如下所示(使用C++語言編寫):

class Point3f;

class LineSegment;

LineSegment GetSelectionRay(int mouse_x, int mouse_y) {

    // 擷取 Model-View、Projection 矩陣 & 擷取Viewport視區

    GLdouble    modelview[16];

    GLdouble    projection[16];

    GLint       viewport[4];

    glGetDoublev (GL_MODELVIEW_MATRIX, modelview);

    glGetDoublev (GL_PROJECTION_MATRIX, projection);

    glGetIntegerv (GL_VIEWPORT, viewport);

    GLdouble world_x, world_y, world_z; 

    // 擷取近裁剪面上的交點

    gluUnProject( (GLdouble) mouse_x, (GLdouble) mouse_y, 0.0,

                    modelview, projection, viewport,

                    &world_x, &world_y, &world_z);

    Point3f near_point(world_x, world_y, world_z);

    // 擷取遠裁剪面上的交點

    gluUnProject( (GLdouble) mouse_x, (GLdouble) mouse_y, 1.0,

                    modelview, projection, viewport,

                    &world_x, &world_y, &world_z);

    Point3f far_point(world_x, world_y, world_z);

    return LineSegment(near_point, far_point);

}

       如果你是使用Win32平台進行開發,那麼應當注意傳入正確的參數。因為無論是使用Win32 API 還是DirectInput 來擷取滑鼠座標,得到的Y值都應取反後再傳入。因為OpenGL預設的原點在視區的左下角,Y軸從左下角指向左上方,而Windows預設的原點在視窗的左上方,而Y軸方向與OpenGL相反,從左上方指向左下角。如所示:

0

X

Win32 預設視窗座標樣式

OpenGL 預設視窗座標樣式

0

X

Y

Y

我們可以看到代碼被注釋分為了三個部分:擷取當前矩陣及視區,擷取近裁剪面的交點,擷取遠裁剪面的交點。

我們通過OpenGL提供的查詢函數輕鬆得到當前的ModelView和Projection矩陣,以及當前的Viewport(視區,也就是視窗的用戶端地區,如果整個視窗地區用於OpenGL渲染的話)。

獲得兩個裁剪面上的交點的代碼基本上是一樣的,唯一的不同點是我們前面曾經詳細地討論過的視窗的Z座標。不錯,這個座標表示的就是“深淺”的概念。它的值從點P’到點P的變化是從0.0f逐漸增至1.0f。此處類似於OpenGL的深度測試機制。

在得到兩個交點之後,我們使用它們通過返回語句直接構建一條線段。在這裡僅僅作為執行個體代碼,故簡捷清晰地直接返回線段對象,而沒有通過引用參數來提高效率。

此時使用者可以使用這個函數來判斷所選擇的對象了。只需在需要的地方判斷對象是否與此線段相交即可判斷對象是否被選中,還可以通過進一步計算其交點位置來得到詳細的交點資訊。這些計算均是常見的電腦圖形學與三維數學計算,比如線段與三角形求交,線段與面求交,線段與球體求交,線段與柱體或錐體求交,等等。請參考所列出的電腦圖形學書籍。

五、結論(Conclusion)

      

       在本文中,我們介紹了一種行之有效三維座標拾取方法,主要使用GLU庫中的工具 + 生產力實現。這種方法速度快,效率高,能在不必重新繪製對象的前提下完成揀選工作。對比OpenGL內建的揀選機制來看,RIP的確在各種方面均有一定的優勢。 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.