使用OpenGL實(shí)現(xiàn)三維坐標(biāo)的鼠標(biāo)揀選
Implementation of RIP(Ray-Intersection-Penetration)
3D Coordinates Mouse Selection Using OpenGL
顧 露 (武漢理工大學(xué) 計(jì)算機(jī)系 中科院智能設(shè)計(jì)與智能制造研究所 湖北武漢 430070)
摘要(Abstract):
本文提出并實(shí)現(xiàn)一種用于三維坐標(biāo)揀選的RIP(Ray-Intersection-Penetration)方法。介紹了如何在已經(jīng)渲染至窗口的三維場(chǎng)景
中,使用鼠標(biāo)或者相關(guān)設(shè)備揀選特定三維對(duì)象的方法。此方法對(duì)于正交投影或透視投影均有效,相對(duì)于OpenGL自帶的選擇與反饋機(jī)制,本方法無(wú)論是揀選精度
還是算法實(shí)現(xiàn)效率均高出許多,是一種比較通用的解決方案。關(guān)鍵詞(Keywords) 正交投影(Ortho-Projection)、透視投影(Perspective-Projection)
世界坐標(biāo)系、屏幕坐標(biāo)系、三維揀選、OpenGL
一、簡(jiǎn)介(Introduction)
OpenGL是一種比較“純粹”的3D圖形API,一般僅用于三維圖形的渲染,對(duì)于特定領(lǐng)域的開(kāi)發(fā)者(如游戲開(kāi)發(fā)者)而言,如果選擇使用
OpenGL進(jìn)行開(kāi)發(fā),類(lèi)似碰撞檢測(cè)的機(jī)制就都需要自行編寫(xiě)了。但是由于鼠標(biāo)在圖形程序中的應(yīng)用非常非常之廣泛(例如現(xiàn)在已經(jīng)很少有PC游戲能完全地脫離
鼠標(biāo)),OpenGL在圖形庫(kù)的基礎(chǔ)上添加了選擇與反饋機(jī)制(Select &
Feedback)來(lái)滿(mǎn)足用戶(hù)使用鼠標(biāo)實(shí)時(shí)操作三維圖形的需要。但由于種種原因,我們需要更為特殊的選擇機(jī)制以滿(mǎn)足特定需求,在這里我們提出了一種簡(jiǎn)單迅
速的RIP(Ray-Intersection-Penetration)方法,可以滿(mǎn)足絕大多數(shù)典型應(yīng)用的需要。
二、相關(guān)研究(Related Work) 用過(guò)OpenGL選擇與反饋機(jī)制的開(kāi)發(fā)者,或多或少可能都會(huì)覺(jué)得它難以令人滿(mǎn)意。大致表現(xiàn)在下面幾個(gè)方面:
一、 編寫(xiě)程序比較繁瑣。
想要使用選擇反饋機(jī)制就需要切換渲染模式,操作命名堆棧,計(jì)算揀選矩陣,檢查選中記錄,這些繁瑣的步驟很容易出錯(cuò),而且非常不便于調(diào)試,只會(huì)降低工作效率和熱情。二、 只能做基于圖元的選定。
如
下圖(1 – a),使用GL_TRIANGLES繪制了一個(gè)三角形,三個(gè)頂點(diǎn)分別為
P1、P2和P3。若使用該機(jī)制,你將只能判斷是否在三維場(chǎng)景中選中了這個(gè)三角形(用戶(hù)點(diǎn)擊處是否在P1、P2和P3的范圍內(nèi)),而無(wú)法判斷用戶(hù)是點(diǎn)擊了
這個(gè)三角形哪一部分(是左邊的m區(qū)域內(nèi)還是右邊的n區(qū)域內(nèi)),因?yàn)樗L制的P1、P2和P3本身構(gòu)成的三角形就是一個(gè)基本圖元,對(duì)于揀選機(jī)制而言是不可分
的。當(dāng)然,把這個(gè)三角形拆成兩個(gè)三角形再分別進(jìn)行測(cè)試也是一個(gè)可行的方案,可是看看圖(1 – b),這可怎么拆呢?還有圖(1 –
c)呢?另外,如果n和m兩個(gè)平面不共面呢?對(duì)于使用者而言,OpenGL提供的揀選機(jī)制功能的確有限。

