[OpenGL ES 06]使用VBO:頂點緩存
羅朝輝 (http://www.cnblogs.com/kesalin/)
本文遵循“署名-非商業用途-保持一致”創作公用協議
這是《OpenGL ES 教程》的第六篇,前五篇請參考如下鏈接:
[OpenGL ES 01]iOS上OpenGL ES之初體驗
[OpenGL ES 02]OpenGL ES渲染管線與着色器
[OpenGL ES 03]3D變換:模型,視圖,投影與Viewport
[OpenGL ES 04]3D變換實踐篇:平移,旋轉,縮放
[OpenGL ES 05]相對空間變換及顏色
一,VBO簡介
在前面幾篇的示例中,都是通過類似如下代碼直接從 CPU 主存中傳遞頂點數據到 GPU 中去進行運算與渲染的。
glVertexAttrib4f(_colorSlot, color[0], color[1], color[2], color[3]);
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices );
glEnableVertexAttribArray(_positionSlot);
glDrawElements(GL_LINES, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, indices);
在上面的代碼中 vertices 和 indices 都是在主存中分配的內存空間,當需要進行渲染時,這些數據便通過 glDrawElements 或 glDrawArrays 從 CPU 主存中拷貝到 GPU 中去進行運算與渲染。這種做法需要頻繁地在 CPU 與 GPU 之間傳遞數據,效率低下,因此出現了 VBO (Vertex Buffer object),即頂點緩存,它直接在 GPU 中開辟一個緩存區域來存儲頂點數據,因為它是用來緩存儲頂點數據,因此被稱之為頂點緩存。我們只會在初始化緩沖區,以及在頂點數據有變化時才需要對該緩沖區進行寫操作。使用頂點緩存能夠大大較少了CPU-GPU 之間的數據拷貝開銷,因此顯著地提升了程序運行的效率。
今天我們就能學習 VBO 在 OpenGL ES 中的運用,示例程序演示了六種編程實現的物體,本文源碼:點此查看,其運行效果如下:

