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. 了解顶点属性的自动补全和截断
05-22 00:43