深入了解OpenGL拾取模式(OpenGL Picking)
在用OpenGL進行圖形程式設計的時候,通常要用滑鼠進行互動操作,比如用滑鼠點選擇畫面中的物體,我們稱之為拾取(Picking),在網上看了很多OpenGL拾取的文章,但大多是隻是介紹在OpenGL中如何拾取,如何利用OpenGL提供的一系列函數來完成拾取,最多再簡單介紹下OpenGL的名字棧(Name stack),拾取矩陣(Picking Matrix)等等,但是拾取真正的原理确沒有提到。是以,我在這裡為大家詳細介紹下OpenGL中拾取是怎樣實作的,以及其背後的真正原理。
OpenGL中的拾取是對OpenGL圖形管線的一個應用。是以OpenGL中的拾取并不是像D3D一樣采用射線交叉測試來判斷是否選中一個目标,而是在圖形管線的投影變換(Projection Transformation)階段利用拾取矩陣來實作的。為了了解這個過程,先來複習一下OpenGL的圖形管線。

總的來說,OpenGL圖形管線大體分為上面的五個階段。在程式設計的時候使用glMatrixMode(GL_MODELVIEW),或者 glMatrixMode(GL_PROJECTION)就是告訴OpenGL我們是要在那個階段進行操作。先來看看投影變換,因為了解投影變換是了解OpenGL拾取的前提條件。為了簡單起見,這裡以正交投影(Orthogonal Projection)為例。在OpenGL中,使用正交投影可以調用glOrtho (left, right, bottom, top, zNear, zFar),其中的六個參數分别對應正交投影視體的六個平面到觀察坐标系原點的距離。一旦在程式中調用了這個函數,OpenGL會馬上建立根據給定的六個參數建立一個視體,并且把視體的大小歸一化到-1到1之間,也就是說,OpenGL會自動把你給的參數所對應的x,y,z值轉換為-1到1之間的值,并且這個視體的中心就是觀察坐标系的原點。要注意的是,當視體歸一化後,z軸的方向要反向,也就是說,這裡OpenGL的右手坐标系要換成左手坐标系。原因很簡單,z軸朝向顯示器裡的方向更符合我們的常識,越向裡就離我們越遠,z的值也就越大。
當我們調用了glOrtho()這個函數後,OpenGL會建立一個矩陣,也就是投影矩陣。這個矩陣可以分解為三個步驟,首先将我們設定的視體移動到觀察坐标是的原點,然後在縮放為邊長為2的機關視體。因為轉化後的視體坐标都在-1和1之間,是以視體的邊長就是2。然後再對z進行反方向。最後的投影矩陣我們用
來表示的話,那麼有
上面的矩陣雖然看起來很複雜,其實很簡單。它就是進行移動,縮放,反号三個操作而已。現在我們在OpenGL中檢查一下是不是進行了這樣的操作。添加下面的代碼。
view plain copy to clipboard print ?
- glMatrixMode(GL_PROJECTION);
- glLoadIdentity();
- glOrtho(-10, 10, -10, 10, -10, 10);
- GLfloat m[16];
- glGetFloatv(GL_PROJECTION_MATRIX, m);
首先進入投影變換階段,然後我們使用glLoadIdentity()在矩陣棧中存入機關矩陣。設定視體為邊長為20的正方體。這裡我們把glortho中的6個參數帶入上面公式計算,得到的結果為
代碼中我們使用了glGetFloatv來獲得目前操作矩陣,然後設定斷點來檢查一下。
可以看到,得到的資料和我們計算的一樣。說明OpenGL的确是建立了這樣的矩陣來進行計算。
弄清楚了OpenGL中的投影變換,現在就開看看大家關心的拾取操作。OpenGL的拾取就是利用投影變換中歸一化視體這個操作來實作的。拾取的時候,我們可以想象想用一個方框來選擇我們要選擇的物體。比如一個邊長為2的正方形,我們用滑鼠在視窗上點選的時候,一旦點到一個位置,那麼就在這個位置生成一個邊長為2的正方形,那麼正方形内包圍的物體就是我們要選擇的物體,如果這個正方形内沒有包圍任何東西,那麼就說明什麼都沒有選擇到。這個過程就和我們歸一化投影,然後再剪裁的過程是一樣的。OpenGL會自動剪裁掉在歸一化視體之外的物體,那麼如果我們把選擇物體用的方框轉換為用投影時的視體,那麼在這個方框外的東西,也就是我們沒有選擇的東西,OpenGL會自動的為我們扔掉。是以OpenGL提供了選擇模式glRenderMode(GL_SELECT),當我們進行拾取前進入這個模式,然後設定好我們的選擇框的大小,再為我們要選擇的物體設定好名字,也就是我們說的名字棧。接下來的操作和投影變換就一樣了,先把這個選擇框移歸一化為邊長為-1到1的正方體,然後移動到原點,最後放大為我們視窗的大小。(這時OpenGL已經把在選擇框外的東西剪裁掉了,如果這個時候我們顯示投影矩陣中的内容的話,就會隻看到我們選擇到的東西,并且放大和視窗一樣大。)然後OpenGL會把選中的物體資訊記錄在一個叫做SelectBuffer的緩沖中,這個緩沖就是個一維數組,裡面儲存了名字棧中名字的個數,選擇到的物體的最小最大深度值,也就是z的值,這個值是個0到1之間的值,也就是裡我們最近的為0,最遠的為1。selectBuffer是個整型的數組,是以這裡儲存的深度值是乘以0xFFFFFFFF後的值。當然最重要的,其中還儲存了我們選擇到的物體的名字,這樣隻要在程式中判斷選擇到物體的名字,我們就可以判斷是不是選擇到了要選擇的物體了。整個拾取的過程可如下。
上圖中左邊的正方體是我們歸一化的視體,拾取的時候就是在這個空間中拾取的。紅色的小框是我們的選擇框。裡面的紅色就是我們選擇到的物體的一部分。現在要做的就是把這個小框轉變為視體,這樣OpenGL才能為我們把不要的東西扔掉。是以,首先還是把這個小框移動到觀察坐标系的原點,然後再放大為我們歸一化視體的大小,這樣整個視體中就隻有我們選中的東西了,上圖中間顯示了這個過程。視體外的東西已經被OpenGL剪裁掉,選中的記錄會儲存到selectbuffer中。因為這些操作是在選擇模式下完成的,是以看不到我們選擇的過程,但是如果我們把選擇的過程顯示出來的話,就會看到上圖右邊的樣子。整個視窗就鋪滿了我們選擇的部分。在OpenGL中,提供了這個設定拾取框的函數。
gluPickMatrix (x, y, width, height, viewport[4]);
其中x,y是滑鼠點選到視窗上的坐标,width和height就是這個拾取框的長寬,viewport是為了得到我們視窗的大小。一但調用了該函數,OpenGL就會建立一個拾取矩陣,分解這個矩陣的話,可以看到,這個矩陣就是上面的移動拾取框到原點,然後再放大為視體大小這兩個步驟。是以即使我們不使用這個函數,也可以自己計算出這個拾取矩陣
同樣,我們還是在OpenGL中檢查一下,是不是做了這樣的操作。在OpenGL中添加下面的代碼。
view plain copy to clipboard print ?
- void SelectObject(GLint x, GLint y)
- {
- GLuint selectBuff[32]={0};//建立一個儲存選擇結果的數組
- GLint hits, viewport[4];
- glGetIntegerv(GL_VIEWPORT, viewport); //獲得viewport
- glSelectBuffer(64, selectBuff); //告訴OpenGL初始化selectbuffer
- glRenderMode(GL_SELECT); //進入選擇模式
- glInitNames(); //初始化名字棧
- glPushName(0); //在名字棧中放入一個初始化名字,這裡為‘0’
- glMatrixMode(GL_PROJECTION); //進入投影階段準備拾取
- glPushMatrix(); //儲存以前的投影矩陣
- glLoadIdentity(); //載入機關矩陣
- float m[16];
- glGetFloatv(GL_PROJECTION_MATRIX, m);
- gluPickMatrix( x, // 設定我們選擇框的大小,建立拾取矩陣,就是上面的公式
- viewport[3]-y, // viewport[3]儲存的是視窗的高度,視窗坐标轉換為OpenGL坐标
- 2,2, // 選擇框的大小為2,2
- viewport // 視口資訊,包括視口的起始位置和大小
- );
- glGetFloatv(GL_PROJECTION_MATRIX, m);
- glOrtho(-10, 10, -10, 10, -10, 10); //拾取矩陣乘以投影矩陣,這樣就可以讓選擇框放大為和視體一樣大
- glGetFloatv(GL_PROJECTION_MATRIX, m);
- draw(GL_SELECT); // 該函數中渲染物體,并且給物體設定名字
- glMatrixMode(GL_PROJECTION);
- glPopMatrix(); // 傳回正常的投影變換
- glGetFloatv(GL_PROJECTION_MATRIX, m);
- hits = glRenderMode(GL_RENDER); // 從選擇模式傳回正常模式,該函數傳回選擇到對象的個數
- if(hits > 0)
- processSelect(selectBuff); // 選擇結果處理
- }
view plain copy to clipboard print ?
- void draw(GLenum model=GL_RENDER)
- {
- if(model==GL_SELECT)
- {
- glColor3f(1.0,0.0,0.0);
- glLoadName(100);
- glPushMatrix();
- glTranslatef(-5, 0.0, 10.0);
- glBegin(GL_QUADS);
- glVertex3f(-1, -1, 0);
- glVertex3f( 1, -1, 0);
- glVertex3f( 1, 1, 0);
- glVertex3f(-1, 1, 0);
- glEnd();
- glPopMatrix();
- glColor3f(0.0,0.0,1.0);
- glLoadName(101);
- glPushMatrix();
- glTranslatef(5, 0.0, -10.0);
- glBegin(GL_QUADS);
- glVertex3f(-1, -1, 0);
- glVertex3f( 1, -1, 0);
- glVertex3f( 1, 1, 0);
- glVertex3f(-1, 1, 0);
- glEnd();
- glPopMatrix();
- }
- else
- {
- glColor3f(1.0,0.0,0.0);
- glPushMatrix();
- glTranslatef(-5, 0.0, -5.0);
- glBegin(GL_QUADS);
- glVertex3f(-1, -1, 0);
- glVertex3f( 1, -1, 0);
- glVertex3f( 1, 1, 0);
- glVertex3f(-1, 1, 0);
- glEnd();
- glPopMatrix();
- glColor3f(0.0,0.0,1.0);
- glPushMatrix();
- glTranslatef(5, 0.0, -10.0);
- glBegin(GL_QUADS);
- glVertex3f(-1, -1, 0);
- glVertex3f( 1, -1, 0);
- glVertex3f( 1, 1, 0);
- glVertex3f(-1, 1, 0);
- glEnd();
- glPopMatrix();
- }
- }
然後設點斷點來檢查一下。
上面看到我們的視口的起始位置就是視窗的原點,大小和視窗的大小一樣,500×500。然後在gluPickMatrix( x, viewport[3]-y, 2,2, viewport )函數調用後,會馬上建立一個拾取矩陣,根據提供的參數,在螢幕上點選的坐标為 x=136,y=261,這是螢幕坐标,轉換成openGL坐标為x=136,y=500-261=239。拾取框的另一個坐标就是138,242,因為這個給的長,寬都是2。現在就得到了拾取框的2個坐标了,
,然後把這2個坐标再轉換為-1到1之間的坐标得到
把這個坐标帶入拾取矩陣中計算,可以得到
然後,在程式中設定斷點,擷取目前操作矩陣檢查一下。
從上面可以看到,和我們計算的結果是一樣的。在設定了拾取矩陣後,又調用了語句glOrtho(-10, 10, -10, 10, -10, 10),這表示讓這個拾取框的大小鋪滿我們整個視窗,OpenGL會把剛才得到的拾取矩陣再乘以這個投影矩陣,我們可以得到
再在程式中設定斷點來檢查一下。
的确和我們計算的結果一樣。然後代碼中使用了glPopMatrix(),由于我們的拾取操作已經完成,到裡位置,拾取的結果資訊已經被OpenGL儲存到了selectbuffer中了,是以這時我們就要不在需要這個拾取矩陣,由于之前使用了glPushMatrix()儲存了原來的投影矩陣,現在隻要glPopMatrix()就可以了。glPopMatrix()後,我們可以再設定斷點看一下是不是真的傳回到原來的投影矩陣。
同樣我們可以從上圖中看到,的确在PopMatrix後,傳回了原來的投影變換,是以現在從選擇模式傳回到正常模式的時候我們就可以看到正常的畫面。
上圖中可以看到紅色正方形中有個白色的小方框,這個就是在設定拾取矩陣時的拾取框,拾取框在紅色正方形内部,說明我們已經選中了紅色的正方形了。如果這個時候把選擇框中的東西顯示出來的話,就可以看到我們選擇到的部分鋪滿整個螢幕。
在前面的代碼中,我們已經為紅色正方形命名為100,藍色的為101,現在我們來看看selectbuffer裡被選中的物體。
可以看到數組的前4個部分有值,第一個表示被選中物體的個數,當然現在是1。第二個表示和第三個表示物體的最小深度值和最大深度值,由于物體是個平面,是以這兩個值是一樣的。這裡的深度值是整數,除以0xffffffff以後就得到了0到1之間的深度值了。第四個值,也就是我們選擇到的物體的名字。這裡就是紅色的正方形。
OpenGL的整個拾取過程就是這樣的。它是利用了圖形管線中投影變換階段來實作拾取操作的。對于OpenGL圖形管線不了解的朋友可能對這種方法會感到困難。但是一旦了解了圖形管線後,再來了解拾取就很容易了。
原文位址:http://blog.csdn.net/zhangci226/archive/2009/10/30/4749526.aspx