深入理解OpenGL拾取模式

本文轉自:http://blog.csdn.net/zhangci226/article/details/4749526

 

在用OpenGL進行圖形編程的時候,通常要用鼠標進行交互操作,比如用鼠標點選擇畫面中的物體,我們稱之爲拾取(Picking),在網上看了很多OpenGL拾取的文章,但大多是隻是介紹在OpenGL中如何拾取,如何利用OpenGL提供的一系列函數來完成拾取,最多再簡單介紹下OpenGL的名字棧(Name stack),拾取矩陣(Picking Matrix)等等,但是拾取真正的原理確沒有提到。所以,我在這裏爲大家詳細介紹下OpenGL中拾取是怎樣實現的,以及其背後的真正原理。

OpenGL中的拾取是對OpenGL圖形管線的一個應用。所以OpenGL中的拾取並不是像D3D一樣採用射線交叉測試來判斷是否選中一個目標,而是在圖形管線投影變換(Projection Transformation)階段利用拾取矩陣來實現的。爲了理解這個過程,先來複習一下OpenGL的圖形管線。image

總的來說,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的值也就越大。

image

當我們調用了glOrtho()這個函數後,OpenGL會建立一個矩陣,也就是投影矩陣。這個矩陣可以分解爲三個步驟,首先將我們設置的視體移動到觀察座標是的原點,然後在縮放爲邊長爲2的單位視體。因爲轉化後的視體座標都在-1和1之間,所以視體的邊長就是2。然後再對z進行反方向。最後的投影矩陣我們用 來表示的話,那麼有

e

上面的矩陣雖然看起來很複雜,其實很簡單。它就是進行移動,縮放,反號三個操作而已。現在我們在OpenGL中檢查一下是不是進行了這樣的操作。添加下面的代碼。

  1. glMatrixMode(GL_PROJECTION);    
  2.   
  3. glLoadIdentity();    
  4.   
  5. glOrtho(-10, 10, -10, 10, -10, 10);    
  6.   
  7.         
  8.   
  9. GLfloat m[16];    
  10.   
  11. glGetFloatv(GL_PROJECTION_MATRIX, m);    

首先進入投影變換階段,然後我們使用glLoadIdentity()在矩陣棧中存入單位矩陣。設置視體爲邊長爲20的正方體。這裏我們把glortho中的6個參數帶入上面公式計算,得到的結果爲

d

代碼中我們使用了glGetFloatv來獲得當前操作矩陣,然後設定斷點來檢查一下。

image

可以看到,得到的數據和我們計算的一樣。說明OpenGL的確是創建了這樣的矩陣來進行計算。弄清楚了OpenGL中的投影變換,現在就開看看大家關心的拾取操作。OpenGL的拾取就是利用投影變換中歸一化視體這個操作來實現的。拾取的時候,我們可以想象想用一個方框來選擇我們要選擇的物體。比如一個邊長爲2的正方形,我們用鼠標在窗口上點擊的時候,一旦點到一個位置,那麼就在這個位置生成一個邊長爲2的正方形,那麼正方形內包圍的物體就是我們要選擇的物體,如果這個正方形內沒有包圍任何東西,那麼就說明什麼都沒有選擇到。這個過程就和我們歸一化投影,然後再剪裁的過程是一樣的。OpenGL會自動剪裁掉在歸一化視體之外的物體,那麼如果我們把選擇物體用的方框轉換爲用投影時的視體,那麼在這個方框外的東西,也就是我們沒有選擇的東西,OpenGL會自動的爲我們扔掉。所以OpenGL提供了選擇模式glRenderMode(GL_SELECT),當我們進行拾取前進入這個模式,然後設定好我們的選擇框的大小,再爲我們要選擇的物體設定好名字,也就是我們說的名字棧。接下來的操作和投影變換就一樣了,先把這個選擇框移歸一化爲邊長爲-1到1的正方體,然後移動到原點,最後放大爲我們窗口的大小。(這時OpenGL已經把在選擇框外的東西剪裁掉了,如果這個時候我們顯示投影矩陣中的內容的話,就會只看到我們選擇到的東西,並且放大和窗口一樣大。)然後OpenGL會把選中的物體信息記錄在一個叫做SelectBuffer的緩衝中,這個緩衝就是個一維數組,裏面保存了名字棧中名字的個數,選擇到的物體的最小最大深度值,也就是z的值,這個值是個0到1之間的值,也就是裏我們最近的爲0,最遠的爲1。selectBuffer是個整型的數組,所以這裏保存的深度值是乘以0xFFFFFFFF後的值。當然最重要的,其中還保存了我們選擇到的物體的名字,這樣只要在程序中判斷選擇到物體的名字,我們就可以判斷是不是選擇到了要選擇的物體了。整個拾取的過程可如下。

image

