AB是一家?VAO與VBO

引用自:AB是一家?VAO與VBO-OpenGL技術-ZwqXin

我想大家都已經熟悉VBO了吧。在GL3.0時代的VBO大體還是處於最重要的地位,但是與此同時也出現了不少新的用法和輔助役,其中一個就是VAO。本文大致小記一下這兩者的聯繫,幫助大家理解一下這個角色。——ZwqXin.com

VBO?See[學一學,VBO]

本文來源於 ZwqXin (http://www.zwqxin.com/), 轉載請註明

原文地址:zwqxin.com/archives/ope

如果你也逐漸步進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. glBindBuffer(GL_ARRAY_BUFFER, m_nTexcoordVBO);
  5. glEnableClientState(GL_TEXTURE_COORD_ARRAY);
  6. glTexCoordPointer(2, GL_FLOAT, 0, NULL);
  7. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nIndexVBO);
  8. glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);
  9. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);
  10. glDisableClientState(GL_TEXTURE_COORD_ARRAY);
  11. glDisableClientState(GL_VERTEX_ARRAY);
  12. 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. glBindBuffer(GL_ARRAY_BUFFER, m_nQuadTexcoordVBO);
  5. glEnableVertexAttribArray(VAT_TEXCOORD);
  6. glVertexAttribPointer(VAT_TEXCOORD, 2, GL_INT, GL_FALSE, 0, NULL);
  7. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nQuadIndexVBO);
  8. glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);
  9. glDisableVertexAttribArray(VAT_POSITION);
  10. glDisableVertexAttribArray(VAT_TEXCOORD);
  11. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);
  12. 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. struct VertexArrayObject
  13. {
  14. BufferObject *pElementArrayBufferObject = NULL;
  15. VertexAttribute attributes[GL_MAX_VERTEX_ATTRIB];
  16. }

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

C++代碼 - 初始化部分

  1. glGenVertexArrays(1, &m_nQuadVAO);
  2. glBindVertexArray(m_nQuadVAO);
  3. glGenBuffers(1, &m_nQuadPositionVBO);
  4. glBindBuffer(GL_ARRAY_BUFFER, m_nQuadPositionVBO);
  5. glBufferData(GL_ARRAY_BUFFER, sizeof(fQuadPos), fQuadPos, GL_STREAM_DRAW);
  6. glEnableVertexAttribArray(VAT_POSITION);
  7. glVertexAttribPointer(VAT_POSITION, 2, GL_INT, GL_FALSE, 0, NULL);
  8. glGenBuffers(1, &m_nQuadTexcoordVBO);
  9. glBindBuffer(GL_ARRAY_BUFFER, m_nQuadTexcoordVBO);
  10. glBufferData(GL_ARRAY_BUFFER, sizeof(fQuadTexcoord), fQuadTexcoord, GL_STREAM_DRAW);
  11. glEnableVertexAttribArray(VAT_TEXCOORD);
  12. glVertexAttribPointer(VAT_TEXCOORD, 2, GL_INT, GL_FALSE, 0, NULL);
  13. glGenBuffers(1, &m_nQuadIndexVBO);
  14. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_nQuadIndexVBO);
  15. glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(nQuadIndex), nQuadIndex, GL_STREAM_DRAW);
  16. glBindVertexArray(NULL);
  17. glBindBuffer(GL_ARRAY_BUFFER, NULL);
  18. glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);

C++代碼 - 渲染部分

  1. glBindVertexArray(m_nQuadVAO);
  2. glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL);
  3. 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都用一用吧!

本文來源於 ZwqXin (http://www.zwqxin.com/), 轉載請註明

原文地址:zwqxin.com/archives/ope


推薦閱讀:

【GDC2017】Terrain Technology and Tools
為什麼需要模擬HDR
用頂點Shader實現的實時陰影
從玩具總動員到汽車總動員2
【GPU精粹與Shader編程】(一) 開篇 & 全系列11本書核心知識點總覽

TAG:計算機圖形學 |