Vertex Buffer Object
對於經歷過fixed pipeline的我來講,VBO的出現對於渲染效能提升讓人記憶深刻。完了,暴露年齡了~
//immediate mode
glBegin(GL_TRIANGLES);
glNormal3f(...);
glVertex3f(...);
glEnd();
//display list
list = glGenLists(1);
glNewList(list, GL_COMPILE);
glBegin(GL_TRIANGLES);
glNormal3f(...);
glVertex3f(...);
glEnd();
glEndList();
glCallList(list);
//vertex array
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(...);
glEnableClientState(GL_COLOR_ARRAY);
glColorPointer(...);
glDrawArrays(...);
上面的程式碼是遠古時期的OpenGL繪製圖元的執行流程,不懂也不用追究了,因為實在太老了。
接下來我們進入正題。
VBO標識的是顯示卡中的一塊儲存區域,我們可以從記憶體中向它傳送頂點資料(空間位置,紋理座標,法線等等),然後在draw的時候作為vertex attribute進行使用。
void init()
{
GLfloat position[] = //空間位置
{
-0.8f, -0.8f, 0.0f,
0.8f, -0.8f, 0.0f,
0.0f, 0.8f, 0.0f
};
GLfloat color[] = //顏色
{
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f
};
GLuint vbo[2] = {0};
glCreateBuffers(2, vbo); //建立buffer物件用來表示vbo
glNamedBufferData(vbo[0], 9 * sizeof(GLfloat), position, GL_STATIC_DRAW); //向vertex buffer上傳資料
glNamedBufferData(vbo[1], 9 * sizeof(GLfloat), color, GL_STATIC_DRAW);
}
至此,我們建立好了兩個VBO並分別存放了空間位置資料和顏色資料。
我們的shader仍然是最簡單的shader:
//vertex shader
#version 460 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
layout(location = 0) out vec3 vs_out_color;
void main(void)
{
vs_out_color = color;
gl_Position = vec4(position, 1.0);
}
//fragment shader
#version 460 core
layout(location = 0) in vec3 fs_in_color;
layout(location = 0) out vec4 frag_color;
void main(void)
{
frag_color = vec4(fs_in_color, 1.0);
}
接下來的問題就是我們如何把VBO與vertex attribute (location) 關聯起來,這時候VAO就閃亮登場了。
Vertex Array Object
GLuint vao = 0;
void init()
{
... //set up vbo
glCreateVertexArrays(1, &vao); //建立vao
//啟用vertex attribute 0 和 1,其中0和1分別與vertex shader中的position(location = 0)和color(location = 1)對應
glEnableVertexArrayAttrib(vao, 0);
glEnableVertexArrayAttrib(vao, 1);
glVertexArrayVertexBuffer(vao, 3, vbo[0], 0, sizeof(GLfloat) * 3); //設定vbo的binding point
glVertexArrayVertexBuffer(vao, 5, vbo[1], 0, sizeof(GLfloat) * 3);
glVertexArrayAttribBinding(vao, 0, 3); //設定vertex attribute的binding point,須與對應的vbo bind到同一個binding point上
glVertexArrayAttribFormat(vao, 0, 3, GL_FLOAT, GL_FALSE, 0); //指定vertex attribute的頂點規範,相當於告訴OpenGL如何解析對應的vbo資料,之後vertex shader就能夠拿到正確的vertex attribute
glVertexArrayAttribBinding(vao, 1, 5);
glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 0);
}
可以看到,自從4.5版本增加了DSA,API的執行順序不是那麼的重要了,因為呼叫OpenGL的命令需要顯式的指定handle,而不是把這個handle繫結到當前的OpenGL Context(如上述程式碼,每次我們都傳入了vao)。另外對於狀態的從屬關係,也更加明確了。
我們把vertex attribute和對應的vbo繫結到了同一個binding point上,相當於告訴OpenGL,vertex attribute的資料來自哪個vbo。這裡我故意把binding point的值分別設定為3和5(其實可以設定為0和1),是擔心有同學會把vertex attribute binding point和vertex attribute index弄混淆了。
另外需要注意的是glVertexArrayVertexBuffer
最後一個引數指定了vbo中元素之間的stride,與glVertexAttribPointer
不同的是,就算vbo中的元素是緊挨著的,也必須設定正確的stride值,而不能設定為0。因為在呼叫glVertexArrayVertexBuffer
的時候,OpenGL對於vbo中的資料該如何解析絲毫不知情。而之所以你之前用到的glVertexAttribPointer
的stride可以設定為0,是因為這個命令同時指定了每個元素的型別(比如GL_FLOAT)以及size(比如由3個GL_FLOAT組成),相當於OpenGL會自動幫我們去算正確的stride的值。有的同學可能會說,glVertexArrayAttribFormat
不是指定了如何解析vbo中的資料嗎,但是你有沒有想過:glVertexArrayAttribFormat
不一定在glVertexArrayAttribBinding
之前呼叫,所以在呼叫glVertexArrayAttribBinding
的時候OpenGL可能還不知道vbo的資料資訊。
我們已經vao設定好了所有必要的資訊了,現在用它進行render
void render()
{
...
glBindVertexArray(vao); //繫結vao
glDrawArrays(GL_TRIANGLES, 0, 3 ); //draw
glBindVertexArray(0); //draw完成,將當前context下的Vertex Array繫結到一個無效的handle上
}
VAO先講到這裡,下面我們看一下EBO。
Element Buffer Object
所謂EBO,就是把頂點索引資料儲存到buffer中,然後用這些索引去vertex buffer中查詢對應的頂點來繪製圖元,以避免在vertex buffer中存放冗餘的頂點資訊。
//繪製一個矩形
void init()
{
GLfloat position[] =
{
-0.5f, 0.5f,
-0.5f, -0.5f,
0.5f, 0.5f,
0.5f, -0.5f,
};
GLfloat color[] =
{
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f
};
GLubyte index[] =
{
0, 1, 3,
2, 0, 3
};
glCreateBuffers(2, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glNamedBufferData(vbo[0], 8 * sizeof(GLfloat), position, GL_STATIC_DRAW);
glNamedBufferData(vbo[1], 12 * sizeof(GLfloat), color, GL_STATIC_DRAW);
glCreateBuffers(1, &ebo); //建立buffer物件
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); //將buffer物件當作GL_ELEMENT_ARRAY_BUFFER
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); //為當前的OpenGL Context的EBO置為無效值
glNamedBufferData(ebo, 6 * sizeof(GLubyte), index, GL_STATIC_DRAW); //向element array buffer傳輸索引資料
glCreateVertexArrays( 1, &vao );
glEnableVertexArrayAttrib(vao, 0);
glEnableVertexArrayAttrib(vao, 1);
glVertexArrayVertexBuffer(vao, 3, vbo[0], 0, sizeof(GLfloat) * 2);
glVertexArrayVertexBuffer(vao, 5, vbo[1], 0, sizeof(GLfloat) * 3);
glVertexArrayAttribBinding(vao, 0, 3);
glVertexArrayAttribFormat(vao, 0, 2, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribBinding(vao, 1, 5);
}
同學們只需要關注我加註釋的那一段程式碼。可以發現,其實EBO的建立過程與VBO極為類似。
不過很遺憾,本來我以為可以同vbo一樣,通過binding point或者其它手段建立EBO和VAO的聯絡,可惜沒找到。所以我們在render的時候,除了要繫結VAO外,還要把用到的EBO繫結至當前的OpenGL Context。
void render()
{
glBindVertexArray(vao);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); //繫結ebo
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (GLvoid*)(nullptr));
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
draw命令也不能用glDrawArrays
,而是用glDrawElements
。現在我確信不存在可以建立EBO和VAO之間聯絡的API了,因為glDrawElements
的最後的兩個引數分別表示EBO存放的資料型別和起始位置的位元組偏移。如果存在這樣的API,那麼這兩個引數的資訊肯定是儲存到了VAO中了(參照VBO和VAO)。
小結:
- VBO表示顯示卡中用於存放vertex attribute資料的一塊快取。
- VAO通過vertex attribute binding point建立vertex attribute index與VBO之間的聯絡,並且在render的時候,只需要繫結一個VAO即可進行draw,減少了狀態切換,提升渲染效能。
- EBO以索引的形式對VBO中的頂點進行多次利用,但是無法建立EBO與VAO之間的聯絡,所以每次draw之前,需要顯式的繫結EBO。