上圖中左邊的正方體是我們歸一化的視體,拾取的時候就是在這個空間中拾取的。紅色的小框是我們的選擇框。裏面的紅色就是我們選擇到的物體的一部分。現在要做的就是把這個小框轉變爲視體,這樣OpenGL才能爲我們把不要的東西扔掉。所以,首先還是把這個小框移動到觀察座標系的原點,然後再放大爲我們歸一化視體的大小,這樣整個視體中就只有我們選中的東西了,上圖中間顯示了這個過程。視體外的東西已經被OpenGL剪裁掉,選中的記錄會保存到selectbuffer中。因爲這些操作是在選擇模式下完成的,所以看不到我們選擇的過程,但是如果我們把選擇的過程顯示出來的話,就會看到上圖右邊的樣子。整個窗口就鋪滿了我們選擇的部分。在OpenGL中,提供了這個設置拾取框的函數。

gluPickMatrix (x, y, width, height, viewport[4]);

其中x,y是鼠標點擊到窗口上的座標width和height就是這個拾取框的長寬viewport是爲了得到我們窗口的大小。一但調用了該函數,OpenGL就會創建一個拾取矩陣,分解這個矩陣的話,可以看到,這個矩陣就是上面的移動拾取框到原點,然後再放大爲視體大小這兩個步驟。所以即使我們不使用這個函數,也可以自己計算出這個拾取矩陣.

p

同樣,我們還是在OpenGL中檢查一下,是不是做了這樣的操作。在OpenGL中添加下面的代碼。

  1. void SelectObject(GLint x, GLint y)    
  2.   
  3. {    
  4.   
  5.   GLuint selectBuff[32]={0};//創建一個保存選擇結果的數組    
  6.   
  7.   GLint hits, viewport[4];      
  8.   
  9.   
  10.   
  11.   glGetIntegerv(GL_VIEWPORT, viewport); //獲得viewport    
  12.   
  13.   glSelectBuffer(64, selectBuff); //告訴OpenGL初始化  selectbuffer    
  14.   
  15.   glRenderMode(GL_SELECT);    //進入選擇模式    
  16.   
  17.   
  18.   
  19.   glInitNames();  //初始化名字棧    
  20.   
  21.   glPushName(0);  //在名字棧中放入一個初始化名字,這裏爲‘0’    
  22.   
  23.   
  24.   
  25.   glMatrixMode(GL_PROJECTION);    //進入投影階段準備拾取    
  26.   
  27.   glPushMatrix();     //保存以前的投影矩陣    
  28.   
  29.   glLoadIdentity();   //載入單位矩陣    
  30.   
  31.   
  32.   
  33.   float m[16];    
  34.   
  35.   glGetFloatv(GL_PROJECTION_MATRIX, m);    
  36.   
  37.   
  38.   
  39.   gluPickMatrix( x,           // 設定我們選擇框的大小,建立拾取矩陣,就是上面的公式  viewport[3]-y,    // viewport[3]保存的是窗口的高度,窗口座標轉換爲OpenGL座標    
  40.   
  41.    2,2,              // 選擇框的大小爲2,2    
  42.   
  43.    viewport          // 視口信息,包括視口的起始位置和大小    
  44.   
  45. );        
  46.   
  47.                                                         
  48.   
  49.     glGetFloatv(GL_PROJECTION_MATRIX, m);    
  50.   
  51. glOrtho(-10, 10, -10, 10, -10, 10);     //拾取矩陣乘以投影矩陣,這樣就可以讓選擇框放大爲和視體一樣大    
  52.   
  53.     glGetFloatv(GL_PROJECTION_MATRIX, m);    
  54.   
  55.   
  56.   
  57.     draw(GL_SELECT);    // 該函數中渲染物體,並且給物體設定名字    
  58.   
  59.         
  60.   
  61.     glMatrixMode(GL_PROJECTION);    
  62.   
  63.     glPopMatrix();  // 返回正常的投影變換    
  64.   
  65.     glGetFloatv(GL_PROJECTION_MATRIX, m);    
  66.   
  67.     hits = glRenderMode(GL_RENDER); // 從選擇模式返回正常模式,該函數返回選擇到對象的個數    
  68.   
  69.     if(hits > 0)    
  70.   
  71.         processSelect(selectBuff);  //  選擇結果處理    
  72.   
  73.     }    
  74.   
  75.   
  76.   
  77.     void draw(GLenum model=GL_RENDER)    
  78.   
  79.     {    
  80.   
  81.         if(model==GL_SELECT)    
  82.   
  83.         {    
  84.   
  85.             glColor3f(1.0,0.0,0.0);    
  86.   
  87.             glLoadName(100);    
  88.   
  89.             glPushMatrix();    
  90.   
  91.         glTranslatef(-5, 0.0, 10.0);    
  92.   
  93.             glBegin(GL_QUADS);    
  94.   
  95.                 glVertex3f(-1, -1, 0);    
  96.   
  97.                 glVertex3f( 1, -1, 0);    
  98.   
  99.             glVertex3f( 1, 1, 0);    
  100.   
  101.                 glVertex3f(-1, 1, 0);    
  102.   
  103.             glEnd();    
  104.   
  105.             glPopMatrix();    
  106.   
  107.     
  108.   
  109.             glColor3f(0.0,0.0,1.0);    
  110.   
  111.             glLoadName(101);    
  112.   
  113.        glPushMatrix();    
  114.   
  115.             glTranslatef(5, 0.0, -10.0);    
  116.   
  117.         glBegin(GL_QUADS);    
  118.   
  119.            glVertex3f(-1, -1, 0);    
  120.   
  121.             glVertex3f( 1, -1, 0);    
  122.   
  123.            glVertex3f( 1, 1, 0);    
  124.   
  125.             glVertex3f(-1, 1, 0);    
  126.   
  127.         glEnd();    
  128.   
  129.             glPopMatrix();    
  130.   
  131.         }    
  132.   
  133.         else    
  134.   
  135.     {    
  136.   
  137.         glColor3f(1.0,0.0,0.0);    
  138.   
  139.         glPushMatrix();    
  140.   
  141.             glTranslatef(-5, 0.0, -5.0);    
  142.   
  143.             glBegin(GL_QUADS);    
  144.   
  145.             glVertex3f(-1, -1, 0);    
  146.   
  147.             glVertex3f( 1, -1, 0);    
  148.   
  149.             glVertex3f( 1, 1, 0);    
  150.   
  151.             glVertex3f(-1, 1, 0);    
  152.   
  153.         glEnd();    
  154.   
  155.             glPopMatrix();    
  156.   
  157.         
  158.   
  159.             glColor3f(0.0,0.0,1.0);    
  160.   
  161.         glPushMatrix();    
  162.   
  163.             glTranslatef(5, 0.0, -10.0);    
  164.   
  165.             glBegin(GL_QUADS);    
  166.   
  167.                 glVertex3f(-1, -1, 0);    
  168.   
  169.             glVertex3f( 1, -1, 0);    
  170.   
  171.             glVertex3f( 1, 1, 0);    
  172.   
  173.             glVertex3f(-1, 1, 0);    
  174.   
  175.             glEnd();    
  176.   
  177.         glPopMatrix();    
  178.   
  179.         }    
  180.   
  181. }   

