iOS --- OpenGLES之頂點緩存對象VBO

在上一篇博客 iOS — OpenGLES之簡單的圖形繪製 中,使用OpenGLES繪製了基本的三角形和矩形。在矩形繪製過程中,使用到了VBO,即Vertex Buffer Object,可視爲GPU中的一塊緩衝區buffer,用於存儲頂點的所有信息。OpenGL在GPU中記錄着這個VBO的id和對應的顯存地址(或地址偏移)。
如果不使用VBO,就直接從CPU主存中傳遞頂點數據到GPU中進行運算和渲染。繪製圖形的過程,實際上就是將內存中存儲的vertices和indices等數據通過glDrawElements/glDrawArrays拷貝到GPU中。而頻繁地在CPU/GPU之間傳遞數據的效率很低,因此可使用VBO緩存頂點數據,只在初始化緩衝區及在頂點數據有變化時才需要對該緩衝區進行操作,能大大減少CPU/GPU之間的數據拷貝開銷。

繪製紅色三角形

設置viewPort,頂點數組

glViewport(0, 0, self.view.frame.size.width, self.view.frame.size.height);

GLfloat vertices[] = {
    0.0f,  0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
    0.5f,  -0.5f, 0.0f };

不使用VBO時

則不使用VBO時,繪製三角形如下:

// 給_positionSlot傳遞vertices數據
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
glEnableVertexAttribArray(_positionSlot);

// Draw triangle
glDrawArrays(GL_TRIANGLES, 0, 3);
[_eaglContext presentRenderbuffer:GL_RENDERBUFFER];

其中,glVertexAttribPointer用於傳遞頂點着色器的位置信息。函數原型:

void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,const GLvoid * pointer)

參數意義如下:
index:頂點數據在着色器程序中的屬性,這裏即_positionSlot。
size:每個頂點屬性的組件數量,這裏3表示每個頂點由三個元素組成,如0.0f, 0.5f, 0.0f。
type:每個頂點屬性的組件類型,這裏即GL_FLOAT。
normalized:指定當被訪問時,固定點數據值是否應該被歸一化或直接轉換成固定點值,這裏即GL_FALSE。
stride:指定相鄰兩個頂點數據之間的偏移量,即間隔大小。OpenGL根據該間隔從由多個頂點數據組成的數據塊中跳躍地讀取相應的頂點數據。這裏vertices數組中僅存儲頂點數據(x,y,z),因此相鄰兩個頂點數據之間的間隔本應爲 sizeof(float) * 3 。但此處傳遞默認參數0的原因在於:0表示在頂點數組中每個頂點數據都是緊密排列的,OpenGL會自動計算各個頂點數據的大小得到對應的間隔。
注意:該函數的最後一個參數pointer的意義會因是否使用VBO而不同
pointer:未使用VBO時,其指向CPU內存中的頂點數據數組,因此這裏是vertices。
而使用VBO時,表示該頂點數據在頂點緩存對象VBO(GL_ARRAY_BUFFER)中的起始偏移量,具體使用示例請看使用VBO繪製三角形的代碼。
那麼,詳細解釋了該函數的所有參數,再回頭看其未使用VBO時的調用方式:

glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);

是不是都清楚了呢?

使用VBO時

使用VBO時,先創建、綁定VBO,傳遞頂點數組

GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(_positionSlot);

// Draw triangle
glDrawArrays(GL_TRIANGLES, 0, 3);
[_eaglContext presentRenderbuffer:GL_RENDERBUFFER];

先看glVertexAttribPointer,之前說過了其各個參數的意義,這裏調用如下:

glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, 0);

最後一個參數pointer爲0,即頂點數據在VBO(GL_ARRAY_BUFFER)中的起始偏移量是0。因上邊代碼初始化了vertices數組大小的空間,綁定到GL_ARRAY_BUFFER,因此這裏的pointer理所當然就是0。

VBO的使用步驟

