AB是一家?VAO與VBO

我想大家都已經熟悉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的了。)

C++代碼1
  1. glBindBuffer(GL_ARRAY_BUFFER, m_nPositionVBO);  
  2. glEnableClientState(GL_VERTEX_ARRAY);  
  3. glVertexPointer(2, GL_FLOAT, 0, NULL);  
  4.   
  5. glBindBuffer(GL_ARRAY_BUFFER, m_nTexcoordVBO);  
  6. glEnableClientState(GL_TEXTURE_COORD_ARRAY);  
  7. glTexCoordPointer(2, GL_FLOAT, 0, NULL);  
  8.   
  9. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nIndexVBO);  
  10.   
  11. glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);  
  12.   
  13. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);  
  14.   
  15. glDisableClientState(GL_TEXTURE_COORD_ARRAY);  
  16. glDisableClientState(GL_VERTEX_ARRAY);  
  17. glBindBuffer(GL_ARRAY_BUFFER, NULL);  
C++代碼2
  1. glBindBuffer(GL_ARRAY_BUFFER, m_nQuadPositionVBO);  
  2. glEnableVertexAttribArray(VAT_POSITION);  
  3. glVertexAttribPointer(VAT_POSITION, 2, GL_INT, GL_FALSE, 0, NULL);  
  4.   
  5. glBindBuffer(GL_ARRAY_BUFFER, m_nQuadTexcoordVBO);  
  6. glEnableVertexAttribArray(VAT_TEXCOORD);  
  7. glVertexAttribPointer(VAT_TEXCOORD, 2, GL_INT, GL_FALSE, 0, NULL);  
  8.   
  9. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nQuadIndexVBO);  
  10.   
  11. glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);  
  12.   
  13. glDisableVertexAttribArray(VAT_POSITION);  
  14. glDisableVertexAttribArray(VAT_TEXCOORD);  
  15.   
  16. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);  
  17. 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和對應的顯存地址(或者地址偏移,類似內存)。用代碼看看吧:

C++代碼
  1.     //生成一個Buffer的ID,不管是什麼類型的  
  2. glGenBuffers(1, &m_nQuadVBO);   
  3. //綁定ID,同時也指定該ID對應的buffer的信息類型是GL_ARRAY_BUFFER  
  4. glBindBuffer(GL_ARRAY_BUFFER, m_nQuadVBO);  
  5. //爲該ID指定一塊指定大小的存儲區域(區域的位置大抵由末參數影響),  傳輸數據      
  6.     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裏把這些”信息“抽象成一個屬性數據體:

  1. struct VertexAttribute  
  2. {  
  3.     bool bIsEnabled = GL_FALSE;  
  4.     int iSize = 4; //This is the number of elements in this attribute, 1-4.  
  5.     unsigned int iStride = 0;  
  6.     VertexAttribType eType = GL_FLOAT;  
  7.     bool bIsNormalized = GL_FALSE;  
  8.     bool bIsIntegral = GL_FALSE;  
  9.     void * pBufferObjectOffset = 0;  
  10.     BufferObject * pBufferObj = 0;  
  11. };  
  12.    
  13. struct VertexArrayObject  
  14. {  
  15.     BufferObject *pElementArrayBufferObject = NULL;  
  16.     VertexAttribute attributes[GL_MAX_VERTEX_ATTRIB];  
  17. }  

這裏,VertexArrayObject 就包括了一個Index-VBO【[索引頂點的VBO與多重紋理下的VBO】(可以沒有,例如繪製用的是glDrawArray)還有一些VertexAttribute。後者包括頂點屬性的格式和位置和一個啓用與否的狀態。這些都對應了上述討論的那幾個函數(注意glVertexAttribPointer和glVertexAttribIPointer的選擇決定bool bIsIntegral,數據是否整型不可規範化)。那麼,現在我們可以知道VAO的用法了:

C++代碼 - 初始化部分
  1. glGenVertexArrays(1, &m_nQuadVAO);  
  2. glBindVertexArray(m_nQuadVAO);  
  3.   
  4.   
  5. glGenBuffers(1, &m_nQuadPositionVBO);  
  6. glBindBuffer(GL_ARRAY_BUFFER, m_nQuadPositionVBO);  
  7. glBufferData(GL_ARRAY_BUFFER, sizeof(fQuadPos), fQuadPos, GL_STREAM_DRAW);  
  8.   
  9. glEnableVertexAttribArray(VAT_POSITION);  
  10. glVertexAttribPointer(VAT_POSITION, 2, GL_INT, GL_FALSE, 0, NULL);
  11.   
  12. glGenBuffers(1, &m_nQuadTexcoordVBO);  
  13. glBindBuffer(GL_ARRAY_BUFFER, m_nQuadTexcoordVBO);  
  14. glBufferData(GL_ARRAY_BUFFER, sizeof(fQuadTexcoord), fQuadTexcoord, GL_STREAM_DRAW);  
  15.   
  16. glEnableVertexAttribArray(VAT_TEXCOORD);  
  17. glVertexAttribPointer(VAT_TEXCOORD, 2, GL_INT, GL_FALSE, 0, NULL);  
  18.   
  19. glGenBuffers(1, &m_nQuadIndexVBO);  
  20. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nQuadIndexVBO);  
  21. glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(nQuadIndex), nQuadIndex, GL_STREAM_DRAW);  
  22.   
  23.   
  24. glBindVertexArray(NULL);  
  25.   
  26. glBindBuffer(GL_ARRAY_BUFFER, NULL);  
  27. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);  
C++代碼 - 渲染部分
  1. glBindVertexArray(m_nQuadVAO);  
  2.   
  3. glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);  
  4.   
  5. 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的函數進去了。

那麼,既然AB是一家,那就兩個站都吧!哦,不對,兩個O都用一用吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章