大家好,歡迎來到聽風的OpenGL日常。
寫在前面
上回說到,預期的三角形并沒有,并沒有,并沒有渲染出來,今天我們來補充下,看看問題出在哪里。
本文重點
我們先來搞清楚VAO,VBO緩存到底做的是什么工作?
首先是VBO(vertex buffer object),為什么我們要用VBO?
不使用VBO時,我們每次繪制(glDrawArrays)圖形時都是從本地內(nèi)存處獲取頂點數(shù)據(jù)然后傳輸給OpenGL來繪制,這樣就會頻繁的操作CPU->GPU增大開銷,從而降低效率。
使用VBO,我們就能把頂點數(shù)據(jù)緩存到GPU開辟的一段內(nèi)存中,然后使用時不必再從本地獲取,而是直接從顯存中獲取,這樣就能提升繪制的效率。
在講清楚這個概念之前,我們還要補充一些概念;
glBegin/glEnd
以此文的例子解釋,我們在傳遞頂點位置數(shù)據(jù)的時候,在OpenGL舊版本里,是通過glVertex逐個從CPU傳遞到GPU的,代碼示例如下:
glBegin(GL_TRIANGLES);
glVertex(0.0f, 0.0f);
glVertex(1.0f, 0.0f);
glVertex(0.0f, 1.0f);
glEnd();
這樣每進行一次glVertex調(diào)用就會向GPU傳遞一次,由于傳輸是同步的,所以效率很低;

于是:
DL
Display List(顯示列表)的出現(xiàn)可以使CPU在傳輸?shù)倪^程中等待數(shù)據(jù)打包完成,待其結(jié)束一次性發(fā)送到GPU,代碼如:
GLuint listName = glGenLists (1);
glNewList (listName, GL_COMPILE);
glBegin (GL_TRIANGLES);
glVertex2f (0.0, 0.0);
glVertex2f (1.0, 0.0);
glVertex2f (0.0, 1.0);
glEnd ();
glEndList ();
...
// 繪制(不傳輸數(shù)據(jù))
glCallList(listName);

顯示列表加快了傳輸效率,但是繪制時是一次性的,那么如果列表中的某單個頂點發(fā)生變化時,那么就需要CPU重新生成新的頂點再發(fā)送到GPU,GPU收集完成后完成繪制,這樣做是極其浪費資源的,當每一幀都有變化時,它就退化成了單個頂點傳輸?shù)姆绞健?/p>
VA
Vertex Array,頂點數(shù)據(jù)要區(qū)別于我們開頭提到的VAO,它跟緩存是沒有關(guān)系的,它也是一種傳輸方案。VA也是通過收集頂點的方式來減少傳輸次數(shù),但與顯示列表不同的是,CPU端將會負責收集所有頂點,收集完成后一次性傳輸?shù)紾PU再進行繪制。

這樣做導致的結(jié)果是,每次進行繪制時,都會進行一次傳輸,所以繪制速度會低于顯示列表。
// 每次繪制都將 vertices 傳輸一次
GLfloat vertices[] = {
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f
}
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(2,GL_FLOAT,0,vertices);
glDrawArray(GL_TRIANGLES, 0, 3);
VBO
VBO是結(jié)合DL和VA的特點,既方便傳輸,又要兼顧修改。
由于VA在CPU收集的頂點是一個整體,所以在GPU向渲染流水線提交數(shù)據(jù)是由一個整體提交的,無法在渲染時做修改;而DL雖然可以單個修改,但是渲染時卻需要等待CPU端修改完成等GPU端收集完成再進行。
為了既提高傳輸效率,又可以使渲染時數(shù)據(jù)在GPU端也可以修改,VBO應運而生。

這樣一來VBO保存了一份頂點數(shù)據(jù),修改操作可以直接在GPU上進行,修改完成直接繪制。
所以按照這樣理解,它的傳輸與修改是分開的,體現(xiàn)在代碼上:
//生成VBO,并傳輸保存到GPU上
GLuint vbo;
glGenBuffer(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STREAM_DRAW);
...
// 繪制時直接從VBO中取得頂點數(shù)據(jù)
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2, (void*)0);
glDrawArray(GL_TRIANGLES, 0, 3);
...
但是這里只用VBO進行渲染沒有可以參考的代碼,本人也是進行了實驗但是最終沒有畫出來,希望有厲害的小伙伴可以一起來交流一下這個問題,這里就留作一個探索。
VAO
本文最終采用的是VAO結(jié)合VBO畫出的三角形,上篇我們討論過了,結(jié)果沒有出來,今天我們把坑填上。
Vertex Array Object,頂點數(shù)據(jù)對象是為了簡化VBO的流程,當所要傳輸?shù)腣BO有很多的時候,我們需要管理多個VBO,這樣對每個VBO都進行記錄會比較亂,我們首先想到的管理方式就是用一個數(shù)組將它們保存起來,沒錯,這就是VAO,很直觀。

