【轉】OpenGL入門學習[十五]

這次講的所有內容都裝在一個立方體中,呵呵。
呵呵,繪製一個立方體,簡單呀,我們學了第一課第二課,早就會了。
先彆着急,立方體是很簡單,但是這裏只是拿立方體做一個例子,來說明OpenGL在繪製方法上的改進。
一個立方體有六個面,每個面是一個正方形,好,繪製六個正方形就可以了。
glBegin(GL_QUADS);
     glVertex3f(...);
     glVertex3f(...);
     glVertex3f(...);
     glVertex3f(...);

     // ...
glEnd();

爲了繪製六個正方形,我們爲每個正方形指定四個頂點,最終我們需要指定6*4=24個頂點。但是我們知道,一個立方體其實總共只有八個頂點,要指定24次,就意味着每個頂點其實重複使用了三次,這樣可不是好的現象。最起碼,像上面這樣重複煩瑣的代碼,是很容易出錯的。稍有不慎,即使相同的頂點也可能被指定成不同的頂點了。
如果我們定義一個數組,把八個頂點都放到數組裏,然後每次指定頂點都使用指針,而不是使用直接的數據,這樣就避免了在指定頂點時考慮大量的數據,於是減少了代碼出錯的可能性。
// 將立方體的八個頂點保存到一個數組裏面
GLfloat vertex_list[][3] = {
     -0.5f, -0.5f, -0.5f,
      0.5f, -0.5f, -0.5f,
     // ...
};
// 指定頂點時,用指針,而不用直接用具體的數據
glBegin(GL_QUADS);
     glVertex3fv(vertex_list[0]);
     glVertex3fv(vertex_list[2]);
     glVertex3fv(vertex_list[3]);
     glVertex3fv(vertex_list[1]);

     // ...
glEnd();

修改之後,雖然代碼變長了,但是確實易讀得多。很容易就看出第0, 2, 3, 1這四個頂點構成一個正方形。
稍稍觀察就可以發現,我們使用了大量的glVertex3fv函數,其實每一句都只有其中的頂點序號不一樣,因此我們可以再定義一個序號數組,把所有的序號也放進去。這樣一來代碼就更加簡單了。
// 將立方體的八個頂點保存到一個數組裏面
GLfloat vertex_list[][3] = {
     -0.5f, -0.5f, -0.5f,
      0.5f, -0.5f, -0.5f,
     -0.5f,   0.5f, -0.5f,
      0.5f,   0.5f, -0.5f,
     -0.5f, -0.5f,   0.5f,
      0.5f, -0.5f,   0.5f,
     -0.5f,   0.5f,   0.5f,
      0.5f,   0.5f,   0.5f,
};

// 將要使用的頂點的序號保存到一個數組裏面
GLint index_list[][4] = {
     0, 2, 3, 1,
     0, 4, 6, 2,
     0, 1, 5, 4,
     4, 5, 7, 6,
     1, 3, 7, 5,
     2, 6, 7, 3,
};

i, j;

// 繪製的時候代碼很簡單
glBegin(GL_QUADS);
(i=0; i<6; ++i)          // 有六個面,循環六次
    (j=0; j<4; ++j)      // 每個面有四個頂點,循環四次
         glVertex3fv(vertex_list[index_list[i][j]]);
glEnd();

這樣,我們就得到一個比較成熟的繪製立方體的版本了。它的數據和程序代碼基本上是分開的,所有的頂點放到一個數組中,使用頂點的序號放到另一個數組中,而利用這兩個數組來繪製立方體的代碼則很簡單。
關於頂點的序號,下面這個圖片可以幫助理解。
http://s16.sinaimg.cn/middle/84181a794cef7cb7c183f&690

正對我們的面,按逆時針順序,背對我們的面,則按順時針順序,這樣就得到了上面那個index_list數組。
爲什麼要按照順時針逆時針的規則呢?因爲這樣做可以保證無論從哪個角度觀察,看到的都是“正面”,而不是背面。在計算光照時,正面和背面的處理可能是不同的,另外,剔除背面只繪製正面,可以提高程序的運行效率。(關於正面、背面,以及剔除,參見第三課,繪製幾何圖形的一些細節問題)
例如在繪製之前調用如下的代碼:
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
則繪製出來的圖形就只有正面,並且只顯示邊線,不進行填充。
效果如圖:
http://s13.sinaimg.cn/middle/84181a794cef7cb78e93c&690

前面的方法中,我們將數據和代碼分離開,看起來只要八個頂點就可以繪製一個立方體了。但是實際上,循環還是執行了6*4=24次,也就是說雖然代碼的結構清晰了不少,但是程序運行的效率,還是和最原始的那個方法一樣。
減少函數的調用次數,是提高運行效率的方法之一。於是我們想到了顯示列表。把繪製立方體的代碼裝到一個顯示列表中,以後只要調用這個顯示列表即可。
這樣看起來很不錯,但是顯示列表有一個缺點,那就是一旦建立後不可再改。如果我們要繪製的不是立方體,而是一個能夠走動的人物,因爲人物走動時,四肢的位置不斷變化,幾乎沒有辦法把所有的內容裝到一個顯示列表中。必須每種動作都使用單獨的顯示列表,這樣會導致大量的顯示列表管理困難。
頂點數組是解決這個問題的一個方法。使用頂點數組的時候,也是像前面的方法一樣,用一個數組保存所有的頂點,用一個數組保存頂點的序號。但最後繪製的時候,不是編寫循環語句逐個的指定頂點了,而是通知OpenGL,“保存頂點的數組”和“保存頂點序號的數組”所在的位置,由OpenGL自動的找到頂點,並進行繪製。
下面的代碼說明了頂點數組是如何使用的:
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, vertex_list);
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);

