GL_ARB_vertex_buffer_object擴展致力於提供頂點數組與顯示列表的優勢來提升OpenGL效率,同時避免它們實現上的不足。頂點緩存對象(VBO)准許頂點數組數據存放在服務端的高性能顯卡內存中,且提供高效數據傳輸。如果緩存對象用於保存像素數據,就被稱為像素緩存對象(PBO)。
使用頂點數組可以降低函數調用次數與降低共享頂點的重復使用。然而,頂點數組的不足之處是頂點數組函數處在客戶端狀態中,且每次引用都須向服務端重新發送數據。
此外,顯示列表為服務端函數,因此,它並不受限於數據傳輸的開銷。不過,一旦顯示列表編譯完成,顯示列表中的數據不能夠修改。
頂點緩存對象(VBO)在服務器端高性能內存中為頂點屬性創建“緩存對象”,並且提供引用這些數組的訪問函數,這些函數與頂點數組中使用的函數相同,如glVertexPointer()、glNormalPointer()、glTexCoordPointer()等。
頂點緩存對象中的內存管理根據用戶提示(“target”與“Usage”模式)將緩存對象放置在最合適內存位置。因此,內存管理能夠通過在系統、AGP與顯卡內存三種內存之間做出平衡的方式優化緩存。
與顯示列表不同,可以通過映射緩存到客戶端內存空間的方式讀取與更新頂點緩存對象中的數據。
VBO的另一個重要優勢是就像顯示列表與紋理那樣可以在多個客戶端共享緩存數據。因為VBO處在服務器端,多個客戶端可以通過相應的標示符訪問同一個緩存。
創建VBO
創建VBO需要3個步驟:
- 使用glGenBuffers()生成新緩存對象。
- 使用glBindBuffer()綁定緩存對象。
- 使用glBufferData()將頂點數據拷貝到緩存對象中。
glGenBuffers()
glGenBuffers()創建緩存對象並且返回緩存對象的標示符。它需要2個參數:第一個為需要創建的緩存數量,第二個為用於存儲單一ID或多個ID的GLuint變量或數組的地址。
void glGenBuffers(GLsizei n, GLuint* buffers)
glBindBuffer()
當緩存對象創建之后,在使用緩存對象之前,我們需要將緩存對象連接到相應的緩存上。glBindBuffer()有2個參數:target與buffer。
void glBindBuffer(GLenum target, GLuint buffer)
target告訴VBO該緩存對象將保存頂點數組數據還是索引數組數據:GL_ARRAY_BUFFER或GL_ELEMENT_ARRAY。任何頂點屬性,如頂點坐標、紋理坐標、法線與顏色分量數組都使用GL_ARRAY_BUFFER。用於glDraw[Range]Elements()的索引數據需要使用GL_ELEMENT_ARRAY綁定。注意,target標志幫助VBO確定緩存對象最有效的位置,如有些系統將索引保存AGP或系統內存中,將頂點保存在顯卡內存中。
當第一次調用glBindBuffer(),VBO用0大小的內存緩存初始化該緩存,並且設置VBO的初始狀態,如用途與訪問屬性。
glBufferData()
當緩存初始化之后,你可以使用glBufferData()將數據拷貝到緩存對象。
void glBufferData(GLenum target,GLsizeiptr size, const GLvoid* data, GLenum usage);
第一個參數target可以為GL_ARRAY_BUFFER或GL_ELEMENT_ARRAY。size為待傳遞數據字節數量。第三個參數為源數據數組指針,如data為NULL,則VBO僅僅預留給定數據大小的內存空間。最后一個參數usage標志位VBO的另一個性能提示,它提供緩存對象將如何使用:static、dynamic或stream、與read、copy或draw。
VBO為usage標志指定9個枚舉值:
GL_STATIC_DRAW GL_STATIC_READ GL_STATIC_COPY GL_DYNAMIC_DRAW GL_DYNAMIC_READ GL_DYNAMIC_COPY GL_STREAM_DRAW GL_STREAM_READ GL_STREAM_COPY
”static“表示VBO中的數據將不會被改動(一次指定多次使用),”dynamic“表示數據將會被頻繁改動(反復指定與使用),”stream“表示每幀數據都要改變(一次指定一次使用)。”draw“表示數據將被發送到GPU以待繪制(應用程序到GL),”read“表示數據將被客戶端程序讀取(GL到應用程序),”copy“表示數據可用於繪制與讀取(GL到GL)。
注意,僅僅draw標志對VBO有用,copy與read標志對頂點/幀緩存對象(PBO或FBO)更有意義,如GL_STATIC_DRAW與GL_STREAM_DRAW使用顯卡內存,GL_DYNAMIC使用AGP內存。_READ_相關緩存更適合在系統內存或AGP內存,因為這樣數據更易訪問。
glBufferSubData()
void glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid* data);
與glBufferData()類似,glBufferSubData()用於向VBO中拷貝數據,不過它僅僅將從給定offset開始的一定范圍的數據替換到現存緩存中。(在使用glBufferSubData()之前,整個緩存必須由glBufferData()指定。)
glDeleteBuffers()
void glDrawBuffers(GLsizei n, const GLenum* bufs);
在VBO不再使用時,你可以使用glDeleteBuffers()刪除一個VBO或多個VBO。在混存對象刪除之后,它的內容將丟失。
下列代碼是為頂點坐標創建單一VBO的實例。注意,在你拷貝數據到VBO之后,你可以應用程序中為頂點數組分配的內存。
GLuint vboId; // VBO標示符 GLfloat* vertices = new GLfloat[vCount*3]; // 創建頂點數組 ... // 創建新的VBO並獲取相關標示符 glGenBuffers(1, &vboId); // 綁定VBO以待使用 glBindBuffer(GL_ARRAY_BUFFER_ARB, vboId); // 更新數據到VBO glBufferData(GL_ARRAY_BUFFER_ARB, dataSize, vertices, GL_STATIC_DRAW_ARB); // 在拷貝數據到VBO之后,可以安全刪除 delete [] vertices; ... // 程序終止時刪除VBO glDeleteBuffers(1, &vboId);
繪制VBO
由於VBO基於現有的頂點數組實現之上,渲染VBO與使用頂點數組渲染幾乎一致。僅有的不同是頂點數組指針現在作為當前綁定緩存對象的偏移值。因此,繪制VBO時除了glBindBuffer()之外不需別的API。
// 為頂點數組與索引數組綁定VBO glBindBuffer(GL_ARRAY_BUFFER_ARB, vboId1); // 頂點坐標 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, vboId2); // 索引坐標 // 除了指針都與頂點數組一致 glEnableClientState(GL_VERTEX_ARRAY); // 開啟頂點坐標數組 glVertexPointer(3, GL_FLOAT, 0, 0); // 最后一個參數為offset,而非ptr // 使用索引數組偏移繪制6個四邊形 glDrawElements(GL_QUADS, 24, GL_UNSIGNED_BYTE, 0); glDisableClientState(GL_VERTEX_ARRAY); // 禁用頂點數組 // 用0綁定,因此,切換到標准指針操作 glBindBuffer(GL_ARRAY_BUFFER_ARB, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
使用0綁定緩存對象將關閉VBO操作。在使用完VBO之后,最好將之關閉,因此具有絕對指針的頂點數組操作將重新開啟。
更新VBO
VBO相對於顯示列表的優勢在於客戶端可以讀取與編輯緩存對象數據,而顯示列表不能這樣做。更新VBO的最簡便方法是使用glBufferData()或glBufferSubData()將新數據重新拷貝到綁定的VBO中。這種情況下,你的應用程序需要始終擁有一個有效的頂點數組。也就是說,你必須始終擁有2份頂點數據:一份在應用程序中,另一份在VBO中。
修改緩存對象的另一個方式是將緩存對象映射到客戶端內存,客戶端可以使用映射緩存的指針更新數據。下文描述如何將VBO映射到客戶端內存以及如何訪問映射數據。
glMapBuffer()
VBO提供glMapBuffer()以將緩存對象綁定到客戶端內存。
void* glMapBuffer(GLenum target, GLenum access);
如果OpenGL能夠將緩存對象映射到客戶端地址空間,glMapBuffer()返回指向緩存的指針。否則它返回NULL。
第一個參數target早在glBindBuffer()中涉及,第二個參數access標志指定怎樣使用映射數據:讀取、改寫或兩者都。
GL_READ_ONLY GL_WRITE_ONLY GL_READ_WRITE
注意,glMapBuffer()引起同步問題。如果GPU任然工作於該緩存對象,glMapBuffer()將一直等待直到GPU結束對應緩存對象上的工作。
為了避免等待,你可以首先使用NULL調用glBufferData(),然后再調用glMapBuffer()。這樣,前一個數據將被丟棄且glMapBuffer()立即返回一個新分配的指針,即使GPU任然工作於前一個數據。
然而,由於你丟棄了前一個數據,這種方法只有在你想更新整個數據集的時候才有效。如果你僅僅希望更改部分數據或讀取數據,你最好不要釋放先前的數據。
glUnmapBuffer()
GLboolean glUnmapBuffer(GLenum target)
在完成VBO數據的修改之后,必須將緩存對象從客戶端內存解除映射。如果成功,glUnmapBuffer()返回GL_TRUE。如返回GL_FALSE,綁定之后的VBO緩存內容是壞的。腐壞現象源自顯示器分辨率的改變或窗口系統的特定事件。此種情況,數據必須重發。
下面是使用綁定方式改變VBO的實例代碼。
// 綁定然后映射該VBO glBindBuffer(GL_ARRAY_BUFFER_ARB, vboId); float* ptr = (float*)glMapBuffer(GL_ARRAY_BUFFER_ARB, GL_WRITE_ONLY_ARB); // 如果指針為空(映射后的),更新VBO if (ptr) { updateMyVBO(ptr, ...); // 修改緩存數據 glUnmapBufferARB(GL_ARRAY_BUFFER_ARB); // 使用之后解除映射 } // 你可以繪制更新后的VBO ...
實例
該實例程序沿法線防線創建VBO抖動。它映射VBO並且使用指向映射緩存的指針每幀更新一次頂點數據。你可以與傳統頂點數組實現方式繼進行性能對比。
它使用2個頂點緩存:一個為了頂點坐標與法向量,另一個僅僅保存索引數組。
下載源文件與二進制文件:vbo.zip,vboSimple.zip。
vboSimple是使用VBO與頂點數組繪制立方體的簡單例子。你可以很容易看出VBO與VA的相同點與不同點。