如圖所示,VAO保存了不同VBO的指針,用戶可以通過這些指針來對數(shù)據(jù)進行操作。
本文中,我們先分別生成VAO和VBO,將VBO綁定到VAO中,將VAO綁定到緩存里
//VAO
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
//VBO
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//設(shè)置對緩沖區(qū)訪問的步長為3以及相位為0,告訴著色器,這個數(shù)據(jù)輸入到著色器的第一個(索引為0)輸入變量,數(shù)據(jù)的長度是3個float
GLuint uPos = glGetAttribLocation( shaderProgram, "aPos" );
glVertexAttribPointer(uPos, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(uPos);
//delete buffer and array
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
這里,glGetAttribLocation( shaderProgram, "aPos" );一句中,shaderProgram就是我們上節(jié)中編譯好的OpenGL程序,"aPos"是頂點著色器代碼中的頂點,還記得嗎?

這里的location你可以修改下它的值,看看結(jié)果uPos的值,會有助于理解它的意義,當然著色器關(guān)鍵字這里先不進行討論。
glVertexAttribPointer(uPos, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
重點討論一下這個函數(shù):
第一個參數(shù):對應著色器代碼中的location;
第二個參數(shù):頂點屬性的大小,在這里是vec3,所以是3;
第三個參數(shù):數(shù)據(jù)類型,沒什么好說的;
第四個參數(shù):定義是否希望數(shù)據(jù)被標準化(Normalize)。如果我們設(shè)置為GL_TRUE,所有數(shù)據(jù)都會被映射到0(對于有符號型signed數(shù)據(jù)是-1)到1之間;
第五個參數(shù):第五個參數(shù)叫做步長(Stride),還是用圖來解釋一下吧,比較直觀;
第六個參數(shù):表示位置數(shù)據(jù)在緩沖中起始位置的偏移量(Offset),當有多個VBO里,可以通過偏移量進行位置鎖定;
上面的第五個參數(shù)中的步長為3,即在VBO里每一個頂點占12個位置,每個位置所占字節(jié)由其保存數(shù)據(jù)類型決定。

函數(shù)glVertexAttribPointer給出了如何從VBO中取得頂點數(shù)據(jù)的方式,所謂的OpenGL位置學(我又開始胡說八道了)。
接下來,快結(jié)束戰(zhàn)斗了,畫三角形吧。
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays( GL_TRIANGLES, 0, 3 );
那么我們就愉快的結(jié)束了。等下!??!

EBO
對于VAO的這種方式,我們有點小想法,現(xiàn)有一個問題,如果我們要畫兩個三角形,你覺得最少可以用幾個頂點呢?答案肯定是4個。但是如果用前面的方法,恐怕我們需要至少6個來完成,那么EBO,索引緩沖對象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)的出現(xiàn)就是為了重復利用頂點。
個人覺得索引這個概念特別好,將頂點用索引作標記,當使用頂點時用索引間接訪問,如圖:

我們來定義一下數(shù)據(jù)和索引;
float vertices2[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
unsigned int indices[] = { // 注意索引從0開始!
0, 1, 3, // 第一個三角形
1, 2, 3 // 第二個三角形
};
建立索引緩沖對象:
unsigned int EBO;
glGenBuffers(1, &EBO);
接下來同VBO類似,將索引復制到緩沖里,
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
最終進行繪制:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
數(shù)據(jù)調(diào)用的過程是,繪制時先從EBO從找到索引,再通過VAO中找到對應的VBO中的頂點,glDrawElements的參數(shù):
第一個:與glDrawArrays一樣,設(shè)置顯示模式;
第二個:總共要繪制的頂點的個數(shù);
第三個:索引的類型;
第四個:指定EBO中的偏移量,類似于VBO中的偏移量;
好了,就到這吧,EBO的部分不多做解釋了;

總結(jié)
BE -> DL -> VA -> VBO -> VAO -> EBO;
如果你最終懂得了這個鏈條的來源,那么恭喜你已經(jīng)理解了。
本文參考:
最后這篇復現(xiàn)沒有成功,希望有復現(xiàn)的朋友可以給出點提示。