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

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

這樣做導(dǎo)致的結(jié)果是,每次進(jìn)行繪制時(shí),都會(huì)進(jìn)行一次傳輸,所以繪制速度會(huì)低于顯示列表。
// 每次繪制都將 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的特點(diǎn),既方便傳輸,又要兼顧修改。
由于VA在CPU收集的頂點(diǎn)是一個(gè)整體,所以在GPU向渲染流水線提交數(shù)據(jù)是由一個(gè)整體提交的,無法在渲染時(shí)做修改;而DL雖然可以單個(gè)修改,但是渲染時(shí)卻需要等待CPU端修改完成等GPU端收集完成再進(jìn)行。
為了既提高傳輸效率,又可以使渲染時(shí)數(shù)據(jù)在GPU端也可以修改,VBO應(yīng)運(yùn)而生。

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

如圖所示,VAO保存了不同VBO的指針,用戶可以通過這些指針來對(duì)數(shù)據(jù)進(jìn)行操作。
本文中,我們先分別生成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è)置對(duì)緩沖區(qū)訪問的步長(zhǎng)為3以及相位為0,告訴著色器,這個(gè)數(shù)據(jù)輸入到著色器的第一個(gè)(索引為0)輸入變量,數(shù)據(jù)的長(zhǎng)度是3個(gè)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"是頂點(diǎn)著色器代碼中的頂點(diǎn),還記得嗎?

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

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

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

我們來定義一下數(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, // 第一個(gè)三角形
1, 2, 3 // 第二個(gè)三角形
};
建立索引緩沖對(duì)象:
unsigned int EBO;
glGenBuffers(1, &EBO);
接下來同VBO類似,將索引復(fù)制到緩沖里,
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
最終進(jìn)行繪制:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
數(shù)據(jù)調(diào)用的過程是,繪制時(shí)先從EBO從找到索引,再通過VAO中找到對(duì)應(yīng)的VBO中的頂點(diǎn),glDrawElements的參數(shù):
第一個(gè):與glDrawArrays一樣,設(shè)置顯示模式;
第二個(gè):總共要繪制的頂點(diǎn)的個(gè)數(shù);
第三個(gè):索引的類型;
第四個(gè):指定EBO中的偏移量,類似于VBO中的偏移量;
好了,就到這吧,EBO的部分不多做解釋了;

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