之前在编写着色器的时候,我们每次都需要在不同物体中定义类似功能的Uniform数据,比如 viewMatrix, projMatrix等。这在所有物体都使用相同着色器时,它的弊端不是那么明显,但是一个游戏中物体使用的着色器往往是各不相同的,所以对于每个着色器都传递一次相同的uniform数据一方面不易于管理,另一方面也造成了性能上的浪费。
假设我们有15个不同的着色器文件,每个文件都定义了viewMatrix与projMatrix Uniform变量,那么每个着色器程序我们需要传递两次Uniform数据,15个着色器程序总共就需要30次的传递至GPU,那么有没有办法只定义一次数据,对所有需要此类数据的着色器都能共享?并且共享的同时对于部分的Uniform数据也能定制化更新?这就是要介绍的UniformBufferObject,下面简称为UBO。
要定义UBO,首先需要知道它的分配规则,为了使UBO能够运行在更多的GPU上,除了定义它们外,还需要指定它们在GPU中的内存大小。UBO默认使用shared layout(共享布局)(布局规则包括了packed,shared,std140,std430),正如它的名称一样,它一旦定义就能够被多个着色器程序所共用,并且内部各个Uniform数据的内存已经由系统内部分配完成,但是这也带来了一定的麻烦,因为UBO是一段Buffer,所以在传递对应的UBO数据时,我们需要明确的知道系统分配时各个Uniform的偏移量,否则无法在一个Buffer中对其有效对应,虽然我们可以使用glGetUniformIndices显式的进行获取,但是还是相对麻烦。所以一般我们采用std140的布局规则,该布局规则为每个Uniform变量显式声明了内存大小,所以我们就可以计算出每个Uniform变量的偏移量,从而确定一个Uniform Buffer段的内存大小。可参考关于GPU内存布局规则。
GLSL中标量类型比如int、float和bool被定义为4字节,每4字节表示为N,一个变量的对齐字节偏移量必须等于基准对齐量的倍数,详细描述见下表。
变量类型 | 基底对齐(字节) | 总大小(字节) |
---|---|---|
标量 (bool, int, uint, float) | 4 | 4 |
双精度浮点 (double) | 8 | 8 |
二维向量 (vec2) | 8 | 8 |
三维向量 (vec3) | 16 | 16 |
四维向量 (vec4) | 16 | 16 |
标量数组或向量数组 | 每个元素的基准对齐16字节 | 每个元素基准对齐16字节 |
矩阵类型 (mat2) | 16 | 32 (2x16) |
矩阵类型 (mat3) | 16 | 48 (3x16) |
矩阵类型 (mat4) | 16 | 64 (4x16) |
结构体 (struct) | 16 | 对齐到16字节的倍数 |
从上表的分配规则可以看出,该分配方式会造成比较大的浪费,比如:bool类型实际只占用1字节,但是需要4字节来分配;另外就是数组的分配,数组中不管标量还是向量每个元素都以16字节分配;因为我们的数据常用做法都是统一放在一个结构体中,这又造成了另一个浪费: 结构体内的上一条Uniform数据与下一条Uniform数据的内存间隔必须是16字节的倍数(如果前后两数据都是标量类型,这种浪费会更明显),所以一个好的组织顺序是能够优化内存分配的(比如:可以把同类逻辑的标量数据用向量存储)。
- 标量数据类型
- 标量数据类型 (bool, int, uint, float, double)
- 基底对齐:4 字节(double 是 8 字节)
- 大小:4 字节(double 是 8 字节)
- 解释:标量的起始地址必须是 4 字节的倍数,double 的起始地址必须是 8 字节的倍数。
- 二维向量 (vec2)
- 基底对齐:8 字节
- 大小:8 字节
- 解释:vec2 的起始地址必须是 8 字节的倍数。
- 三维向量 (vec3)
- 基底对齐:16 字节
- 大小:16 字节(实际占用 12 字节,但在 std140 中被视为 16 字节)
- 解释:vec3 的起始地址必须是 16 字节的倍数,尽管它只占用了 12 字节,但它会被补齐到 16 字节以满足对齐要求。
- 四维向量 (vec4)
- 基底对齐:16 字节
- 大小:16 字节
- 解释:vec4 的起始地址必须是 16 字节的倍数。
- 标量数组或向量数组
- 每个元素的基底对齐:每个元素为16字节
- 解释:每个元素16字节。
- 矩阵类型 (mat2, mat3, mat4)
- 每行的基底对齐:16 字节
- 总大小:
- mat2:每行 8 字节,总共 16 字节(2 行,每行 8 字节)
- mat3:每行 16 字节,总共 48 字节(3 行,每行 16 字节)
- mat4:每行 16 字节,总共 64 字节(4 行,每行 16 字节)
- 结构体 (struct)
- 基底对齐:结构体的基底对齐是其成员中最大基底对齐值的倍数,通常是 16 字节(即 4N)。
- 大小:结构体的总大小必须是对齐到 16 字节的倍数,即使最后一个成员没有完全填满 16 字节,也会在结构体末尾添加填充字节以确保对齐。
以下面代码为例:
// 摘自Learn Opengl
layout (std140) uniform ExampleBlock
{
// base alignment ---------- // aligned offset
float value; // 4 // 0
vec3 vector; // 16 // 16 (必须是16的倍数,因此 4->16)
mat4 matrix; // 16 // 32 (第 0 行)
// 16 // 48 (第 1 行)
// 16 // 64 (第 2 行)
// 16 // 80 (第 3 行)
float values[3]; // 16 (数组中的标量与vec4相同)//96 (values[0])
// 16 // 112 (values[1])
// 16 // 128 (values[2])
bool boolean; // 4 // 144
int integer; // 4 // 148
};
上述是在着色器中对UBO的结构体声明,其中layout (std140) uniform指定该结构体是个UBO,并且使用std140的布局规则。由于vec3的基底对齐是16字节,所以value变量到vector变量的偏移量是16字节。
Opengl中正式支持UBO是在Opengl3.1的版本,在这版本之前需要确定是否有可支持的扩展(扩展名为 ARB_Uniform_Buffer_Object),比如GLEW库中可以通过判断GLEW_ARB_uniform_buffer_object是否为true,true则支持,false为不支持:
if (GLEW_ARB_uniform_buffer_object) {
printf ("GLEW_ARB_uniform_buffer_object = YES\n");
} else {
printf ("GLEW_ARB_uniform_buffer_object = NO\n");
}
因为每台设备对UBO可支持的最大绑定数量是不确定的,可以借助GL_MAX_UNIFORM_BUFFER_BINDINGS宏确定当前设备最大绑定数量:
GLint blocks = 0;
// 本人计算机运行webgl,实测支持的最大绑定数量为24
glGetIntegerv (GL_MAX_UNIFORM_BUFFER_BINDINGS, &blocks);
printf ("GL_MAX_UNIFORM_BUFFER_BINDINGS = %i\n", blocks);
关于glGetIntergerv(WebGL中为getParameter)的可支持状态获取可查看下面链接:
生成UBO的方式与生成VAO/VBO的方式类似,都是通过glGenBuffers生成Buffer,并通过glBindBuffer绑定,以及通过glBufferData分配数据, 但是需要明确指定Buffer类型为GL_UNIFORM_BUFFER:
GLuint cam_ubo_buffer;
glGenBuffers (1, &cam_ubo_buffer);
glBindBuffer (GL_UNIFORM_BUFFER, cam_ubo_buffer);
// cam_ubo只简单定义viewMat与projMat,所以它的总长度为sizeof (float) * 32
glBufferData (GL_UNIFORM_BUFFER, sizeof (float) * 32, NULL, GL_DYNAMIC_DRAW);
每个在Shader中定义的UBO block都需要指定其绑定索引,上面我们通过GL_MAX_UNIFORM_BUFFER_BINDINGS获取UBO能够绑定的最大数量,如果绑定的最大数量为M,则绑定索引的可能值范围为(0 ~ M-1),如果整个项目中只使用了一个UBO那么直接使用索引0作为绑定目标就可以了,但是绑定索引不要指定为可能值范围之外的数(比如:M或-1等),这会使UBO失效,并会输出如下的类似警告:
GL_INVALID_OPERATION: It is undefined behaviour to have a used but unbound uniform buffer.
要使用UBO绑定索引,需要先指定Shader中哪些UBO Block与绑定索引产生关联,那就需要手动指定Shader中各个UBO block的绑定索引:
// 设置ubo的默认绑定索引为0
int ubo_binding_idx = 0;
// 获取monkey这个物体对应的shader文件中定义的名称为cam_ubo_block的结构体索引位置
GLuint uniform_block_index_monkey =
glGetUniformBlockIndex (monkeyShaderProgram, "cam_ubo_block");
// 让shader中定义的ubo block结构体与绑定索引产生关联
glUniformBlockBinding (monkeyShaderProgram, uniform_block_index_monkey, ubo_binding_idx);
// 获取cube这个物体对应的shader文件中定义的名称为cam_ubo_block的结构体索引位置
GLuint uniform_block_index_cube_sp =
glGetUniformBlockIndex (cubeShaderPrograme, "cam_ubo_block");
// 让shader中定义的ubo block结构体与绑定索引产生关联
glUniformBlockBinding (cubeShaderPrograme, uniform_block_index_cube_sp, ubo_binding_idx);
如果整个项目中只定义了一个UBO,那么可以省略glGetUniformBlockIndex的过程,直接给定glUniformBlockBinding中第二个参数值为0即可,但是一般项目的UBO个数不止一个(比如有ShadowMapUBO, CameraUBO, GlobalUBO等)。
定义好了UBO相关配置后,就可以对绑定的数据进行更新了,这里有两种实现方式,一种是使用glBufferSubData更新数据:
// 通过glBindBufferBase把ubo数据与绑定索引关联起来
// 这里也可以使用glBindBufferRange函数,它需要一个附加的偏移量和大小参数,
// 这样子就可以绑定Uniform缓冲的特定一部分到绑定点中
// glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
glBindBufferBase (GL_UNIFORM_BUFFER, ubo_binding_idx, cam_ubo_buffer);
// 对proj矩阵进行更新
glBufferSubData (
GL_UNIFORM_BUFFER, 0, sizeof (float) * 16, proj_mat.m);
// 对view矩阵进行更新
glBufferSubData (
GL_UNIFORM_BUFFER, sizeof (float) * 16, sizeof (float) * 16, view_mat.m);
另一种是使用glMapBufferRange的方式更新数据,一般来讲这种方式更新数据特别是大数据更新会更加高效:
// 通过glBindBufferBase把ubo数据与绑定索引关联起来
glBindBufferBase (GL_UNIFORM_BUFFER, ubo_binding_idx, cam_ubo_buffer);
float* cam_ubo_ptr = (float*)glMapBufferRange (
GL_UNIFORM_BUFFER,
0,
sizeof (float) * 32,
// 设置写标志
GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT
);
// 对proj矩阵进行更新
memcpy (&cam_ubo_ptr[0], proj_mat.m, sizeof (float) * 16);
// 对view矩阵进行更新
memcpy (&cam_ubo_ptr[16], view_mat.m, sizeof (float) * 16);
// 数据设置完毕后记得Unmap
glUnmapBuffer(GL_UNIFORM_BUFFER);
简单讲glMapBufferRange的方式可以拿到指向的内存地址,可以直接对内存地址内部的缓冲区数据进行更新,所以它将更高效;而glBufferSubData的方式需要经过中间拷贝的过程。这是Opengl Insights 中的两者对比:
这是顶点着色器中的代码:
#version 400
in vec3 vp;
//uniform mat4 P, V; // old uniforms
/* new virtual camera block */
layout (std140) uniform cam_ubo_block{
mat4 P;
mat4 V;
};
void main () {
gl_Position = P * V * vec4 (vp, 1.0);
}
顶点着色器中定义了一个叫cam_ubo_block的UBO 结构体,它使用了std140的布局规范,注意:在实际使用中可以直接取内部的uniform变量进行实际计算,而不需要定义cam_ubo_block的变量(比如在main函数中直接取P与V矩阵进行顶点变换)。