三、降低了渲染效率。
OpenGL
中的選擇和反饋是與普通渲染方式不同的一種特殊的渲染方式。我們使用時(shí)一般是先在幀緩存中渲染普通場(chǎng)景,然后進(jìn)入選擇模式重繪場(chǎng)景,此時(shí)幀緩存的內(nèi)容并無(wú)
變化。也就是說(shuō),為了選擇某些物體,我們需要在一幀中使用不同的渲染方式將其渲染兩遍。我們知道對(duì)對(duì)象進(jìn)行渲染是比較耗時(shí)的操作,當(dāng)場(chǎng)景中需要選擇的對(duì)象
多而雜的時(shí)候,采用這個(gè)機(jī)制是非常影響速度的。
另外在OpenGL紅寶書(shū)中介紹了一種簡(jiǎn)便易行的辦法:在后緩沖中使用不同的顏色重繪所有對(duì)象,每個(gè)對(duì)象用一個(gè)單色來(lái)標(biāo)示其顏色,這樣畫(huà)好之后我們讀取鼠
標(biāo)所在點(diǎn)的顏色,就能夠確定我們揀選了哪個(gè)物體。這種方法有一個(gè)缺陷,當(dāng)場(chǎng)景中需要選擇的對(duì)象的數(shù)目超出一定限度時(shí),可能會(huì)出現(xiàn)標(biāo)識(shí)數(shù)的溢出。對(duì)于這個(gè)問(wèn)
題,紅寶書(shū)給出的解決辦法就是多次掃描。實(shí)踐證明這種方法的確簡(jiǎn)便易行,但仍有不少局限性,而且做起來(lái)并不比第一種機(jī)制方便多少。限于篇幅,不再贅述。三、具體描述(Related Work) 看過(guò)了上面兩種方法,我們會(huì)發(fā)現(xiàn)這兩種方法都不是十分的方便,而且使用者不能對(duì)其進(jìn)行完全的控制,不能精確地判定鼠標(biāo)定位與實(shí)際的世界空間中三維坐標(biāo)的關(guān)系。那么有什么更好的辦法能夠更簡(jiǎn)單更精確地對(duì)其加以控制呢? 實(shí)際上此處給出的解決方案十分簡(jiǎn)單,就是一個(gè)很普通也很有用的 GLU 函數(shù) gluUnProject()。
此函數(shù)的具體用途是將一個(gè)OpenGL視區(qū)內(nèi)的二維點(diǎn)轉(zhuǎn)換為與其對(duì)應(yīng)的場(chǎng)景中的三維坐標(biāo)。
轉(zhuǎn)換過(guò)程如下圖所示(由點(diǎn)P在窗口中的XY坐標(biāo)得到其在三維空間中的世界坐標(biāo)):

這個(gè)函數(shù)在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); 其中前三個(gè)值表示窗口坐標(biāo),中間三個(gè)分別為模型視圖矩陣(Model/View Matrix),投影矩陣(Projection Matrix)和視區(qū)(ViewPort),最后三個(gè)為輸出的世界坐標(biāo)值?! 】赡苣銜?huì)問(wèn):窗口坐標(biāo)不是只有X軸和Y軸兩個(gè)值么,怎么這里還有Z值?這就要從二維空間與三維空間的關(guān)系說(shuō)起了?!?br> 眾所周知,我們通過(guò)一個(gè)放置在三維世界中的攝像機(jī),來(lái)觀察當(dāng)前場(chǎng)景中的對(duì)象。通過(guò)使用諸如gluPerspective()
這樣的OpenGL函數(shù),我們可以設(shè)置這個(gè)攝像機(jī)所能看到的視野的大小范圍。這個(gè)視野的邊界所圍成的幾何體是一個(gè)標(biāo)準(zhǔn)的平截頭體(Frustum),可以
看做是金字塔狀的幾何體削去金字塔的上半部分后形成的一個(gè)臺(tái)狀物,如果還原成金字塔狀,就得到了通常我們所說(shuō)的視錐(View
Frustum)這個(gè)視錐的錐頂就是視點(diǎn)(View Point)也就是攝像機(jī)所在的位置。平截頭體,視錐以及視點(diǎn)之間的關(guān)系,如下圖所示:

在上面的圖中,遠(yuǎn)裁剪面ABCD和近裁剪面A’B’C’D’構(gòu)成了平截頭體,加上虛線部分就是視錐,頂點(diǎn)O就是攝像機(jī)所在的視點(diǎn)。我們?cè)诖翱谥兴芸吹降臇|東,全部都在此平截頭體內(nèi)。這跟前面的窗口坐標(biāo)Z值有什么關(guān)系呢?看下圖