使用VBO的步驟一般比較固定,如下:

GLuint vertexBuffer; // VBO的id
glGenBuffers(1, &vertexBuffer); // 創建一個VBO
// 綁定該VBO到GL_ARRAY_BUFFER目標
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
// 爲該VBO申請空間,初始化並傳遞數據
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBindBuffer的第一個參數可以是GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER, GL_PIXEL_UNPACK_BUFFER等。GL_ARRAY_BUFFER用於頂點數據,而GL_ELEMENT_ARRAY_BUFFER用於頂點索引。
glBufferData的第二個參數指定了該VBO的大小,通常是頂點數組所佔用空間的大小。第三個參數爲頂點數組。第四個參數可以指定GL_STREAM_DRAW, GL_STREAM_READ, GL_STREAM_COPY, GL_STATIC_DRAW, GL_STATIC_READ, GL_STATIC_COPY, GL_DYNAMIC_DRAW, GL_DYNAMIC_READ, or GL_DYNAMIC_COPY等多種。這裏GL_STATIC_DRAW指的是頂點數據一般不會改變,而如果頂點數組會經常被改動,則可使用GL_DYNAMIC_DRAW。

再來看glBufferData函數,假如將其第二個參數改爲 sizeof(vertices) / 3 * 2,即申請的VBO大小僅能放置vertices數組中的兩個頂點數據 {0.0f, 0.5f, 0.0f} 和 { -0.5f, -0.5f, 0.0f }。則繪製的區域即僅爲座標原點(0, 0, 0)及這兩個頂點組成的區域,如圖:

這裏寫圖片描述

繪製矩形

接下來看看繪製矩形的情況。矩形都是有兩個三角形組成的。

不使用VBO時

不使用頂點索引數組

首先,按照最基本的方式依次設置頂點位置及顏色,使用glDrawArrays依次繪製頂點。

// 頂點數組
const GLfloat Vertices[] = {
    -1,-1,0,// 左下,黑色
    1,-1,0, // 右下,紅色
    -1,1,0, // 左上,藍色

    1,-1,0, // 右下,紅色
    -1,1,0, // 左上,藍色
    1,1,0,  // 右上,綠色
};

// 顏色數組
const GLfloat Colors[] = {
    0,0,0,1, // 左下,黑色
    1,0,0,1, // 右下,紅色
    0,0,1,1, // 左上,藍色

    1,0,0,1, // 右下,紅色
    0,0,1,1, // 左上,藍色
    0,1,0,1, // 右上,綠色
};

// 取出Vertex結構體的Position,賦給_positionSlot
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, Vertices);
glEnableVertexAttribArray(_positionSlot);

// Vertex結構體,偏移3個float的位置,即是Color值
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, 0, Colors);
glEnableVertexAttribArray(_colorSlot);

glDrawArrays(GL_TRIANGLES, 0, 6);

三角形的繪製方式

glDrawArrays函數的第一個參數可設置爲GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN,分別是繪製三角形的三種方式。

這裏寫圖片描述

GL_TRIANGLES是以每三個頂點繪製一個三角形。第一個三角形使用頂點V0,V1,V2,第二個使用V3,V4,V5,以此類推。如果頂點的個數n不是3的倍數,那麼最後的1個或者2個頂點會被忽略。
以上方式即在glDrawArrays函數中使用GL_TRIANGLES。
而GL_TRIANGLE_STRIP第一個三角形爲V0、V1、V2,第二個三角形爲V1、V2、V3,第三個三角形爲V2、V3、V4,依次類推。GL_TRIANGLE_FAN第一個三角形爲V0、V1、V2,第二個三角形爲V0、V2、V3,第三個三角形爲V0、V3、V4,依次類推。

所以,如果使用GL_TRIANGLE_STRIP繪製以上矩形,頂點數組與顏色數組也要相應變化:

