向Vertex Shader傳遞vertex attribute

胖Po發表於2021-05-21

VBO、VAO和EBO那一節,介紹瞭如何向Vertex Shader傳遞vertex attribute的基本方法。現在我準備把這個話題再次擴充套件開。

傳遞整型資料

之前我們的頂點屬性資料都是float型別的,現在我使用int(unsigned int)型別或者double型別的資料怎麼辦?

比如我現在用GLubyte來定義三角形的顏色:

GLfloat trianglePosition[] =
{
    -1.0f, -1.0f, 0.0f,
    1.0f, -1.0f, 0.0f,
    0.0f, 1.0f, 0.0f
};

GLubyte triangleColor[] =
{
    255, 0, 0,
    0, 255, 0,
    0, 0, 255
};

GLuint vbo[2] = { 0 };
glCreateBuffers(2, vbo);

glNamedBufferStorage(vbo[0], sizeof(trianglePosition), trianglePosition, 0);
glNamedBufferStorage(vbo[1], sizeof(triangleColor), triangleColor, 0);

GLuint vao = 0;
glCreateVertexArrays(1, &vao);

glEnableVertexArrayAttrib(vao, 0);
glEnableVertexArrayAttrib(vao, 1);

glVertexArrayVertexBuffer(vao, 3, vbo[0], 0, sizeof(GLfloat) * 3);
glVertexArrayVertexBuffer(vao, 5, vbo[1], 0, sizeof(GLubyte) * 3);	//設定vao與binding point關聯的buffer的stride是sizeof(GLubyte)*3

