OpenGl 繪制一個立方體
為了繪制六個正方形,我們為每個正方形指定四個頂點,最終我們需要指定6*4=24個頂點。但是我們知道,一個立方體其實總共只有八個頂點,要指定24次,就意味着每個頂點其實重復使用了三次,這樣可不是好的現象。最起碼,像上面這樣重復煩瑣的代碼,是很容易出錯的。稍有不慎,即使相同的頂點也可能被指定成不同的頂點了。
如果我們定義一個數組,把八個頂點都放到數組里,然后每次指定頂點都使用指針,而不是使用直接的數據,這樣就避免了在指定頂點時考慮大量的數據,於是減少了代碼出錯的可能性。 // 將立方體的八個頂點保存到一個數組里面
立方體的各個頂點的順序如下圖所示:
1. 定義立方體的各個頂點數組
將立方體的八個頂點保存到一個數組里面。這樣在指定頂點時,用指針,而不用直接用具體的數據。
1 // 將立方體的八個頂點保存到一個數組里面 2 static const GLfloat vertex_list[][3] = { 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 0.5f, -0.5f, 0.5f, 9 -0.5f, 0.5f, 0.5f, 10 0.5f, 0.5f, 0.5f, 11 };
使用時可以直接采用指針繪制。
1 glBegin(GL_QUADS); 2 glVertex3fv(vertex_list[0]); 3 glVertex3fv(vertex_list[2]); 4 glVertex3fv(vertex_list[3]); 5 glVertex3fv(vertex_list[1]); 6 7 // ... 8 glEnd();
很容易就看出第0, 2, 3, 1這四個頂點構成一個正方形。稍稍觀察就可以發現,我們使用了大量的glVertex3fv函數,其實每一句都只有其中的頂點序號不一樣,因此我們可以再定義一個序號數組,把所有的序號也放進去。這樣一來代碼就更加簡單了。
2. 定義立方體使用的各個頂點數組的序號數組
將要使用的頂點的序號保存到一個數組里面。
1 static const GLint index_list[][4] = { 2 0, 2, 3, 1, 3 0, 4, 6, 2, 4 0, 1, 5, 4, 5 4, 5, 7, 6, 6 1, 3, 7, 5, 7 2, 6, 7, 3, 8 };
3. 繪制
1 // 繪制的時候代碼很簡單 2 glBegin(GL_QUADS); 3 for(int i=0; i<6; ++i) // 有六個面,循環六次 4 for(int j=0; j<4; ++j) // 每個面有四個頂點,循環四次 5 glVertex3fv(vertex_list[index_list[i][j]]); 6 glEnd();
這樣,我們就得到一個比較成熟的繪制立方體的版本了。它的數據和程序代碼基本上是分開的,所有的頂點放到一個數組中,使用頂點的序號放到另一個數組中,而利用這兩個數組來繪制立方體的代碼則很簡單。
4. 立方體各個面繪制的頂點順序
正對我們的面,按逆時針順序,背對我們的面,則按順時針順序,這樣就得到了上面那個index_list數組。為什么要按照順時針逆時針的規則呢?因為這樣做可以保證無論從哪個角度觀察,看到的都是“正面”,而不是背面。不同的繪制順序,同一個面有“正”“反”面之分。
在計算光照時,正面和背面的處理可能是不同的,另外,剔除背面只繪制正面,可以提高程序的運行效率。
例如在繪制之前調用如下的代碼:
glFrontFace(GL_CCW);//逆時針 glCullFace(GL_BACK); glEnable(GL_CULL_FACE); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
則繪制出來的圖形就只有正面,並且只顯示邊線,不進行填充。
5. 優化
前面的方法中,我們將數據和代碼分離開,看起來只要八個頂點就可以繪制一個立方體了。但是實際上,循環還是執行了6*4=24次,也就是說雖然代碼的結構清晰了不少,但是程序運行的效率,還是和最原始的那個方法一樣。
減少函數的調用次數,是提高運行效率的方法之一。
於是我們想到了顯示列表。把繪制立方體的代碼裝到一個顯示列表中,以后只要調用這個顯示列表即可。
這樣看起來很不錯,但是顯示列表有一個缺點,那就是一旦建立后不可再改。如果我們要繪制的不是立方體,而是一個能夠走動的人物,因為人物走動時,四肢的位 置不斷變化,幾乎沒有辦法把所有的內容裝到一個顯示列表中。必須每種動作都使用單獨的顯示列表,這樣會導致大量的顯示列表管理困難。
頂點數組是解決這個問題的一個方法。使用頂點數組的時候,也是像前面的方法一樣,用一個數組保存所有的頂點,用一個數組保存頂點的序號。但最后繪制的時候,不是編寫循環語句逐個的指定頂點了,而是通知OpenGL,“保存頂點的數組”和“保存頂點序號的數組”所在的位置,由OpenGL自動的找到頂點,並進行繪制。
下面的代碼說明了頂點數組是如何使用的:
1 glEnableClientState(GL_VERTEX_ARRAY); 2 glVertexPointer(3, GL_FLOAT, 0, vertex_list); 3 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。
6. 關於tride參數
頂點數組並不要求所有的數據都連續存放。如果數據沒有連續存放,則指定數據之間的間隔即可。
例如:我們使用一個struct來存放頂點中的數據。注意每個頂點除了坐標外,還有額外的數據(這里是一個int類型的值)。
typedef struct __point__ { GLfloat position[3]; int 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, }; static 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, sizeof(Point), vertex_list); glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);
注意最后三行代碼,可以看到,幾乎所有的地方都和原來一樣,只在glVertexPointer函數的第三個參數有所不同。這個參數就是stride,它表示“從一個數據的開始到下一個數據的開始,所相隔的字節數”。這里設置為sizeof(Point)就剛剛好。如果設置為0,則表示數據是緊密排列的,對於3個GLfloat的情況,數據緊密排列時stride實際上為3*4=12。
7. 混合數組
如果需要同時使用顏色數組、頂點坐標數組、紋理坐標數組、等等,有一種方式是把所有的數據都混合起來,指定到同一個數組中。這就是混合數組。
1 GLfloat arr_c3f_v3f[] = { 2 1, 0, 0, 0, 1, 0, 3 0, 1, 0, 1, 0, 0, 4 0, 0, 1, -1, 0, 0, 5 }; 6 GLuint index_list[] = {0, 1, 2}; 7 glInterleavedArrays(GL_C3F_V3F, 0, arr_c3f_v3f); 8 glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, index_list);
glInterleavedArrays,可以設置混合數組。這個函數會自動調用glVertexPointer, glColorPointer等函數,並且自動的開啟或禁用相關的數組。函數的第一個參數表示了混合數組的類型。例如GL_C3F_V3F表示:三個浮點數作為顏色、三個浮點數作為頂點坐標。
也可以有其它的格式,比如
GL_V2F, GL_V3F, GL_C4UB_V2F, GL_C4UB_V3F, GL_C3F_V3F, GL_N3F_V3F, GL_C4F_N3F_V3F, GL_T2F_V3F,
GL_T4F_V4F, GL_T2F_C4UB_V3F, GL_T2F_C3F_V3F, GL_T2F_N3F_V3F, GL_T2F_C4F_N3F_V3F, GL_T4F_C4F_N3F_V4F等等。
其中
T:表示紋理坐標,C:表示顏色,N:表示法線向量,V:表示頂點坐標。
8. 再來說說頂點數組與顯示列表的區別
兩者都可以明顯的減少函數的調用次數,但是還是各有優點的。
對於頂點數組,頂點數據是存放在內存中的,也就是存放在客戶端。每次繪制的時候,需要把所有的頂點數據從客戶端(內存)發送到服務端(顯示設備),然后進行處理。對於顯示列表,頂點數據是放在顯示列表中的,顯示列表本身又是存放在服務器端的,所以不會重復的發送數據。
對於頂點數組,因為頂點數據放在內存中,所以可以隨時修改,每次繪制的時候都會把當前數組中的內容作為頂點數據發送並進行繪制。對於顯示列表,數據已經存放到服務器段,並且無法取出,所以無法修改。
也就是說,顯示列表可以避免數據的重復發送,效率會較高;頂點數組雖然會重復的發送數據,但由於數據可以隨時修改,靈活性較好。
9. 頂點緩沖區對象
(提示:頂點緩沖區對象是OpenGL 1.5所提供的功能,但它在成為標准前是一個ARB擴展,可以通過GL_ARB_vertex_buffer_object擴展來使用這項功能。前面已經講過,ARB擴展的函數名稱以字母ARB結尾,常量名稱以字母_ARB結尾,而標准函數、常量則去掉了ARB字樣。很多的OpenGL實現同時支持vertex buffer object的標准版本和ARB擴展版本。我們這里以ARB擴展來講述,因為目前絕大多數個人計算機都支持ARB擴展版本,但少數顯卡僅支持OpenGL 1.4,無法使用標准版本。)
前面說到頂點數組和顯示列表在繪制立方體時各有優劣,那么有沒有辦法將它們的優點集中到一起,並且盡可能的減少缺點呢?
頂點緩沖區對象就是為了解決這個問題而誕生的。它數據存放在服務端,同時也允許客戶端靈活的修改,兼顧了運行效率和靈活性。
頂點緩沖區對象跟紋理對象有很多相似之處。
首先,分配一個緩沖區對象編號,然后,為對應編號的緩沖區對象指定數據,以后可以隨時修改其中的數據。下面的表格可以幫助類比理解。
紋理對象 頂點緩沖區對象
分配編號 glGenTextures glGenBuffersARB
綁定(指定為當前所使用的對象) glBindTexture glBindBufferARB
指定數據 glTexImage* glBufferDataARB
修改數據 glTexSubImage* glBufferSubDataARB
頂點數據和序號各自使用不同的緩沖區。
具體的說,就是頂點數據放在GL_ARRAY_BUFFER_ARB類型的緩沖區中,序號數據放在GL_ELEMENT_ARRAY_BUFFER_ARB類型的緩沖區中。
具體的情況可以用下面的代碼來說明:
1 static GLuint vertex_buffer; 2 static GLuint index_buffer; 3 4 // 分配一個緩沖區,並將頂點數據指定到其中 5 glGenBuffersARB(1, &vertex_buffer); 6 glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer); 7 glBufferDataARB(GL_ARRAY_BUFFER_ARB, 8 sizeof(vertex_list), vertex_list, GL_STATIC_DRAW_ARB); 9 10 // 分配一個緩沖區,並將序號數據指定到其中 11 glGenBuffersARB(1, &index_buffer); 12 glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer); 13 glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 14 sizeof(index_list), index_list, GL_STATIC_DRAW_ARB);
在指定緩沖區數據時,最后一個參數是關於性能的提示。一共有STREAM_DRAW, STREAM_READ, STREAM_COPY, STATIC_DRAW, STATIC_READ, STATIC_COPY, DYNAMIC_DRAW, DYNAMIC_READ, DYNAMIC_COPY這九種。每一種都表示了使用頻率和用途,OpenGL會根據這些提示進行一定程度的性能優化。
(提示僅僅是提示,不是硬性規定。也就是說,即使使用了STREAM_DRAW,告訴OpenGL這段緩沖區數據一旦指定,以后不會修改,但實際上以后仍可修改,不過修改時可能有較大的性能代價)
當使用glBindBufferARB后,各種使用指針為參數的OpenGL函數,行為會發生變化。
以glColor3fv為例,通常,這個函數接受一個指針作為參數,從指針所指的位置取出連續的三個浮點數,作為當前的顏色。
但使用glBindBufferARB后,這個函數不再從指針所指的位置取數據。函數會先把指針轉化為整數,假設轉化后結果為k,則會從當前緩沖區的第k個字節開始取數據。特別一點,如果我們寫glColor3fv(NULL);因為NULL轉化為整數后通常是零,所以從緩沖區的第0個字節開始取數據,也就是從緩沖區最開始的位置取數據。
這樣一來,原來寫的
glVertexPointer(3, GL_FLOAT, 0, vertex_list);
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list);
在使用緩沖區對象后,就變成了
glVertexPointer(3, GL_FLOAT, 0, NULL);
glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, NULL);
以下是完整的使用了頂點緩沖區對象的代碼:
1 static GLfloat vertex_list[][3] = { 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 -0.5f, 0.5f, 0.5f, 9 0.5f, 0.5f, 0.5f, 10 }; 11 12 static GLint index_list[][4] = { 13 0, 2, 3, 1, 14 0, 4, 6, 2, 15 0, 1, 5, 4, 16 4, 5, 7, 6, 17 1, 3, 7, 5, 18 2, 6, 7, 3, 19 }; 20 21 if( GLEE_ARB_vertex_buffer_object ) { 22 // 如果支持頂點緩沖區對象 23 static int isFirstCall = 1; 24 static GLuint vertex_buffer; 25 static GLuint index_buffer; 26 if( isFirstCall ) { 27 // 第一次調用時,初始化緩沖區 28 isFirstCall = 0; 29 30 // 分配一個緩沖區,並將頂點數據指定到其中 31 glGenBuffersARB(1, &vertex_buffer); 32 glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer); 33 glBufferDataARB(GL_ARRAY_BUFFER_ARB, 34 sizeof(vertex_list), vertex_list, GL_STATIC_DRAW_ARB); 35 36 // 分配一個緩沖區,並將序號數據指定到其中 37 glGenBuffersARB(1, &index_buffer); 38 glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer); 39 glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 40 sizeof(index_list), index_list, GL_STATIC_DRAW_ARB); 41 } 42 glBindBufferARB(GL_ARRAY_BUFFER_ARB, vertex_buffer); 43 glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, index_buffer); 44 45 // 實際使用時與頂點數組非常相似,只是在指定數組時不再指定實際的數組,改為指定NULL即可 46 glEnableClientState(GL_VERTEX_ARRAY); 47 glVertexPointer(3, GL_FLOAT, 0, NULL); 48 glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, NULL); 49 } else { 50 // 不支持頂點緩沖區對象 51 // 使用頂點數組 52 glEnableClientState(GL_VERTEX_ARRAY); 53 glVertexPointer(3, GL_FLOAT, 0, vertex_list); 54 glDrawElements(GL_QUADS, 24, GL_UNSIGNED_INT, index_list); 55 }
可以分配多個緩沖區對象,頂點坐標、顏色、紋理坐標等數據,可以各自單獨使用一個緩沖區。
每個緩沖區可以有不同的性能提示,比如在繪制一個運動的人物時,頂點坐標數據經常變化,但法線向量、紋理坐標等則不會變化,可以給予不同的性能提示,以提高性能。
10. 小結
本課從繪制一個立方體出發,描述了OpenGL在各個版本中對於繪制的處理。
繪制物體的時候,應該將數據單獨存放,盡量不要到處寫類似glVertex3f(1.0f, 0.0f, 1.0f)這樣的代碼。將頂點坐標、頂點序號都存放到單獨的數組中,可以讓繪制的代碼變得簡單。
可以把繪制物體的所有命令裝到一個顯示列表中,這樣可以避免重復的數據傳送。但是因為顯示列表一旦建立,就無法修改,所以靈活性很差。
OpenGL 1.1版本,提供了頂點數組。它可以指定數據的位置、頂點序號的位置,從而有效的減少函數調用次數,達到提高效率的目的。但是它沒有避免重復的數據傳送,所以效率還有待進一步提高。
OpenGL 1.5版本,提供了頂點緩沖區對象。它綜合了顯示列表和頂點數組的優點,同時兼顧運行效率和靈活性,是繪制物體的一個好選擇。如果系統不支持OpenGL 1.5,也可以檢查是否支持擴展GL_ARB_vertex_buffer_object。
參考網址:http://blog.sina.com.cn/s/blog_4d035b080100k3el.html