// 頂點數組
const GLfloat Vertices[] = {
    -1,-1,0,// 左下,黑色
    1,-1,0, // 右下,紅色
    -1,1,0, // 左上,藍色
    1,1,0,  // 右上,綠色
};

// 顏色數組
const GLfloat Colors[] = {
    0,0,0,1, // 左下,黑色
    1,0,0,1, // 右下,紅色
    0,0,1,1, // 左上,藍色
    0,1,0,1, // 右上,綠色
};

glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, Vertices);
glEnableVertexAttribArray(_positionSlot);
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, 0, Colors);
glEnableVertexAttribArray(_colorSlot);

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

如果使用GL_TRIANGLE_FAN呢,因取得三角形頂點的方式不同,因此也必須對頂點數組中頂點的序列做相應調整如下,才能繪製出跟以上兩種方式一樣的矩形:

// 頂點數組
const GLfloat Vertices[] = {
    -1,1,0, // 左上,藍色
    -1,-1,0,// 左下,黑色
    1,-1,0, // 右下,紅色
    1,1,0,  // 右上,綠色
};

// 顏色數組
const GLfloat Colors[] = {
    0,0,1,1, // 左上,藍色
    0,0,0,1, // 左下,黑色
    1,0,0,1, // 右下,紅色
    0,1,0,1, // 右上,綠色
};

glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, Vertices);
glEnableVertexAttribArray(_positionSlot);
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, 0, Colors);
glEnableVertexAttribArray(_colorSlot);

glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

大家可以仔細比較一下GL_TRIANGLE_STRIP與GL_TRIANGLE_FAN兩種方式的區別。

使用頂點索引數組

僅通過頂點數組的方式來繪製圖形,會有很多頂點被重複繪製。那麼可以引入了索引數組,結合glDrawElements函數避免頂點的重複繪製,以節約資源。

// 頂點數組
const GLfloat Vertices[] = {
    -1,-1,0,// 左下,黑色
    1,-1,0, // 右下,紅色
    -1,1,0, // 左上,藍色
    1,1,0,  // 右上,綠色
};

// 顏色數組
const GLfloat Colors[] = {
    0,0,0,1, // 左下,黑色
    1,0,0,1, // 右下,紅色
    0,0,1,1, // 左上,藍色
    0,1,0,1, // 右上,綠色
};

// 索引數組
const GLubyte Indices[] = {
    0,1,2, // 三角形0
    1,2,3  // 三角形1
};

glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, Vertices);
glEnableVertexAttribArray(_positionSlot);
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, 0, Colors);
glEnableVertexAttribArray(_colorSlot);

glDrawElements(GL_TRIANGLES, sizeof(Indices)/sizeof(Indices[0]), GL_UNSIGNED_BYTE, Indices);

glDrawElements的原型是

void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices);

mode:設置GL_TRIANGLES或其他三角形組合方式。
count:索引數組中的元素個數。
type:索引數組中的元素類型。
indices:這裏是索引數組。
需要注意的是,最後一個參數indices的含義其實也是會因爲是否使用VBO而不同,這裏未使用VBO即爲索引數組。而如果使用了VBO,即表示索引數據在VBO(GL_ELEMENT_ARRAY_BUFFER)中的偏移量,使用方式請看下邊內容。

使用VBO時

有了以上的基礎,接下來再來看使用索引數組+VBO的方式繪製矩形,就容易理解得多了。
首先,將頂點的位置和顏色封裝到結構體Vertex中,

// 定義一個Vertex結構
typedef struct {
    float Position[3];
    float Color[4];
} Vertex;

// 頂點數組
const Vertex Vertices[] = {
    {{-1,-1,0}, {0,0,0,1}},// 左下,黑色
    {{1,-1,0}, {1,0,0,1}}, // 右下,紅色
    {{-1,1,0}, {0,0,1,1}}, // 左上,藍色
    {{1,1,0}, {0,1,0,1}},  // 右上,綠色
};

// index數組
const GLubyte Indices[] = {
    0,1,2, // 三角形0
    1,2,3  // 三角形1
};

