概述
作為在立即模式(glBegin()與glEnd()之間)下指定單個頂點數據的替代,你可以保存頂點數據在一組列表中,包括頂點位置、法線、紋理坐標與顏色信息。並且你可以通過索引數組解引用數組元素繪制選定的幾何圖元。
看看下面的用立即模式繪制立方體的代碼。
glBegin(GL_TRIANGLES); // draw a cube with 12 triangles
// 前面 =================
glVertex3fv(v0); // v0-v1-v2
glVertex3fv(v1);
glVertex3fv(v2);
glVertex3fv(v2); // v2-v3-v0
glVertex3fv(v3);
glVertex3fv(v0);
// 右面 =================
glVertex3fv(v0); // v0-v3-v4
glVertex3fv(v3);
glVertex3fv(v4);
glVertex3fv(v4); // v4-v5-v0
glVertex3fv(v5);
glVertex3fv(v0);
// 上面 ===================
glVertex3fv(v0); // v0-v5-v6
glVertex3fv(v5);
glVertex3fv(v6);
glVertex3fv(v6); // v6-v1-v0
glVertex3fv(v1);
glVertex3fv(v0);
... // 繪制其余3面
glEnd();
為構造每個面的2個三角形,需要調用glVertex*()6次。例如,正面分為v0-v1-v2與v2-v3-v0兩個三角形。一個立方體有6個面,因此glVertex*()的調用次數為36。如果你還需為相關頂點指定法線、紋理坐標與顏色,這增加對OpenGL函數的調用。
另一個需要注意的是:頂點“v0”被三個相鄰的面共用:正面、右面與頂面。在立即模式下,你必須提供這個共用點6次,就像代碼中那樣每個面2次。
使用頂點數字會降低函數調用次數及共用頂點的重復使用。因此,可以提供渲染效率。在此,解釋3種不同的使用頂點數組的OpenGL函數:glDrawArrays()、glDrawElements()與glDrawRangeElements()。然而,更好的方法是使用頂點緩存對象(VBO)與顯示列表。
初始化
OpenGL提供glEnableClientState()與glDisableClientState()函數啟用/禁用6中不同類別的數組。此外,有6個函數用於指定數組的精確位置(地址),因此在你的應用程序中OpenGL可以訪問這些數組。
- glVertexPointer():指定頂點坐標數組指針
- glNormalPointer():指定法線數組指針
- glColorPointer():指定RGB顏色數組指針
- glIndexPointer():指定索引顏色數組指針
- glTexCoordPointer():指定紋理坐標數組指針
- glEdgeFlagPointer():指定邊標志數組指針
每個函數都需要不同的參數。可以參考OpenGL API手冊。邊標志用於標記頂點是否在邊界上。因此,如果glPolygonMode()為GL_LINE時,只有邊具有邊標記的那些邊是可見的。
glVertexPointer(GLint size, GLenum type, GLsizei stride, const GLvoid* pointer)
- size:頂點坐標數量,對於2D點為2,3D點為3。
- type:GL_FLOAT、GL_SHORT、GL_INT或GL_DOUBLE。
- stride:連續兩個頂點間的字節偏移量(用於交叉數組)。
- pointer:頂點數組指針。
glNormalPointer(GLenum type, GLsizei stride, const GLvoid* pointer)
- type:GL_FLOAT、GL_SHORT、GL_INT或GL_DOUBLE。
- stride:連續兩個法線間的字節偏移量(用於交叉數組)。
- pointer:法線數組指針。
注意,頂點數組保存在你的應用程序(系統內存),它在客戶端。且處在服務端的OpenGL訪問它們。這就是為什么擁有頂點數組這些特殊命令的原因,使用glEnableClientState()與glDisableClientState()而不是glEnable()與glDisable()。
glDrawArrays()
glDrawArrays()從開啟的數組中順序讀取頂點數據。由於glDrawArray()不准許在頂點數組中跳躍,你必須為每個面重復指定共用頂點。
glDrawArrays()具有3個參數。第一個參數為圖元類型。第二個參數為數組的其實偏移位置。最后一個參數為傳遞給OpenGL渲染管線的頂點數量。在上面繪制立方體的實例中,第一個參數為GL_TRIANGLES,第二個參數為0,即從數組開始讀取。最后一個參數為36:立方體具有6個面,且每個面需要繪制兩個三角形的6個頂點,6×6=36。
GLfloat vertices[] = {...}; // 36頂點坐標 ... // 啟用並指定頂點數組指針 glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3, GL_FLOAT, 0, vertices); // 繪制立方體 glDrawArrays(GL_TRIANGLES, 0, 36); // 在繪制之后禁用頂點數組 glDisableClientState(GL_VERTEX_ARRAY);
由於使用glDrawArrays(),你可以用單個glDrawArrays()調用替換36次的glVertex*()調用。然而,我們依舊需要重復指定共用頂點,因此數組中的頂點數量依舊為36個,而不是8個。glDrawElements()是降低數組中頂點數量的方法,因此,它准許向OpenGL傳遞更少的數據。
glDrawElements()
glDrawElements()通過頂點數組相關的隨機數組索引繪制圖元序列。它降低函數調用次數與頂點傳遞數量。此外,OpenGL可以緩存最近處理過的頂點以及重用它們,而不必向頂點變換管線重復發送相同的頂點。
glDrawElements()需要4個參數。第一個參數為圖元類型,第二個為索引數組數量,第三個位索引數組的數據類型,最后一個參數為索引數組地址。在此例中,參數分別為:GL_TRIANGLES、36、GL_UNSIGNED_BYTE與indices。
GLfloat vertices[] = {...}; // 8個頂點坐標 GLubyte indices[] = {0,1,2, 2,3,0, // 36個索引 0,3,4, 4,5,0, 0,5,6, 6,1,0, 1,6,7, 7,2,1, 7,4,3, 3,2,7, 4,7,6, 6,5,4}; ... // 啟用並指定頂點數組指針 glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3, GL_FLOAT, 0, vertices); // 繪制立方體 glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, indices); // 繪制之后禁用頂點數組 glDisableClientState(GL_VERTEX_ARRAY);
現在,頂點數組的大小為8,這就是立方體的頂點數量,而沒有任何重復。
注意,索引數組的數據類型為GLubyte,而不是GLuint或GLushort。為了降低索引數組的大小,它應該是能夠容納索引數量的最小數據類型,否則,由於索引數組過大能夠引起性能下降。因為頂點數組包含8個頂點,GLubyte足夠容納所有索引值。
另一個需要考慮的問題是共用頂點上的法向量。如果共用頂點的相鄰多邊形的法線各不相同,則法向量需要同面的數量一樣多,每個面一個法向量。
例如,頂點v0為正面、右面與頂面所共用,不過法線並不能共用。正面的法線為n0,右面的法線為n1,頂面的法線為n2。對於這種情況,法線並不與共用頂點相同,頂點就不能夠僅在頂點數組中指定一次。為了匹配法向數組中元素的數量,頂點數組中必須多次指定該頂點坐標。具有法線的典型立方體需要24個單一頂點:6個面×每面4個頂點。參考示例代碼中的實際實現。
glDrawRangeElements()
與glDrawElements()類似,glDrawRangeElements()也適用於隨機訪問頂點數組。不過glDrawRangeElements()有額外的2個參數(start與end索引)以指定需要讀取的頂點范圍。通過增加該范圍限定,OpenGL能夠在渲染之前僅獲取限定數量的頂點數組,且能夠提高性能。
glDrawRangeElements()中新增的參數為start與end索引,OpenGL從這些值(end-start+1)中獲取限定數量的頂點。並且索引數組中的值必須位於start與end索引之間。注意,並不是范圍(start,end)之間的所有頂點都會被接引用。然而,如果你指定一個稀疏使用范圍,會引起不必要的對數組中未使用的那些頂點的處理。
GLfloat vertices[] = {...}; // 8個頂點坐標 GLubyte indices[] = {0,1,2, 2,3,0, // 第一部分(18個索引) 0,3,4, 4,5,0, 0,5,6, 6,1,0, 1,6,7, 7,2,1, // 第二部分(18個索引) 7,4,3, 3,2,7, 4,7,6, 6,5,4}; ... // 開啟並指定頂點數組指針 glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer(3, GL_FLOAT, 0, vertices); // 繪制第一部分, 范圍為6 - 0 + 1 = 7用到的頂點 glDrawRangeElements(GL_TRIANGLES, 0, 6, 18, GL_UNSIGNED_BYTE, indices); // 繪制第二部分, 范圍為6 - 0 + 1 = 7用到的頂點 glDrawRangeElements(GL_TRIANGLES, 1, 7, 18, GL_UNSIGNED_BYTE, indices+18); // 繪制之后禁用頂點數組 glDisableClientState(GL_VERTEX_ARRAY);
你可以通過使用GL_MAX_ELEMENTS_VERTICES與GL_MAX_ELEMENTS_INDICES調用glGetIntegerv()查詢可獲取頂點的最大數量與可引用索引的最大數量。
注意,glDrawRangeElements()在OpenGL 1.2或更高版本有效。
實例
該實例程序以4中不同方式繪制立方體:立即模式、glDrawArrays()、glDrawElements()與glDrawRangeElements()。
- draw1():以立即模式繪制立方體。
- draw2():以glDrawArrays()繪制立方體。
- draw3():以glDrawElements()繪制立方體。
- draw4():以glDrawRangeElements()繪制立方體。
- draw5():以glDrawElements()與間隔頂點數組方式繪制立方體。
下載源代碼與二進制文件:vertexArray.zip
為了正確執行該程序,顯卡必須支持OpenGL v1.2或更高。通過glinfo確認你的顯卡驅動是否支持OpenGL v1.2或更高。