然後設點斷點來檢查一下。

image

上面看到我們的視口的起始位置就是窗口的原點,大小和窗口的大小一樣,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之間的座標得到

image

把這個座標帶入拾取矩陣中計算,可以得到

gif.latex

然後,在程序中設置斷點,獲取當前操作矩陣檢查一下。

image

從上面可以看到,和我們計算的結果是一樣的。在設置了拾取矩陣後,又調用了語句glOrtho(-10, 10, -10, 10, -10, 10),這表示讓這個拾取框的大小鋪滿我們整個窗口,OpenGL會把剛纔得到的拾取矩陣再乘以這個投影矩陣,我們可以得到

F

再在程序中設置斷點來檢查一下。

image

的確和我們計算的結果一樣。然後代碼中使用了glPopMatrix(),由於我們的拾取操作已經完成,到裏位置,拾取的結果信息已經被OpenGL保存到了selectbuffer中了,所以這時我們就要不在需要這個拾取矩陣,由於之前使用了glPushMatrix()保存了原來的投影矩陣,現在只要glPopMatrix()就可以了。glPopMatrix()後,我們可以再設置斷點看一下是不是真的返回到原來的投影矩陣。

image

同樣我們可以從上圖中看到,的確在PopMatrix後,返回了原來的投影變換,所以現在從選擇模式返回到正常模式的時候我們就可以看到正常的畫面。

image

上圖中可以看到紅色正方形中有個白色的小方框,這個就是在設置拾取矩陣時的拾取框,拾取框在紅色正方形內部,說明我們已經選中了紅色的正方形了。如果這個時候把選擇框中的東西顯示出來的話,就可以看到我們選擇到的部分鋪滿整個屏幕。

image

在前面的代碼中,我們已經爲紅色正方形命名爲100,藍色的爲101,現在我們來看看selectbuffer裏被選中的物體。

image 可以看到數組的前4個部分有值,第一個表示被選中物體的個數,當然現在是1。第二個表示和第三個表示物體的最小深度值和最大深度值,由於物體是個平面,所以這兩個值是一樣的。這裏的深度值是整數,除以0xffffffff以後就得到了0到1之間的深度值了。第四個值,也就是我們選擇到的物體的名字。這裏就是紅色的正方形。

OpenGL的整個拾取過程就是這樣的。它是利用了圖形管線中投影變換階段來實現拾取操作的。對於OpenGL圖形管線不瞭解的朋友可能對這種方法會感到困難。但是一旦理解了圖形管線後,再來理解拾取就很容易了。

發佈了68 篇原創文章 · 獲贊 15 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章