二,API介紹
1,總覽
OpenGL ES 中通過如下函數來實現 VBO:
| glGenBuffers | 創建頂點緩存對象 |
| glBindBuffer | 將頂點緩存對象設置為當前數組緩存對象(array buffer object)或當前元素緩存對象(element buffer object) |
| glBufferData | 為頂點緩存對象申請內存空間,並進行初始化(視傳入的參數而定) |
| glBufferSubData | 初始化或更新頂點緩存對象 |
| glDeleteBuffers | 刪除頂點緩存對象 |
2,創建頂點緩存對象
void glGenBuffers (GLsizei n, GLuint* buffers);
參數 n : 表示需要創建頂點緩存對象的個數;
參數 buffers :用於存儲創建好的頂點緩存對象句柄;
同第一篇文章《[OpenGL ES 01]OpenGL ES之初體驗》中的講的 render buffer 對象句柄一樣,在這里,頂點緩存對象句柄始終是大於 0 的正整數,0 是 OpenGL ES 保留。該函數能夠一次產生多個頂點緩存對象。
3,將頂點緩存對象設置為(或曰綁定到)當前數組緩存對象或元素緩存對象
void glBindBuffer (GLenum target, GLuint buffer);
參數 target :指定綁定的目標,取值為 GL_ARRAY_BUFFER(用於頂點數據) 或 GL_ELEMENT_ARRAY_BUFFER(用於索引數據);
參數 buffer :頂點緩存對象句柄;
4,為頂點緩存對象分配空間
void glBufferData (GLenum target, GLsizeiptr size, const GLvoid* data, GLenum usage);
參數 target:與 glBindBuffer 中的參數 target 相同;
參數 size :指定頂點緩存區的大小,以字節為單位計數;
data :用於初始化頂點緩存區的數據,可以為 NULL,表示只分配空間,之后再由 glBufferSubData 進行初始化;
usage :表示該緩存區域將會被如何使用,它的主要目的是用於提示OpenGL該對該緩存區域做何種程度的優化。其參數為以下三個之一:
GL_STATIC_DRAW:表示該緩存區不會被修改;
GL_DyNAMIC_DRAW:表示該緩存區會被周期性更改;
GL_STREAM_DRAW:表示該緩存區會被頻繁更改;
如果頂點數據一經初始化就不會被修改,那么就應該盡量使用 GL_STATIC_DRAW,這樣能獲得更好的性能。
5,更新頂點緩沖區數據
void glBufferSubData (GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid* data);
參數 :offset 表示需要更新的數據的起始偏移量;
參數 :size 表示需要更新的數據的個數,也是以字節為計數單位;
data :用於更新的數據;
6,釋放頂點緩存
void glDeleteBuffers (GLsizei n, const GLuint* buffers);
參數與 glGenBuffers 類似,就不再累述,該函數用於刪除頂點緩存對象,釋放頂點緩存。
三,多面手:glVertexAttribPointer 和 glDrawElements
在介紹如何使用 VBO 進行渲染之前,我們先來回顧一下之前使用頂點數組進行渲染用到的函數:
void glVertexAttribPointer (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr);
參數 index :為頂點數據(如頂點,顏色,法線,紋理或點精靈大小)在着色器程序中的槽位;
參數 size :指定每一種數據的組成大小,比如頂點由 x, y, z 3個組成部分,紋理由 u, v 2個組成部分;
參數 type :表示每一個組成部分的數據格式;
參數 normalized : 表示當數據為法線數據時,是否需要將法線規范化為單位長度,對於其他頂點數據設置為 GL_FALSE 即可。如果法線向量已經為單位長度設置為 GL_FALSE 即可,這樣可免去不必要的計算,提升效率;
stride : 表示上一個數據到下一個數據之間的間隔(同樣是以字節為單位),OpenGL ES根據該間隔來從由多個頂點數據混合而成的數據塊中跳躍地讀取相應的頂點數據;
ptr :值得注意,這個參數是個多面手。如果沒有使用 VBO,它指向 CPU 內存中的頂點數據數組;如果使用 VBO 綁定到 GL_ARRAY_BUFFER,那么它表示該種類型頂點數據在頂點緩存中的起始偏移量。
那 GL_ELEMENT_ARRAY_BUFFER 表示的索引數據呢?那是由以下函數使用的:
void glDrawElements (GLenum mode, GLsizei count, GLenum type, const GLvoid* indices);
參數 mode :表示描繪的圖元類型,如:GL_TRIANGLES,GL_LINES,GL_POINTS;
參數 count : 表示索引數據的個數;
參數 type : 表示索引數據的格式,必須是無符號整形值;
indices :這個參數也是個多面手,如果沒有使用 VBO,它指向 CPU 內存中的索引數據數組;如果使用 VBO 綁定到 GL_ELEMENT_ARRAY_BUFFER,那么它表示索引數據在 VBO 中的偏移量。
四,使用示例
在今天的示例中,我借用《iPhone 3D Programming》中創建可編程3維物體的部分代碼來創建3維物體的頂點以及索引,在這里就略去這部分的介紹,有興趣研究的同學可以查看源碼。在這里就只講與頂點緩存相關的部分代碼。
首先是創建頂點緩存對象,分配空間並初始化:
// Create the VBO for the vertice.
//
GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, vBufSize * sizeof(GLfloat), vbuf, GL_STATIC_DRAW);
// Create the VBO for the line indice
//
GLuint lineIndexBuffer;
glGenBuffers(1, &lineIndexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, lineIndexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, lineIndexCount * sizeof(GLushort), lineBuf, GL_STATIC_DRAW);
// Create the VBO for the triangle indice
//
GLuint triangleIndexBuffer;
glGenBuffers(1, &triangleIndexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, triangleIndexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, triangleIndexCount * sizeof(GLushort), triangleBuf, GL_STATIC_DRAW);
然后,使用 VBO 進行渲染:
- (void)drawSurface
{
if (_currentVBO == nil)
return;
glBindBuffer(GL_ARRAY_BUFFER, [_currentVBO vertexBuffer]);
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, [_currentVBO vertexSize] * sizeof(GLfloat), 0);
glEnableVertexAttribArray(_positionSlot);
// Draw the red triangles.
//
glVertexAttrib4f(_colorSlot, 1.0, 0.0, 0.0, 1.0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, [_currentVBO triangleIndexBuffer]);
glDrawElements(GL_TRIANGLES, [_currentVBO triangleIndexCount], GL_UNSIGNED_SHORT, 0);
// Draw the black lines.
//
glVertexAttrib4f(_colorSlot, 0.0, 0.0, 0.0, 1.0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, [_currentVBO lineIndexBuffer]);
glDrawElements(GL_LINES, [_currentVBO lineIndexCount], GL_UNSIGNED_SHORT, 0);
glDisableVertexAttribArray(_positionSlot);
}
由於本示例可描繪 6 個3維幾何物體,因此 _currentVBO 表示當前描繪的幾何物體,這是一個 DrawableVBO 對象。DrawableVBO 類聲明如下:
@interface DrawableVBO : NSObject @property (nonatomic, assign) GLuint vertexBuffer; @property (nonatomic, assign) GLuint lineIndexBuffer; @property (nonatomic, assign) GLuint triangleIndexBuffer; @property (nonatomic, assign) int vertexSize; @property (nonatomic, assign) int lineIndexCount; @property (nonatomic, assign) int triangleIndexCount; - (void) cleanup; @end
它包含一個用於頂點數據的頂點緩存對象 vertexBuffer 和兩個用於索引數據的頂點緩存對象 lineIndexBuffer 和 triangleIndexBuffer,這些對象都是通過前面的創建頂點緩存對象部分代碼生成的。vertexSize 表示頂點數據的大小,而 lineIndexCount 和 triangleIndexCount 表示索引數據的個數。方法 cleanup 是用於清理頂點緩存對象,其實現如下:
- (void) cleanup
{
if (vertexBuffer != 0) {
glDeleteBuffers(1, &vertexBuffer);
vertexBuffer = 0;
}
if (lineIndexBuffer != 0) {
glDeleteBuffers(1, &lineIndexBuffer);
lineIndexBuffer = 0;
}
if (triangleIndexBuffer) {
glDeleteBuffers(1, &triangleIndexBuffer);
triangleIndexBuffer = 0;
}
}
五,運行效果
本示例演示了 6 中不同形狀的可編程幾何物體,並使用 Quaternion 來響應手指滑動形成的旋轉操作。示例運行效果如圖所示:

六,練習作業
在示例中,是通過編程方式來生成頂點數據與索引數據。那如果我想用已有頂點數據和索引數據來使用 VBO,那么該如何做呢?下面提供一個立方體 cube 的頂點數據和索引數據,看聰明的你能不能修改它,加入本示例中成為第七個幾何圖形,這個作業就留個你了。
// Cube 頂點數據以及索引數據
const GLfloat vertices[] = {
-1.5f, -1.5f, 1.5f, -0.577350, -0.577350, 0.577350,
-1.5f, 1.5f, 1.5f, -0.577350, 0.577350, 0.577350,
1.5f, 1.5f, 1.5f, 0.577350, 0.577350, 0.577350,
1.5f, -1.5f, 1.5f, 0.577350, -0.577350, 0.577350,
1.5f, -1.5f, -1.5f, 0.577350, -0.577350, -0.577350,
1.5f, 1.5f, -1.5f, 0.577350, 0.577350, -0.577350,
-1.5f, 1.5f, -1.5f, -0.577350, 0.577350, -0.577350,
-1.5f, -1.5f, -1.5f, -0.577350, -0.577350, -0.577350
};
const GLushort indices[] = {
// Front face
3, 2, 1, 3, 1, 0,
// Back face
7, 5, 4, 7, 6, 5,
// Left face
0, 1, 7, 7, 1, 6,
// Right face
3, 4, 5, 3, 5, 2,
// Up face
1, 2, 5, 1, 5, 6,
// Down face
0, 7, 3, 3, 7, 4
};