如
此圖所示,點(diǎn)P和點(diǎn)P’分別在遠(yuǎn)裁剪面ABCD和近裁剪面A’B’C’D’上。我們點(diǎn)擊屏幕上的點(diǎn)P,反映到視錐中,就是選中了所有的從點(diǎn)P到點(diǎn)P’的
點(diǎn)。舉個(gè)形象的例子,這就像是我們挽弓放箭,如果射出去的箭近乎筆直地飛出(假設(shè)力量非常之大近乎無(wú)窮),從挽弓的地點(diǎn)直至擊中目標(biāo),在這條直線的軌跡上
任何物體都將被一穿而過(guò)。對(duì)應(yīng)這里的情況,用戶(hù)單擊鼠標(biāo)獲得屏幕上的某一點(diǎn),即是指定了從視點(diǎn)指向屏幕深處的某一方向,也就確定了屏幕上某條從O點(diǎn)出發(fā)的
射線(在圖中即為OP)。在這里,我們稱(chēng)呼其為揀選射線。
因此,從窗口的XY坐標(biāo),我們僅僅只能獲得一條出發(fā)自O(shè)點(diǎn)的揀選射線,并不能得到用戶(hù)想要的點(diǎn)在這條射線上的確切位置。
這時(shí)候窗口坐標(biāo)的Z值就能派上用場(chǎng)了。我們通過(guò)Z值,來(lái)指定我們想要的點(diǎn)在射線上的位置。假如用戶(hù)點(diǎn)擊了屏幕上的點(diǎn)(100,100)得到了這條射線OP,那么我們傳入值1.0f就表示近裁剪面上的P點(diǎn),而值0.0f則對(duì)應(yīng)遠(yuǎn)裁剪面上的P’點(diǎn)。
這
樣,我們通過(guò)引入一個(gè)窗口坐標(biāo)的Z值,就能指定視錐內(nèi)任意點(diǎn)的三維坐標(biāo)。與此同時(shí),我們還解決了前面紅寶書(shū)給出的方法中存在的缺陷——同一位置上重疊物體
的選擇問(wèn)題。解決辦法是:從屏幕坐標(biāo)得到射線之后,分別讓重疊的物體與該射線求交,得到的交點(diǎn),然后根據(jù)這些與視點(diǎn)的遠(yuǎn)近確定選擇的對(duì)象。如此我們就不必
受“僅僅只能選取屏幕中離觀察者最近的物體”的限制了。這樣一來(lái),如果需要的話(huà),我們甚至可以用代碼來(lái)作一定的限定,通過(guò)判斷交點(diǎn)與視點(diǎn)的距離,使得與該
揀選射線相交的物體中,離視點(diǎn)遠(yuǎn)的對(duì)象才能被選取,這樣就能夠?qū)δ切簳r(shí)被其他對(duì)象遮住的物體進(jìn)行選取。
至于如何求揀選射線與對(duì)象的交點(diǎn),在各種圖形學(xué)的書(shū)中的數(shù)學(xué)部分均有講述,在此不再贅述。
四、例程(Sample Code Fragment)
前面講述了RIP方法,現(xiàn)在我們來(lái)看如何編寫(xiě)代碼以實(shí)現(xiàn)之,以及一些需要注意的問(wèn)題。
由于揀選射線以線段形式存儲(chǔ)更加便于后面的計(jì)算,況且我們可以直接得到縱跨整個(gè)平截頭體的線段(即前面圖中的線段PP’),故我們直接計(jì)算出這條連接遠(yuǎn)近裁剪面的線段。我們將揀選射線的線段形式稱(chēng)之為揀選線段。
在下面的代碼前方聲明有兩個(gè)類(lèi)Point3f和LineSegment這分別表示由三個(gè)浮點(diǎn)數(shù)構(gòu)成的三維空間中的點(diǎn),以及由兩個(gè)點(diǎn)構(gòu)成的空間中的一條線段。
應(yīng)注意代碼中用到了類(lèi)Point3f的一個(gè)需要三個(gè)浮點(diǎn)參數(shù)的構(gòu)造函數(shù),以及類(lèi)LineSegment的一個(gè)需要兩個(gè)點(diǎn)參數(shù)的構(gòu)造函數(shù)。
獲取揀選射線的例程如下所示(使用C++語(yǔ)言編寫(xiě)):class Point3f;
class LineSegment;
LineSegment GetSelectionRay(int mouse_x, int mouse_y) {
// 獲取 Model-View、Projection 矩陣 & 獲取Viewport視區(qū)
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; // 獲取近裁剪面上的交點(diǎn)
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); // 獲取遠(yuǎn)裁剪面上的交點(diǎn)
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平臺(tái)進(jìn)行開(kāi)發(fā),那么應(yīng)當(dāng)注意傳入正確的參數(shù)。因?yàn)闊o(wú)論是使用Win32 API 還是DirectInput
來(lái)獲取鼠標(biāo)坐標(biāo),得到的Y值都應(yīng)取反后再傳入。因?yàn)镺penGL默認(rèn)的原點(diǎn)在視區(qū)的左下角,Y軸從左下角指向左上角,而Windows默認(rèn)的原點(diǎn)在窗口的
左上角,而Y軸方向與OpenGL相反,從左上角指向左下角。如下圖所示:

我們可以看到代碼被注釋分為了三個(gè)部分:獲取當(dāng)前矩陣及視區(qū),獲取近裁剪面的交點(diǎn),獲取遠(yuǎn)裁剪面的交點(diǎn)。
我們通過(guò)OpenGL提供的查詢(xún)函數(shù)輕松得到當(dāng)前的ModelView和Projection矩陣,以及當(dāng)前的Viewport(視區(qū),也就是窗口的客戶(hù)端區(qū)域,如果整個(gè)窗口區(qū)域用于OpenGL渲染的話(huà))。
獲得兩個(gè)裁剪面上的交點(diǎn)的代碼基本上是一樣的,唯一的不同點(diǎn)是我們前面曾經(jīng)詳細(xì)地討論過(guò)的窗口的Z坐標(biāo)。不錯(cuò),這個(gè)坐標(biāo)表示的就是“深淺”的概念。它的值從點(diǎn)P’到點(diǎn)P的變化是從0.0f逐漸增至1.0f。此處類(lèi)似于OpenGL的深度測(cè)試機(jī)制。
在得到兩個(gè)交點(diǎn)之后,我們使用它們通過(guò)返回語(yǔ)句直接構(gòu)建一條線段。在這里僅僅作為實(shí)例代碼,故簡(jiǎn)捷清晰地直接返回線段對(duì)象,而沒(méi)有通過(guò)引用參數(shù)來(lái)提高效率。
此
時(shí)用戶(hù)可以使用這個(gè)函數(shù)來(lái)判斷所選擇的對(duì)象了。只需在需要的地方判斷對(duì)象是否與此線段相交即可判斷對(duì)象是否被選中,還可以通過(guò)進(jìn)一步計(jì)算其交點(diǎn)位置來(lái)得到
詳細(xì)的交點(diǎn)信息。這些計(jì)算均是常見(jiàn)的計(jì)算機(jī)圖形學(xué)與三維數(shù)學(xué)計(jì)算,比如線段與三角形求交,線段與面求交,線段與球體求交,線段與柱體或錐體求交,等等。請(qǐng)
參考所列出的計(jì)算機(jī)圖形學(xué)書(shū)籍。
五、結(jié)論(Conclusion)
在本文中,我們介紹了一種行之有效的三維坐標(biāo)拾取方法,主要使用GLU庫(kù)中的實(shí)用工具實(shí)現(xiàn)。這種方法速度快,效率高,能在不必重新繪制對(duì)象的前提下完成揀選工作。對(duì)比OpenGL自帶的揀選機(jī)制來(lái)看,RIP的確在各種方面均有一定的優(yōu)勢(shì)。
六、參考文獻(xiàn)(Reference) 【1】《OpenGL Programming Guide》
OpenGL ARB Mason Woo, Jackie Heider, Tom Davis, Dave Shreiner
【2】《OpenGL Reference Manual》
OpenGL ARB
【3】《Computer Graphics》
Donald Heam, M. Pauline Baker
【4】《Computer Graphics using OpenGL 2nd Edition》
F.S. Hill, JR.
posted on 2010-06-05 18:45
風(fēng)輕云淡 閱讀(3050)
評(píng)論(0) 編輯 收藏 引用 所屬分類(lèi):
OpenGL