glVertexArrayAttribBinding(vao, 0, 3);
glVertexArrayAttribFormat(vao, 0, 3, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribFormat(vao, 1, 3, GL_UNSIGNED_BYTE, GL_TRUE, 0);	//設定data type為GL_UNSIGNED_BYTE,並且normalized設定為GL_TRUE
glVertexArrayAttribBinding(vao, 1, 5);

以上程式碼你應該很熟悉,有兩處我加了註釋,標記出與傳遞GLfloat型別資料的不同之處。第一處是設定binding point對應的buffer的stride,這個很容易理解,沒什麼值得討論的東西。關鍵看第二處:

glVertexArrayAttribFormat(vao, 1, 3, GL_UNSIGNED_BYTE, GL_TRUE, 0);	//設定data type為GL_UNSIGNED_BYTE,並且normalized設定為GL_TRUE

之前我就一直納悶這個命令的GLboolean normalized到底是幹啥用的,現在終於搞清楚了:這個引數只對整型的頂點屬性資料起作用,來決定是否對整數型別的資料進行歸一化,怎麼個歸一化法呢?我演示給你看:

//vertex shader
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;		//是vec3而不是uvec3

...
void main(void)
{
    ...
}

注意雖然資料型別是GLubyte型別的,但是傳遞到vertex shader卻是以float為基礎的vec3。這時候vertex shader接受到的color資料其實是:(1.0, 0.0, 0.0)(0.0, 1.0, 0.0)(0.0, 0.0, 1.0)。對於無符號的整數型別譬如GLuint,GLushort和GLubyte,會從[0, MAX]線性對映到[0.0, 1.0];而對於有符號整型譬如GLuint,GLushort和GLubyte,則會從[Min, Max]線性對映到[-1.0, 1.0]。公式分別如下:

  • 無符號型別的歸一化:\(f = \frac{c}{2^{b}-1}\)
  • 有符號型別的歸一化:\(f = \frac{2c-1}{2^{b}-1}\)

c表示整數數值的大小,b表示這個整數有多少位,比如GLubyte和GLbyte是8位,GLuint和GLint是32位。

如果我們設定nomalized屬性為GL_FALSE,那麼整數會被直接強制轉換為浮點數型別,也就是說vertex shader接受的color資料就會變成:(255.0, 0.0, 0.0)

(0.0, 255.0, 0.0)(0.0, 0.0, 255.0)

如果數值非常大的整數歸一化到浮點數,是會丟失精度的,因此範圍比較大的整數不適合歸一化成浮點數,這時候我們需要直接引用整數型別的頂點屬性(或者更多的時候是你需要的就是整數型別的頂點屬性):

glVertexAttribIFormat(vao, 1, 3, GL_UNSIGNED_BYTE, 0);

此命令中的I字元表示的是整數型別的意思,因為是直接引用的整數型別的資料,所以此命令不需要GLboolean normalized引數。shader也變為:

//vertex shader
layout(location = 0) in vec3 position;
layout(location = 1) in uvec3 color;		//是vec3而不是uvec3

...
void main(void)
{
    ...
}

如程式碼所示:我們可以在shader中直接引用無符號整數型別的資料了。

傳遞雙精度浮點型資料

有了前面的鋪墊,傳遞double型別的資料很自然就能想到存在類似這樣的命令:

void glVertexArrayAttribLFormat(GLuint vaobj,
 	GLuint attribindex,
 	GLint size,
 	GLenum type,
 	GLuint relativeoffset);

使用起來也和你想的一樣:

GLfloat trianglePosition[] =
{
    -1.0f, -1.0f, 0.0f,
    1.0f, -1.0f, 0.0f,
    0.0f, 1.0f, 0.0f
};

GLdouble triangleColor[] =
{
    1.0, 1.0, 1.0,
    0.5, 0.5, 0.5,
    0.0, 0.0, 0.0
};

GLuint vbo[2] = { 0 };
glCreateBuffers(2, vbo);

glNamedBufferStorage(vbo[0], sizeof(trianglePosition), trianglePosition, 0);
glNamedBufferStorage(vbo[1], sizeof(triangleColor), triangleColor, 0);

GLuint vao = 0;
glCreateVertexArrays(1, &vao);

glEnableVertexArrayAttrib(vao, 0);
glEnableVertexArrayAttrib(vao, 1);

glVertexArrayVertexBuffer(vao, 3, vbo[0], 0, sizeof(GLfloat) * 3);
glVertexArrayVertexBuffer(vao, 5, vbo[1], 0, sizeof(GLdouble) * 3);	//設定vao與binding point關聯的buffer的stride是sizeof(GLdouble)*3

glVertexArrayAttribBinding(vao, 0, 3);
glVertexArrayAttribFormat(vao, 0, 3, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribLFormat(vao, 1, 3, GL_DOUBLE, 0);	//設定data type為GL_DOUBLE
glVertexArrayAttribBinding(vao, 1, 5);
//vertex shader
layout(location = 0) in vec3 position;
layout(location = 1) in dvec3 color;		//dvec3表示double型別的向量

...
void main(void)
{
    ...
}

interleaved attributes

之前我們使用vertex attribute的方式稱為separate attributes,意思是每個vertex attribute分別單獨存在兩個vbo中。或者像這樣,也是separate attribute的變種:

//空間位置和顏色連續存放到一個buffer中
GLfloat triangle[] =
{
    -1.0f, -1.0f,		//空間位置
    1.0f, -1.0f,
    0.0f, 1.0f,
    1.0f, 0.0f, 0.0f,	//顏色
    0.0f, 1.0f, 0.0f,
    0.0f, 0.0f, 1.0f
};

glNamedBufferStorage(vbo, sizeof(triangle), triangle, 0);

glEnableVertexArrayAttrib(vao, 0);
glEnableVertexArrayAttrib(vao, 1);

glVertexArrayVertexBuffer(vao, 3, vbo, 0, 2 * sizeof(GLfloat));
glVertexArrayAttribFormat(vao, 0, 2, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribBinding(vao, 0, 3);

glVertexArrayVertexBuffer(vao, 5, vbo, 6 * sizeof(GLfloat), 3 * sizeof(GLfloat));	//顏色的offset是跨過空間位置空間
glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribBinding(vao, 1, 5);

現在我再舉例說明如何使用interleaved attributes,以加深對glVertexArrayVertexBufferglVertexArrayAttribLFormat的理解。

仍然是繪製一個三角形:

//空間位置和顏色交叉存放
GLfloat triangle[] =
{
    -1.0f, -1.0f,			//空間位置
    1.0f, 0.0f, 0.0f,		//顏色
    1.0f, -1.0f,
    0.0f, 1.0f, 0.0f,
    0.0f, 1.0f,
    0.0f, 0.0f, 1.0f
};

glNamedBufferStorage(vbo, sizeof(triangle), triangle, 0);

glEnableVertexArrayAttrib(vao, 0);
glEnableVertexArrayAttrib(vao, 1);

glVertexArrayVertexBuffer(vao, 3, vbo, 0, 5 * sizeof(GLfloat));
glVertexArrayAttribFormat(vao, 0, 2, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribBinding(vao, 0, 3);

glVertexArrayVertexBuffer(vao, 5, vbo, 0, 5 * sizeof(GLfloat));
glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat));	//顏色的relativeoffset是跳過頂點位置,即兩個GLfloat
glVertexArrayAttribBinding(vao, 1, 5);

其實顏色的offset也可以指定給glVertexArrayVertexBuffer的第三個引數offset,而不是glVertexArrayAttribFormat的最後一個引數relativeoffset

glVertexArrayVertexBuffer(vao, 5, vbo, 2 * sizeof(GLfloat), 5 * sizeof(GLfloat));//指定顏色資料的offset
glVertexArrayAttribFormat(vao, 1, 3, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribBinding(vao, 1, 5);

那這offsetrelativeoffset到底有什麼區別呢?

下面以如下方式繪製一個三角形和一個矩形。

GLfloat triangle_rect[] =
{
    //三角形的空間位置和顏色交叉存放
    -1.0f, -1.0f,			//三角形的空間位置
    1.0f, 0.0f, 0.0f,		//三角形的顏色
    1.0f, -1.0f,
    0.0f, 1.0f, 0.0f,
    0.0f, 1.0f,
    0.0f, 0.0f, 1.0f,

    //矩形的空間位置和顏色也是交叉存放
    -0.5f, 0.0f,		//矩形的空間位置
    0.0f, 0.0f, 0.0f,	//矩形的顏色
    -0.5f, -1.0f,
    0.3f, 0.3f, 0.3f,
    0.5f, 0.0f,
    0.7f, 0.7f, 0.7f,
    0.5f, -1.0f,
    1.0f, 1.0f, 1.0f
};

glNamedBufferStorage(vbo, sizeof(triangle_rect), triangle_rect, 0);

glEnableVertexArrayAttrib(triangle_vao, 0);
glEnableVertexArrayAttrib(triangle_vao, 1);

glVertexArrayVertexBuffer(triangle_vao, 3, vbo, 0, 5 * sizeof(GLfloat));
glVertexArrayAttribFormat(triangle_vao, 0, 2, GL_FLOAT, GL_FALSE, 0);
glVertexArrayAttribBinding(triangle_vao, 0, 3);

glVertexArrayVertexBuffer(triangle_vao, 5, vbo, 2 * sizeof(GLfloat), 5 * sizeof(GLfloat));
glVertexArrayAttribFormat(triangle_vao, 1, 3, GL_FLOAT, GL_FALSE, 2 * 0);
glVertexArrayAttribBinding(triangle_vao, 1, 5);


glEnableVertexArrayAttrib(rect_vao, 0);
glEnableVertexArrayAttrib(rect_vao, 1);

glVertexArrayVertexBuffer(rect_vao, 3, vbo, 3 * 5 * sizeof(GLfloat), 5 * sizeof(GLfloat));
glVertexArrayAttribBinding(rect_vao, 0, 3);
glVertexArrayAttribFormat(rect_vao, 0, 2, GL_FLOAT, GL_FALSE, 0);

glVertexArrayVertexBuffer(rect_vao, 5, vbo, 3 * 5 * sizeof(GLfloat), 5 * sizeof(GLfloat));	//矩形顏色的offset為跨過所有的三角形的資料
glVertexArrayAttribBinding(rect_vao, 1, 5);
glVertexArrayAttribFormat(rect_vao, 1, 3, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat));	//矩形顏色的relative offset為跨過自身的空間位置資料

看到上述程式碼,我相信你可能明白一些了:offset往往描述的是跨越到屬於自己的資料區(跨過三角形的所有頂點資料),而在自己的資料區跨越是用relative offset來描述的(跨過矩形本身的空間位置資料)。其實也是有公式的:

//索引某個頂點的attribute公式

location = binding[attrib.binding].memory + // Start of data store in memory
binding[attrib.binding].offset + // Offset of vertex attribute in buffer
binding[attrib.binding].stride * vertex.index + // Start of *this* vertex
vertex.relative_offset; // Start of attribute relative to vertex

頂點的自動補全和截斷

自動補全:

...
glVertexArrayAttribFormat(vao, 1, 1, GL_FLOAT, GL_FALSE, 0);	//指定attribute index 1只有一個float分量

//vertex shader
layout(location = 1) in vec4 color;		//color的y和z分量被補全為0,w分量為1
...

截斷:

...
glVertexArrayAttribFormat(vao, 1, 4, GL_FLOAT, GL_FALSE, 0);	//指定attribute index 1只有一個4個float分量

//vertex shader
layout(location = 1) in vec2 color;		//只拿到了x和y分量,z和w直接被丟棄掉了
...

小結

通過這一節,我們掌握瞭如下內容:

  1. 能夠給Vertex Shader傳遞整數型別的vertex attribute,以及歸一化和不歸一化的區別
  2. 能夠給Vertex Shader傳遞double型別的vertex attribute
  3. 學會separate attribute和interleaved attribute的頂點資料組織以及傳遞方式,兩種型別分別有兩種,一共四種
  4. 瞭解頂點屬性的自動補全和截斷

相關文章