VBO、VAO和EBO

胖Po發表於2021-05-15

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弄混淆了。

vertex attribute binding point

另外需要注意的是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)。

小結:

  1. VBO表示顯示卡中用於存放vertex attribute資料的一塊快取。
  2. VAO通過vertex attribute binding point建立vertex attribute index與VBO之間的聯絡,並且在render的時候,只需要繫結一個VAO即可進行draw,減少了狀態切換,提升渲染效能。
  3. EBO以索引的形式對VBO中的頂點進行多次利用,但是無法建立EBO與VAO之間的聯絡,所以每次draw之前,需要顯式的繫結EBO。

相關文章