本文介紹如何在OpenGL 4.5+環(huán)境下用最現(xiàn)代的方式渲染頂點(diǎn)。本文持續(xù)更新中。
前言
上一節(jié)我們講了如何在Modern OpenGL下渲染矩形體,但其中用到的主要是OpenGL 3.x中內(nèi)容。OpenGL 4.x增添了很多新內(nèi)容,并且一部分3.x的內(nèi)容得到改進(jìn)。本節(jié)中會(huì)詳細(xì)講解新版和與舊版的區(qū)別與聯(lián)系,并給出應(yīng)用最新技術(shù)的例子。
1. 直接狀態(tài)訪問 DSA (Direct State Access)
OpenGL 4.5中給了我們DSA,可以在調(diào)用方法時(shí)直接傳入要操作的OpenGL對(duì)象,而不再需要綁定操作,更加高效。與之對(duì)應(yīng)的,所有glGen*方法均不再使用,因?yàn)檫@些方法只生成了一個(gè)對(duì)象(實(shí)質(zhì)上是分配了一個(gè)ID),并沒有定義這個(gè)對(duì)象的類型。在4.5以前,一個(gè)對(duì)象的定義發(fā)生在首次綁定時(shí)。例如:
- glGenTextures(1, &texture)生成一個(gè)紋理
- 第一次調(diào)用glBindTexture(GL_TEXTURE_2D, texture)時(shí),定義了該紋理是2D紋理。
從OpenGL 4.5起,由于不再需要綁定操作,所有創(chuàng)建OpenGL對(duì)象的操作都被glCreate*所替代,例如:
- glCreateTextures(GL_TEXTURE_2D, 1, &texture),創(chuàng)建一個(gè)2D紋理。
如果依然使用glGen*而不進(jìn)行首次綁定,則該OpenGL對(duì)象是無效對(duì)象,操作時(shí)會(huì)報(bào)出GL_INVALID_OPERATION錯(cuò)誤。
2. 頂點(diǎn)數(shù)組對(duì)象 Vertex Array Object
Modern OpenGL渲染中必須使用VAO,它無處不在。VAO本身不存儲(chǔ)任何頂點(diǎn)數(shù)據(jù),它會(huì)保存我們要渲染時(shí)所需要的頂點(diǎn)的定義、規(guī)格與配置。一旦我們配置好了一個(gè)VAO,只要綁定它,就可以直接調(diào)用渲染函數(shù),而不需要調(diào)用任何定義/配置類的函數(shù)。
創(chuàng)建一個(gè)VAO
GLuint vao_id;
glCreateVertexArrays(/* number */ 1, &vao_id);
3. 通用頂點(diǎn)屬性 Generic Vertex Attribute
頂點(diǎn)屬性是頂點(diǎn)著色器(Vertex Shader)的輸入。在Modern OpenGL中,我們必須自己定義我們需要哪些頂點(diǎn)屬性。每定義的一個(gè)屬性,都可以叫做通用頂點(diǎn)屬性。與之對(duì)應(yīng)的,在傳統(tǒng)固定管線中,頂點(diǎn)著色器存在內(nèi)置頂點(diǎn)屬性,但它們都已經(jīng)廢棄。所以我們說的頂點(diǎn)屬性都是指通用頂點(diǎn)屬性,它們的名字、類型都是完全自定義的。例如,我們的頂點(diǎn)著色器:
layout(location = 0) in vec2 a_Pos;
layout(location = 1) in vec4 a_Color;
smooth out ...
void main() {
...
頂點(diǎn)屬性用in表示,location = 0顯式指定了屬性索引(attribute index)是0,location不一定連續(xù)。屬性名在這種情況下僅在shader內(nèi)使用。
4. 頂點(diǎn)緩沖綁定點(diǎn) Vertex Buffer Binding Point
頂點(diǎn)緩沖綁定點(diǎn)是在一個(gè)VAO內(nèi)共享的,我們可以將一種配置綁定到一個(gè)綁定點(diǎn)上,再將一個(gè)或多個(gè)頂點(diǎn)屬性與之綁定,這樣就可以隨時(shí)切換頂點(diǎn)屬性而不需要重新配置緩沖。也就是說,我們定義一個(gè)綁定點(diǎn),和它如何從VBO中讀取數(shù)據(jù),這個(gè)定義信息會(huì)被存入VAO,關(guān)系圖如下:

設(shè)置綁定點(diǎn)與VBO關(guān)系的函數(shù)為glVertexArrayVertexBuffer:
glVertexArrayVertexBuffer(GLuint vaobj, GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride)
- vaobj - 要操作的VAO
- bindingindex - 綁定點(diǎn)的索引,0起始
- buffer - 存儲(chǔ)頂點(diǎn)數(shù)據(jù)的那個(gè)VBO
- offset - 第一個(gè)頂點(diǎn)屬性的起始偏移量
- stride - 到下個(gè)頂點(diǎn)數(shù)據(jù)的跨度
比如:我們定義0號(hào)綁定點(diǎn)對(duì)應(yīng)12個(gè)字節(jié)大小,然后我們的VBO創(chuàng)建為vbo_first,它的數(shù)據(jù)是每12個(gè)字節(jié)一組頂點(diǎn)屬性,那么offset是0,stride為12。如果它第8到20字節(jié),28到40字節(jié)以此類推,是我們需要的數(shù)據(jù)的話,那么offset是8(第一組數(shù)據(jù)是8開始),stride是20(8到28是20, 20到40是20)。剩下的一個(gè)個(gè)8字節(jié)(0到8,20到28...)也許是其他地方需要的數(shù)據(jù)。因?yàn)橐粋€(gè)VBO可以存任何東西,VBO用一個(gè)還是多個(gè),數(shù)據(jù)怎么存放,可自由設(shè)置。
5. 頂點(diǎn)屬性格式 Vertex Attribute Format
重點(diǎn)來了,雖然我們?cè)趕hader中定義了頂點(diǎn)屬性,但OpenGL不知道我們是如何定義的,也就不知道怎么把數(shù)據(jù)傳給shader的attribute,所以我們必須指定它們。
glVertexArrayAttribFormat(GLuint vaobj, GLuint attribindex, GLint size, GLenum type,
GLboolean normalized, GLuint relativeoffset);
第一個(gè)參數(shù)傳入我們的VAO。attribindex傳入屬性索引,也就是頂點(diǎn)著色器中(location = 幾)的幾。
- size - 指定了該頂點(diǎn)屬性的大小,不是字節(jié)數(shù),是分量數(shù)!該值只能是1,2,3,4。例如 vec2,分量數(shù)是 2。
- type - 指定了VBO數(shù)據(jù)中對(duì)應(yīng)此屬性的類型??蛇x的值有 GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_HALF_FLOAT, GL_FLOAT, GL_DOUBLE, GL_FIXED, GL_INT_2_10_10_10_REV, GL_UNSIGNED_INT_2_10_10_10_REV 和 GL_UNSIGNED_INT_10F_11F_11F_REV。
- normalized - 指定了數(shù)據(jù)是否要被標(biāo)準(zhǔn)化。如果數(shù)據(jù)類型的符號(hào)型數(shù)值,則轉(zhuǎn)換為[-1, 1]的浮點(diǎn)值,如果是無符號(hào)型數(shù)值,則轉(zhuǎn)換為[0, 1]的浮點(diǎn)值。如果不標(biāo)準(zhǔn)化,則直接轉(zhuǎn)換為浮點(diǎn)值。坐標(biāo)不需要標(biāo)準(zhǔn)化,所以這里輸入 GL_FALSE。
注意:上面的函數(shù)最終會(huì)把數(shù)據(jù)轉(zhuǎn)成浮點(diǎn)型。如果使用 glVertexArrayAttribIFormat(多了一個(gè)I, Integer),則會(huì)轉(zhuǎn)成整數(shù)類型,并且 type 只能 GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT 和 GL_UNSIGNED_INT。這個(gè)方法不含 normalized 參數(shù)。
此外還有一個(gè)relativeoffset參數(shù),相對(duì)偏移量,也就是在綁定點(diǎn)中的偏移量,而不是VBO中的偏移量。我們剛剛定義了綁定點(diǎn)0占據(jù)12個(gè)字節(jié),這樣的話我們可以把之前的vec2 a_Pos和vec4 a_Color都綁定到這個(gè)綁定點(diǎn)上,如果我們的VBO是前8個(gè)字節(jié)是坐標(biāo),后4個(gè)字節(jié)是顏色,那么就分別調(diào)用:
glVertexArrayAttribFormat(vao_id, 0, 2, GL_FLOAT, false, 0);
// location = 1, vec4 (4個(gè)分量), 相對(duì)偏移量是8
glVertexArrayAttribFormat(vao_id, 1, 4, GL_UNSIGNED_BYTE, true, 8);
在VBO中存儲(chǔ)后4個(gè)字節(jié)為RGBA顏色值,標(biāo)準(zhǔn)化到浮點(diǎn)型,因?yàn)閡nsigned int是0~255,所以轉(zhuǎn)換時(shí)會(huì)除255得到shader中使用的顏色值。8是因?yàn)樽鴺?biāo)2兩個(gè)浮點(diǎn)值8字節(jié),一個(gè)綁定點(diǎn)是12字節(jié)。
別忘了,glVertexArrayAttribFormat只定義了屬性格式,要綁定到綁定點(diǎn)上,還需要調(diào)用glVertexArrayAttribBinding:
glVertexArrayAttribBinding(GLuint vaobj, GLuint attribindex, GLuint bindingindex);
第一個(gè)參數(shù)是VAO,第二個(gè)是屬性索引,第三個(gè)是綁定點(diǎn)的索引。這里我們就需要把0和1號(hào)屬性全都綁定到0號(hào)綁定點(diǎn):
glVertexArrayAttribBinding(vao_id, 0, 0);
glVertexArrayAttribBinding(vao_id, 1, 0);
假設(shè)我們頂點(diǎn)著色器中還有2號(hào)和3號(hào)屬性,也是vec2和vec4。那么只需最開始調(diào)用glVertexArrayAttribFormat,然后在需要切換的時(shí)候調(diào)用glVertexArrayAttribBinding切換綁定即可。你會(huì)發(fā)現(xiàn),我們并沒有重新配置綁定點(diǎn),也就是怎么從VBO中讀取。而且,我們從來沒綁定過VBO,它只是作為參數(shù)傳入。
6. 元素緩沖對(duì)象 Element Buffer Object
如果需要索引繪制(indexed drawing),我們還要將所使用的EBO配置進(jìn)VAO:
glVertexArrayElementBuffer(vao_id, ebo_id);
這樣一來,所有的配置都存進(jìn)了VAO,由多個(gè)VBO存放頂點(diǎn)數(shù)據(jù),只要綁定VAO即可渲染,渲染循環(huán)如下:
glBindVertexArray(vao_id);
glUseProgram(program);
glDrawElementsInstanced(...);
要更新頂點(diǎn)數(shù)據(jù),只需在下一幀渲染開始前使用glNamedBufferSubData更新VBO即可。