其中:
glEnableClientState(GL_VERTEX_ARRAY); 表示啓用頂點數組。
glVertexPointer(3, GL_FLOAT, 0, vertex_list); 指定頂點數組的位置,3表示每個頂點由三個量構成(x, y, z),GL_FLOAT表示每個量都是一個GLfloat類型的值。第三個參數0,參見後面介紹“stride參數”。最後的vertex_list指明瞭數組實際的位置。
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list); 根據序號數組中的序號,查找到相應的頂點,並完成繪製。GL_QUADS表示繪製的是四邊形,24表示總共有24個頂點,GL_UNSIGNED_INT表示序號數組內每個序號都是一個GLuint類型的值,index_list指明瞭序號數組實際的位置。
上面三行代碼代替了原來的循環。可以看到,原來的glBegin/glEnd不再需要了,也不需要調用glVertex*系列函數來指定頂點,因此可以明顯的減少函數調用次數。另外,數組中的內容可以隨時修改,比顯示列表更加靈活。

詳細一點的說明。
頂點數組實際上是多個數組,頂點座標、紋理座標、法線向量、頂點顏色等等,頂點的每一個屬性都可以指定一個數組,然後用統一的序號來進行訪問。比如序號3,就表示取得顏色數組的第3個元素作爲顏色、取得紋理座標數組的第3個元素作爲紋理座標、取得法線向量數組的第3個元素作爲法線向量、取得頂點座標數組的第3個元素作爲頂點座標。把所有的數據綜合起來,最終得到一個頂點。
可以用glEnableClientState/glDisableClientState單獨的開啓和關閉每一種數組。
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
用以下的函數來指定數組的位置:
glVertexPointer
glColorPointer
glNormalPointer
glTexCoordPointer

爲什麼不使用原來的glEnable/glDisable函數,而要專門的規定一個glEnableClientState/glDisableClientState函數呢?這跟OpenGL的工作機制有關。OpenGL在設計時,認爲可以將整個OpenGL系統分爲兩部分,一部分是客戶端,它負責發送OpenGL命令。一部分是服務端,它負責接收OpenGL命令並執行相應的操作。對於個人計算機來說,可以將CPU、內存等硬件,以及用戶編寫的OpenGL程序看做客戶端,而將OpenGL驅動程序、顯示設備等看做服務端。
通常,所有的狀態都是保存在服務端的,便於OpenGL使用。例如,是否啓用了紋理,服務端在繪製時經常需要知道這個狀態,而我們編寫的客戶端OpenGL程序只在很少的時候需要知道這個狀態。所以將這個狀態放在服務端是比較有利的。
但頂點數組的狀態則不同。我們指定頂點,實際上就是把頂點數據從客戶端發送到服務端。是否啓用頂點數組,只是控制發送頂點數據的方式而已。服務端只管接收頂點數據,而不必管頂點數據到底是用哪種方式指定的(可以直接使用glBegin/glEnd/glVertex*,也可以使用頂點數組)。所以,服務端不需要知道頂點數組是否開啓。因此,頂點數組的狀態放在客戶端是比較合理的。
爲了表示服務端狀態和客戶端狀態的區別,服務端的狀態用glEnable/glDisable,客戶端的狀態則用glEnableClientState/glDisableClientState。
stride參數。
頂點數組並不要求所有的數據都連續存放。如果數據沒有連續存放,則指定數據之間的間隔即可。
例如:我們使用一個struct來存放頂點中的數據。注意每個頂點除了座標外,還有額外的數據(這裏是一個int類型的值)。
__point__ {
     GLfloat position[3];
          id;
} Point;
Point vertex_list[] = {
     -0.5f, -0.5f, -0.5f, 1,
      0.5f, -0.5f, -0.5f, 2,
     -0.5f,   0.5f, -0.5f, 3,
      0.5f,   0.5f, -0.5f, 4,
     -0.5f, -0.5f,   0.5f, 5,
      0.5f, -0.5f,   0.5f, 6,
     -0.5f,   0.5f,   0.5f, 7,
      0.5f,   0.5f,   0.5f, 8,
};
GLint index_list[][4] = {
     0, 2, 3, 1,
     0, 4, 6, 2,
     0, 1, 5, 4,
     4, 5, 7, 6,
     1, 3, 7, 5,
     2, 6, 7, 3,
};
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, (Point), vertex_list);
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);

注意最後三行代碼,可以看到,幾乎所有的地方都和原來一樣,只在glVertexPointer函數的第三個參數有所不同。這個參數就是stride,它表示“從一個數據的開始到下一個數據的開始,所相隔的字節數”。這裏設置爲sizeof(Point)就剛剛好。如果設置爲0,則表示數據是緊密排列的,對於3個GLfloat的情況,數據緊密排列時stride實際上爲3*4=12。
混合數組。如果需要同時使用顏色數組、頂點座標數組、紋理座標數組、等等,有一種方式是把所有的數據都混合起來,指定到同一個數組中。這就是混合數組。
GLfloat arr_c3f_v3f[] = {
     1, 0, 0, 0, 1, 0,
     0, 1, 0, 1, 0, 0,
  
發佈了132 篇原創文章 · 獲贊 2 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章