接下來,設置VBO:

// setup VBOs
// GL_ARRAY_BUFFER用於頂點數組
GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
// 綁定vertexBuffer到GL_ARRAY_BUFFER對象,
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
// 爲VBO申請空間,初始化並傳遞數據
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);

// GL_ELEMENT_ARRAY_BUFFER用於頂點數組對應的indices
GLuint indexBuffer;
glGenBuffers(1, &indexBuffer);
// 綁定vertexBuffer到GL_ELEMENT_ARRAY_BUFFER對象,
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);

這裏,用到了兩個VBO,分別存儲頂點數據和索引數據,其使用方式是一致的。要注意GL_ARRAY_BUFFER與GL_ELEMENT_ARRAY_BUFFER的區別,分別對應頂點數據和索引數據。

使用VBO時,glVertexAttribPointer的使用如下:

// 取出Vertex結構體的Position,賦給_positionSlot
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
glEnableVertexAttribArray(_positionSlot);

// Vertex結構體,偏移3個float的位置,即是Color值
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid *)(sizeof(float) * 3));
glEnableVertexAttribArray(_colorSlot);

對於頂點位置,glVertexAttribPointer函數的第二個參數依舊是3,但因Vertex結構體中不僅包含了位置數據,也包含了顏色數據,因此Vertices數組中描述頂點位置的數據不是緊密排列的,故第五個參數stride不能傳遞默認參數0,而是 sizeof(Vertex) 。最後一個參數pointer爲0,即頂點位置數據在VBO中的偏移量爲0。
對於頂點顏色,第二個參數是4,即顏色數據由4個float數據組成(分別對應RGBA),第五個參數stride同樣是 sizeof(Vertex) 。最後一個參數pointer爲 (GLvoid )(sizeof(float) 3) ,即頂點顏色數據在VBO中的偏移量爲3個float的大小,即兩個相鄰的頂點顏色數據之間的間隔爲Vertex結構體中的頂點位置數據大小。由此也可看出,該偏移量是針對每個頂點數據而言的。在OpenGL的世界中,都是針對一個個位置或像素進行繪製的。

最後,使用glDrawElements繪製矩形:

glDrawElements(GL_TRIANGLE_STRIP, sizeof(Indices)/sizeof(Indices[0]), GL_UNSIGNED_BYTE, 0);

該函數在每個vertex上調用我們的vertex shader,以及每個像素調用fragment shader。使用VBO時,最後一個參數0表示索引數據在VBO(GL_ELEMENT_ARRAY_BUFFER)中的偏移量,這裏注意跟未使用VBO時進行對比。
如果不使用索引數組,使用glDrawArrays如下:

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

同時,以上關於索引VBO(GL_ELEMENT_ARRAY_BUFFER)的操作也不需要了。
相比glDrawArray, 使用頂點索引數組+glDrawElements可減少存儲和繪製重複頂點的資源消耗。

總結

  1. VBO可大大減少GPU/CPU之間頻繁傳遞數據的開銷。
  2. VBO可分爲GL_ARRAY_BUFFER和GL_ELEMENT_ARRAY_BUFFER兩種,分別用於存儲頂點數據和索引數據。
  3. 索引數組與glDrawElements結合使用,可減少存儲和繪製重複頂點的資源消耗。
  4. glVertexAttribPointer的最後一個參數的含義會因爲是否使用VBO而不同。
  5. glDrawElements的最後一個參數的含義會因爲是否使用VBO而不同。

Demo地址

本文的一系列demo都可在github中找到,DemoOpenGL,如有不準確的地方,歡迎指正。

參考資料

iOS — OpenGLES之簡單的圖形繪製
【OpenGL】理解GL_TRIANGLE_STRIP等繪製三角形序列的三種方式
OpenGL Tutorial for iOS: OpenGL ES 2.0
OpenGL ES 06 使用VBO:頂點緩存

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