我想大家都已經熟悉VBO了吧。在GL3.0時代的VBO大體還是處於最重要的地位,但是與此同時也出現了不少新的用法和輔助役,其中一個就是VAO。本文大致小記一下這兩者的聯繫,幫助大家理解一下這個角色。——ZwqXin.com
VBO?See[學一學,VBO]
本文來源於 ZwqXin (http://www.zwqxin.com/),
轉載請註明
原文地址:http://www.zwqxin.com/archives/opengl/vao-and-vbo-stuff.html
如果你也逐漸步進GL3.0開始的新標準,你大概會留意到傳統的繪圖方式(glVertex)已經要被廢掉了,不僅如此,以最高繪製速度爲標記的顯示列表方式也已經被印上deprecated了,這樣,在以前的文章([學一學,VBO] )中的討論,在新標準的面前都顯得沒什麼必要了。我想說的是,OpenGL對GPU的入口“頂點傳送”——或者說,繪製方式,儘量不要再選擇傳統方式(glVertex)或顯示列表(glCallList)甚至VA(vertex array)了。哪怕你是用的一個compatable的GL-context,哪怕頂點數據部分持續變化或者恆定不變,也得注意要儘量儘量使用VBO來組織你的數據。
另外的一點,就是儘量不要以客戶端狀態函數來使用VBO了。我是說——glEnableClientState/glDisableClientState,還有glVertexPointer這類函數。VBO的本意是把本地(GL客戶端)的數據完全交給GPU(GL服務端)來管理,所以若非爲了數據的更新,你完全可以在調用glBufferData之後選擇扔棄保存在本地內存中的數據。VBO可以說只有在傳輸數據的時候跟本地客戶端有聯繫,它的狀態是服務端(我們的流水線)管理的,當初沿用VA的那些客戶端狀態函數,還有一個原因就是它們方便地與shader裏面的固定attribute(gl_Position之類)建立聯繫【見[OpenGL/GLSL數據傳遞小記(2.x)]】,但是GLSL已經也不推薦使用那些attrbute了。(事實上,以上這些都是deprecated的了。)
- glBindBuffer(GL_ARRAY_BUFFER, m_nPositionVBO);
- glEnableClientState(GL_VERTEX_ARRAY);
- glVertexPointer(2, GL_FLOAT, 0, NULL);
- glBindBuffer(GL_ARRAY_BUFFER, m_nTexcoordVBO);
- glEnableClientState(GL_TEXTURE_COORD_ARRAY);
- glTexCoordPointer(2, GL_FLOAT, 0, NULL);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nIndexVBO);
- glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);
- glDisableClientState(GL_TEXTURE_COORD_ARRAY);
- glDisableClientState(GL_VERTEX_ARRAY);
- glBindBuffer(GL_ARRAY_BUFFER, NULL);
- glBindBuffer(GL_ARRAY_BUFFER, m_nQuadPositionVBO);
- glEnableVertexAttribArray(VAT_POSITION);
- glVertexAttribPointer(VAT_POSITION, 2, GL_INT, GL_FALSE, 0, NULL);
- glBindBuffer(GL_ARRAY_BUFFER, m_nQuadTexcoordVBO);
- glEnableVertexAttribArray(VAT_TEXCOORD);
- glVertexAttribPointer(VAT_TEXCOORD, 2, GL_INT, GL_FALSE, 0, NULL);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nQuadIndexVBO);
- glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);
- glDisableVertexAttribArray(VAT_POSITION);
- glDisableVertexAttribArray(VAT_TEXCOORD);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);
- glBindBuffer(GL_ARRAY_BUFFER, NULL);
以上兩段是效果一致的VBO渲染部分的代碼。儘量用第二種吧。使用第二種的前提是你使用shader來進行頂點處理,VAT_POSITION/VAT_TEXCOORD需要與Shader裏代表頂點/紋理座標的attribute變量建立聯繫(參考[OpenGL/GLSL數據傳遞小記(2.x)]),在這個GL3.0之後的時代裏,這種前提也算不上什麼前提就是了。我們來囫圇吞棗地猜測一下OpenGL是怎麼處理VBO的數據的。
1. VBO
與其他buffer object一樣,VBO歸根到底是顯卡存儲空間裏的一塊緩存區(Buffer)而已,這個Buffer有它的名字(VBO的ID),OpenGL在GPU的某處記錄着這個ID和對應的顯存地址(或者地址偏移,類似內存)。用代碼看看吧:
- //生成一個Buffer的ID,不管是什麼類型的
- glGenBuffers(1, &m_nQuadVBO);
- //綁定ID,同時也指定該ID對應的buffer的信息類型是GL_ARRAY_BUFFER
- glBindBuffer(GL_ARRAY_BUFFER, m_nQuadVBO);
- //爲該ID指定一塊指定大小的存儲區域(區域的位置大抵由末參數影響), 傳輸數據
- glBufferData(GL_ARRAY_BUFFER, sizeof(fQuadData), fQuadData, GL_STREAM_DRAW);
這裏是VBO的初始化階段。在這裏我們看到了這是對位置,還是顏色,還是紋理座標,還是法線,還是其他頂點屬性進行設置的嗎?是的,這個信息是:起碼在初始化階段,一個VBO對於交給它存儲的數據到底是什麼,完全不知道。我們此時再看回上面兩段渲染部分的代碼,就明白了:哦,原來這都是在渲染時確定的!
對於第一段渲染代碼,glVertexPointer(2, GL_FLOAT, 0, NULL)這個函數指定了VBO裏的是什麼數據——頂點位置,float類型,2個float指涉一個頂點位置,在區域裏無偏移地採集數據,等等。之後的glDrawElements只不過根據組織模式(GL_TRIANGLES,這個是直接交給vertex處理後的Geometry處理的)和索引數據去採集VBO裏的這些數據罷了——它從某個地方獲取了glBindBuffer指定的位置,還有glVertexPointer設定的信息(由glEnableClientState啓用),它進行繪製所需要的一切——這個地方,就是所謂的GL-Context吧,那個保存了所有運行時流水線狀態的東西。
對於第二段渲染代碼,大體是一樣的,只是glVertexAttribPointer使用第一個參數(location)指涉對應vertex-shader裏哪個in attribute。VBO在渲染階段才指定數據位置和“頂點信息”(Vertex Specification),然後根據此信息去解析緩存區裏的數據,聯繫這兩者中間的橋樑是GL-Contenxt。GL-context整個程序一般只有一個,所以如果一個渲染流程裏有兩份不同的繪製代碼,GL-context就負責在它們之間進行狀態切換。這也是爲什麼要在渲染過程中,在每份繪製代碼之中有glBindBuffer/glEnableVertexAttribArray/glVertexAttribPointer。那麼優化方法就來了——把這些都放到初始化時候完成吧!——這樣做的限制條件是“負責記錄狀態的GL-context整個程序一般只有一個”,那麼就不直接用GL-context記錄,用別的東西做狀態記錄吧——這個東西針對"每份繪製代碼“有一個,記錄該次繪製所需要的所有VBO所需信息,把它保存到GPU特定位置,繪製的時候直接在這個位置取信息繪製。
於是,VAO誕生了。
2.VAO
VAO的全名是Vertex Array Object,首先,它不是Buffer-Object,所以不用作存儲數據;其次,它針對”頂點“而言,也就是說它跟”頂點的繪製“息息相關,在GL3.0的世界觀裏,這相當於”與VBO息息相關“。(提示,它跟VA真是蝦米關係都沒有的,嘛,雖然這的確讓人誤會,我最初見到這個名詞時也誤會了的說。)
按上所述,它的定位是state-object(狀態對象,記錄存儲狀態信息)。這明顯區別於buffer-object。如果有人碎碎念”既然是記錄頂點的信息,爲什麼不叫vertex attribute object“呢?我想說這些孩子你們真沒認真看文章嘛——VAO記錄的是一次繪製中做需要的信息,這包括”數據在哪裏-glBindBuffer(GL_ARRAY_BUFFER)“、”數據的格式是怎樣的-glVertexAttribPointer“(頂點位置的數據在哪裏,頂點位置的數據的格式是怎樣的/紋理座標的數據在哪裏,紋理座標的數據的格式是怎樣的....視乎你讓它關聯多少個VBO、VBO裏有多少種數據),順帶一提的是,這裏的狀態還包括這些屬性關聯的shader-attribute的location的啓用(glEnableVertexAttribArray)、這些頂點屬性對應的頂點索引數據的位置(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER),如果你指定了的話)。在GL的wiki裏把這些”信息“抽象成一個屬性數據體:
- struct VertexAttribute
- {
- bool bIsEnabled = GL_FALSE;
- int iSize = 4; //This is the number of elements in this attribute, 1-4.
- unsigned int iStride = 0;
- VertexAttribType eType = GL_FLOAT;
- bool bIsNormalized = GL_FALSE;
- bool bIsIntegral = GL_FALSE;
- void * pBufferObjectOffset = 0;
- BufferObject * pBufferObj = 0;
- };
- struct VertexArrayObject
- {
- BufferObject *pElementArrayBufferObject = NULL;
- VertexAttribute attributes[GL_MAX_VERTEX_ATTRIB];
- }
這裏,VertexArrayObject 就包括了一個Index-VBO【[索引頂點的VBO與多重紋理下的VBO] 】(可以沒有,例如繪製用的是glDrawArray)還有一些VertexAttribute。後者包括頂點屬性的格式和位置和一個啓用與否的狀態。這些都對應了上述討論的那幾個函數(注意glVertexAttribPointer和glVertexAttribIPointer的選擇決定bool bIsIntegral,數據是否整型不可規範化)。那麼,現在我們可以知道VAO的用法了:
- glGenVertexArrays(1, &m_nQuadVAO);
- glBindVertexArray(m_nQuadVAO);
- glGenBuffers(1, &m_nQuadPositionVBO);
- glBindBuffer(GL_ARRAY_BUFFER, m_nQuadPositionVBO);
- glBufferData(GL_ARRAY_BUFFER, sizeof(fQuadPos), fQuadPos, GL_STREAM_DRAW);
- glEnableVertexAttribArray(VAT_POSITION);
- glVertexAttribPointer(VAT_POSITION, 2, GL_INT, GL_FALSE, 0, NULL);
- glGenBuffers(1, &m_nQuadTexcoordVBO);
- glBindBuffer(GL_ARRAY_BUFFER, m_nQuadTexcoordVBO);
- glBufferData(GL_ARRAY_BUFFER, sizeof(fQuadTexcoord), fQuadTexcoord, GL_STREAM_DRAW);
- glEnableVertexAttribArray(VAT_TEXCOORD);
- glVertexAttribPointer(VAT_TEXCOORD, 2, GL_INT, GL_FALSE, 0, NULL);
- glGenBuffers(1, &m_nQuadIndexVBO);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nQuadIndexVBO);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(nQuadIndex), nQuadIndex, GL_STREAM_DRAW);
- glBindVertexArray(NULL);
- glBindBuffer(GL_ARRAY_BUFFER, NULL);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);
- glBindVertexArray(m_nQuadVAO);
- glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);
- glBindVertexArray(NULL);
以上就是VAO的使用方法了,很直觀吧?
使用VAO的好處?看上面那麼簡潔的渲染部分代碼就夠了。
你甚至可以認爲VAO就是一個狀態容器,其中粗體字的那幾行就是它以及它所”包含“的東西——填充了”VertexArrayObject結構體“的東西。注意:1.沒有一個合適的地方給glDisableVertexAttribArray了,事實上調用glBindVertexArray(NULL)的時候裏面所有狀態都”關掉“了,也就沒所謂針對頂點屬性的location做其他什麼;2.glBindBuffer(GL_ARRAY_BUFFER, NULL)/glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL)一定要在glBindVertexArray(NULL)後面(不然VAO就把它們也包含了,最後就渲染不出東西了);3.glDrawElements裏面的東西(頂點索引的屬性狀態)VAO可沒記錄保存哦;4.glVertexPointer那類函數理論上也可以,但是建議還是不要混用deprecated的函數進去了。