OpenGL超级宝典第七版简体中文-第五章:数据(二部分)

一致区块(Uniform Blocks)

有一天我们将会要编写非常复杂的着色器。其中的一些需要很多的常量数据,使用一致变量传递所有的数据变得十分低效。如果我们的应用中有很多的着色器,我们需要调用各种glUniform*()函数来一一设置这些着色器。我们还需要记录各个一致变量的变化情况。有一些变化发生在每个对象上,有一些变化发生在每一帧,而另一些可能只需要初始化一次。这意味着我们要么我们在不同的场合更新不同的一致变量(这使得应用难以维护)或者在所有时候更新全部的一致变量(这使用应用变得低效)。

为了避免对所有的一致变量都调用一次glUniform*(),使得更新大批量的一致变量更容易,以及在不同程式(program)间方便地共享一系列的一致变量,OpenGL允许我们将一致变量编组为一个一致区块并将整个区块存入一个缓冲对象(buffer object)。这里的缓冲对象与我们之前讨论过的一样。通过改变缓冲绑定(buffer binding)或者覆写绑定缓冲(bound buffer)的内容使得我们可以快速地设置整个组内的一致变量的值。我们还可以在改变程式的情况下使得缓冲保持绑定,这样新的程式将会看到当前一系列一致变量的值。这个功能称为一致变量缓冲对象(uniform buffer object - UBO)。事实上迄今为止我们所使用过的一致变量都存于缺省区块(default block)。在着色器全局范围内声明的任何一致变量都存于缺省区块。我们无法将缺省区块存入一致变量缓冲对象,要使用一致变量缓冲对象我们需要创建至少一个有名一致区块。

为了将一系列的一致变量存入一个缓冲对象,我们需要在着色器中使用一个有名一致区块。这看起来很像第三章中的数据块接口(Interface Blocks),但一致区块使用uniform关键字而不是in或者out。清单5.9展示了在着色器中一致区块的代码看起来是什么样。

uniform TransformBlock
{
    float scale;    // Global scale to apply to everything
    vec3 translation;   // Translation in X,Y and Z
    float rotation[3];  // Rotation around X,Y and Z axes
    mat4 projection_matrix; // A generalized projection matrix to apply
                            // after scale and rotate
} transform;

清单5.9: 一致区块声明示例

这段代码声明了一个名为TransformBlock的一致区块,同时还声明了一个名为transform的实例。在着色器代码中我们可以用它的实例名字transform(比如transform.scale或者transform.projection_matrix)来引用一致区块的成员。不过我们若想设置用以供给数据的缓冲对象的数据,我们需要知道一致区块的名字TransformBlock。如果我们想拥有多个一致区块的实例,且每个实例使用自己的缓冲对象,我们可以将transform开辟为一个数组。一致区块内的成员在每个实例内都拥有同样的位置,不过在着色器内我们可以引用一致区块的好几个实例。在接下来的章节我们将会看到,当我们要为一致区块填充数据时,检索一致区块内的成员位置是十分重要的。

构建一致区块

着色器中的具名一致区块的数据可以存入缓冲对象。通常来讲填充缓冲对象(使用glBufferData()以及glMapBuffer())是应用的任务。问题是缓冲对象中的数据看起来是怎样的?实际上有两种可能性,选择哪一种都是一种权衡。

第一种方法是使用一种标准的、普遍赞成的数据布局。这意味着我们的应用程式可以简单地将数据拷贝到缓冲对象中并假定一致区块内的成员都有特定的位置--甚至于我们可以事先将数据存储在磁盘上然后将数据从磁盘直接读入到映射的缓冲对象(使用glMapBuffer()映射)内。标准布局可能会在一致区块内的各个成员间留有空白,这使得缓冲对象会比本来的要大,并且这种便捷可能会损失一些性能。尽管如此,使用标准布局在所有情况下几乎都是安全的。

另外一种方法是让OpenGL来决定如何放置数据。这能生成最高效的着色器,但这样也意味着我们的应用需要搞明白把数据放在哪才能让OpenGL顺利读取。在这种情况下,存储在一致区块缓冲对象中的数据被以共享布局(shared layout)进行排列。这是缺省的布局方式,在我们不进行任何其他显式指定的情况下OpenGL将会这么办。在共享布局下数据在一致区块缓冲对象中的布局由OpenGL依据运行时最高效能和着色器最佳访问决定。在某些情况下这能为着色器带来最大的效能,但随之而来的会给应用程式增加工作量。这种布局之所以被称为共享布局,是因为一旦OpenGL将缓冲对象内的数据排列好,这种排列方式将在共享同样的一致区块声明的多个程式和着色器间保持一致。这使得我们可以和任何程式使用同一个缓冲对象。为了使用共享布局,应用程式必须获知缓冲对象中一致区块内的各个成员的位置。

首先我们会描述标准布局(standard layout),这也是我们推荐使用的布局方式(尽管它不是缺省的布局方式)。为了指示OpenGL我们要使用标准布局,我们必须在声明一致区块时带上布局指示器。在清单5.10中我们展示了使用标准布局指示器(std140)来声明一致区块TransformBlock

layout (std140) uniform TransformBlock
{
    float scale;    // Global scale to everything
    vec3 translation;   // Translation in X,Y and Z
    float rotation[3];  // Rotation around X,Y and Z axes
    mat4 projection_matrix; // A generalized projection matrix to
                            // apply after scale and rotate
} transform;

清单5.10: 使用std140布局声明一致区块

一旦一致区块使用标准布局(std140)进行了声明,那区块的每个成员占用的空间即为预定的,并且在缓冲对象中开始的偏移也是可预知的,这些都是通过一套规则可以演算出的。我们总结一下这套规则:

在缓冲中任何占用N个字节的类型都始于缓冲的N字节边界处。这意味着诸如intfloatbool这些标准GLSL数据类型(皆为32位即4字节长)都会始于4字节倍数处。长度为2的这些类型的向量则总是始于2N字节边界处。比如在内存中长度为8字节的vec2则总是始于8字节边界处。三或者四元素向量则总是始于4N字节边界处。比如vec3vec4类型始于16字节边界处。数组的每个成员(标量或是向量类型,比如int数组或者vec3数组)也依照这些规则总是始于某个边界处,不过这个边界总是凑足vec4来算的。这意味着除了vec4数组(以及Nx4矩阵)以外其他类型都不会被紧凑打包,在每个元素之间都会有间隔。矩阵被当做较短的向量数组,矩阵数组被当做很长的向量数组。最后,结构体和结构体数组还有额外的打包要求,整个结构体始于它的最长的成员的边界处,这个边界凑足vec4的长度。

特别要注意的是std140布局和诸如C++之类的应用程式语言的打包规则之间的区别。尤其是一致区块内的数组是不一定会紧凑打包的。这意味我们没法在一致区块中创建一个float数组,然后简单地从一个floatC数组拷贝数据进去,因为C数组的数据会被紧凑打包而一致区块内的数组数据不会。

这听起来很复杂,但它是合符逻辑且定义良好的,并且使得很多的图形硬件可以高效地实现一致区块缓冲对象。回到我们的TransformBlock示例,我们可以通过上述规则计算出区块的各个成员在缓冲对象内的偏移。清单5.11展示了一个一致区块的声明,并附有各个成员的偏移:

layout (std140) uniform TransformBlock
{
    // Member               base-alignment  offset  aligned-offset
    float scale;            // 4            0       0
    vec3 translation;       // 16           4       16
    float rotation[3];      // 16           28      32 (rotation[0])
                            //                      48 (rotation[1])
                            //                      64 (rotation[2])
    mat4 projection_matrix; // 16           80      80 (column 0)
                            //                      96 (column 1)
                            //                      112 (column 2)
                            //                      128 (column 3)
} transform;

清单5.11: 一致区块示例和偏移

在原始的ARB_uniform_buffer_object扩展规格文档有各种类型的对齐的完整示例。

在使用std140布局时还可以在着色器中直接指定一致区块内各成员的偏移。当然这种情况下我们仍需遵循std140布局的对齐规则,但我们可以在成员间留一些空白以及�无次序地声明成员。使用offset布局指示器来指定一致区块内成员的偏移。清单5.12示一例:

layout (std140) uniform ManuallyLaidOutBlock
{
    layout (offset = 32) vec4 foo;  // At offset 32 bytes
    layout (offset = 8) vec2 bar;   // At offset 8 bytes
    layout (offset = 48) vec3 baz;  // At offset 48 bytes
} myBlock;

清单5.12: 带有用户指定偏移的一致区块

我们注意到在清单5.12中的一致区块的第一个成员foo被声明为始于区块的偏移32处。这是没问题的,因为32是16的整数倍(16是vec4的大小),并且bar始于偏移8处也是符合对齐需求的(8是vec2的大小)。不过bar在内存中是在foo之前的,这是因为我们并没有按照次序指定它们的位置。然后我们声明baz于偏移48处。尽管bazvec3类型的,但它必须始于16字节边界处。

我们还可以显示地让数据类型对齐在它们原始对齐边界的整数倍处。我们可以使用align布局指示器达成比目的。这和offset布局指示器的用法一致,但它只是简单地将成员推到指定对齐边界的整数倍处,只要这个边界值符合成员的对齐要求。align指示器还可以用于整个一致区块以强制所有成员对齐到指定边界处。我们可以同时使用alignoffset将成员推到大于它原始偏移的下一个偏移处或者等于它原始偏移的偏移处。

在清单5.13中我们使用偏移16重新声明了ManuallyLaidOutBlock。这个对齐边界满足vec4vec3的需求,所以foobaz的偏移不会受到影响。但bar的原始对齐边界是8,这并不是16的整数倍。所以bar会被对齐到指定对齐边界(也就是16)的下一个16字节边界处。

layout (stdout, align = 16) uniform ManuallyLaidOutBlock
{
    layout (offset = 32) vec4 foo;  // At offset 32 bytes
    layout (offset = 8) vec2 bar;   // At offset 16 bytes
    layout (offset = 48) vec3 baz;  // At offset 48 bytes
}

清单5.13:带有用户指定偏移和对齐边界的一致区块

当然,我们可以选择使用共享布局(shared layout)把所有的事情都丢给OpenGL,并且这样OpenGL可能会生成一个比std140略微高效的布局,但这么一点高效可能并不值得附加的额外工作。如果我们决意要使用共享布局,那我们可以确定出OpenGL为每个成员赋予的偏移。一致区块内的每个成员都有一个索引值,这个索引值可以用来引用这个成员以便查找成员在区块中的大小和位置。要获取一致区块内成员的索引值,调用:

void glGetUniformIndices(GLuint program,
                         GLsizei uniformCount,
                         const GLchar ** uniformNames,
                         GLuint * uniformIndices);

通过这个函数的一次调用我们就可以获得一大批一致变量的索引值(甚至是整个程式的一致变量),尽管它们是不同一致区块的成员。它接收一个参数(uniformCount)用来表示要获取索引值的一致变量的个数、一个一致变量名称的数组(uniformNames)以及最终存放获取的索引值的数组(uniformIndices)。清单5.14展示了我们如何获取TransformBlock各个成员的索引值:

static const GLchar * uniformNames[4] = 
{
    "TransformBlock.scale",
    "TransformBlock.translation",
    "TransformBlock.rotation",
    "TransformBlock.projection_matrix"
};
GLuint uniformIndices[4];

glGetUniformIndices(program, 4, uniformNames, uniformIndices);

清单5.14: 获取一致区块成员的索引值

这段代码运行之后,一致区块的四个成员的偏移将会被存放在数组uniformIndices中。现在我们掌握了一致变量的索引值,我们可以用它们来获取一致区块成员在缓冲中的位置。我们可以调用:

void glGetActiveUniformsiv(GLuint program,
                           GLsizei uniformCount,
                           const GLuint * uniformIndices,
                           GLenum pname,
                           GLint * params);

这个函数可以获取指定一致区块成员的许多信息。我们感兴趣的信息有成员在缓冲内的偏移、数组跨度(对于TransformBlock.rotation)以及矩阵跨度(TransformBlock.projection_matrix)。这些值告诉我们该把数据置于缓冲的何处以便着色器使用。我们可以将pname分别设置为GL_UNIFORM_OFFSETGL_UNIFORM_ARRAY_STRIDEGL_UNIFORM_MATRIX_STRIDE从而获得这些值。清单5.15展示了这些代码:

GLint uniformOffsets[4];
GLint arrayStrides[4];
GLint matrixStrides[4];
glGetActiveUniformsiv(program, 4, uniformIndices, GL_UNIFORM_OFFSET, uniformOffsets);
glGetActiveUniformsiv(program, 4, uniformIndices, GL_UNIFORM_ARRAY_STRIDE, arrayStrides);
glGetActiveUniformsiv(program, 4, uniformIndices, GL_UNIFORM_MATRIX_STRIDE, matrixStrides);

清单5.15:获取一致区块成员的信息

清单5.15的代码运行之后,uniformOffsets包含有区块TransformBlock成员的偏移值,arrayStrides包含有数组成员的跨度(现在只有rotation),matrixStrides包含有矩阵成员的跨度(现在只有projection_matrix)。

其他可以获得的关于一致区块成员的信息包含一致变量的数据类型、占用的内存大小以及区块内数组和矩阵的相关信息。为了初始化有更多复杂数据类型的缓冲对象我们会用到其中的一些信息,尽管在编写着色器时成员的大小和数据类型我们应该是已经知道的。pname其他可接收的值和返回数据在表5.4中进行罗列:

value of pname                              what you get back
GL_UNIFORM_TYPE                             一致变量的数据类型(GLenum)
GL_UNIFORM_SIZE                             数组的大小(以GL_UNIFORM_TYPE为单位)。
                                            若一致变量不是数组,则总为1.
GL_UNIFORM_NAME_LENGTH                      一致变量名称长度
GL_UNIFORM_BLOCK_INDEX                      一致变量所属区块的索引值
GL_UNIFORM_OFFSET                           一致变量在区块内的偏移值
GL_UNIFORM_ARRAY_STRIDE                     数组内相邻成员间的字节间隔。
                                            若一致变量不是数组,则总为0.
GL_UNIFORM_MATRIX_STRIDE                    列优先矩阵的每个列首元素间的字节间隔
                                            或者行优先矩阵的每个行首元素间的字节间隔。
                                            若一致变量不是矩阵,则总为0.
GL_UNIFORM_IS_ROW_MAJOR                     若一致变量为行优先的矩阵,则为1,反之为0.

表5.4:glGetActiveUniformsiv()可使用的一致变量查询参数

如果我们感兴趣的一致变量的类型都是简单类型,比如: int, float, bool甚至这些变量的向量(vec4之类),那我们需要的就只是它们的偏移。一旦我们获知这些一致变量在缓冲中的位置,那我们就可以通过将偏移传递给glBufferSubData()来更新适当位置的数据,或者直接用代码中在内存中组装号缓冲。我们演示后一种情况,因为这能再次凸显出一致变量是存储在内存中的,一如顶点信息可以存储在缓冲中。同时这样也会减少对OpenGL的调用次数,有时这会带来更高的性能。在下面的例子中我们在应用程序的内存中组装好数据然后用glBufferSubData()来载入到缓冲。当然我们还可以用glMapBufferRange()得到缓冲内存区域的指针,然后直接将数据组装到里面。让我们从一致区块TransformBlock中最简单的一致变量scale开始设置。这个一致变量是一个单精度浮点数,它的位置存储在数组uniformIndices的第一个元素中。清单5.16展示了如果设置这个单精度浮点数的值。

// 为缓冲分配内存(记得要释放)
unsigned char* buffer = (unsigned char*)malloc(4096);

// TransformBlock.scale存储在uniformOffset[0]偏移处,所以我们可以直接偏移到此处然后将scale存储至此
*((float*)(buffer+uniformOffsets[0])) = 3.0f;

清单5.16:设置一致区块中的一个单精度浮点数

接下来我们初始化TransformBlock.translation的数据。这是一个vec3,这意味着它由3个在内存中紧凑打包的单精度浮点数组成。要更新它的数据,我们只需要找到这个向量的第一个元素的位置并将3个连续的单精度浮点数存储在那。我们在清单5.17展示。

// 在内存中存储3个连续的GLfloat值来更新vec3
((float*)(buffer+uniformOffsets[1]))[0] = 1.0f;
((float*)(buffer+uniformOffsets[1]))[1] = 2.0f;
((float*)(buffer+uniformOffsets[1]))[2] = 3.0f;

清单5.17:获取一致区块成员的各个索引

现在来看看rotation数组。这里我们本可以用一个vec3,但是为了做为示例,我们还是用一个三元素的数组从而演示GL_UNIFORM_ARRAY_STRIDE参数的使用。当使用共享布局时,数组元素之间被一个由具体实现定义的间隔隔开,这个间隔的大小单位是字节。这意味着我们把数据存放到缓冲中时,得同时考虑GL_UNIFORM_OFFSETEGL_UNIFORM_ARRAY_STRIDE,清单5.18展示这种情况。

// TransformBlock.ratations[0]存放在缓冲的uniformOffsets[2]处。数组中的每个元素都有多个arrayStrides[2]字节间隔。
const GLfloat rotations[] = { 30.0f, 40.0f, 60.0f };
unsigned int offset = uniformOffsets[2];

for (int n = 0; n < 3; n++)
{
    *((float*)(buffer+offset)) = rotations[n];
    offset += arrayStrides[2];
}

清单5.18:指定一致区块中数组的数据。

最后我们来设置TransformBlock.projection_matrix

在一致区块中矩阵(Matrix)和向量数组表现地极为相似。对于列优先的矩阵(缺省情况)来说,矩阵的每一列即被当做一个向量,这个向量的长度即为矩阵的高度。同样的,一个行优先的矩阵也被当做一个向量数组对待,只是数组的每一个元素将是矩阵的一行。一如正常的数组,矩阵的每个行(列)的起始偏移由具体的实现定义。这个值可传递参数GL_UNIFORM_MATRIX_STRIDE来调用函数glGetActiveUniformsiv()获知。矩阵的每列可使用类似设置TransformBlock.translationvec3的代码来设置。在清单5.19中我们将进行示例。

// the first column of TransformBlock.project_matrix is at 
// uniformOffsets[3] bytes into the buffer. The columns are
// spaced matrixStride[3] bytes apart and are essentially vec4s.
// This is the source matrix - remember, it's column major.

const GLfloat matrix[] = 
{
    1.0f, 2.0f, 3.0f, 4.0f,
    9.0f, 8.0f, 7.0f, 6.0f,
    2.0f, 4.0f, 6.0f, 8.0f,
    1.0f, 3.0f, 5.0f, 7.0f
};

for (int col = 0; col < 4; col ++)
{
    GLuint offset = uniformOffsets[3] + matrixStride[3]*col;
    for (int row = 0; row < 4; row ++) 
    {
        *((float*)(buffer+offset)) = matrix[col*4+row];
        offset += sizeof(GLfloat);
    }
}

清单5.19:设置一致区块的矩阵

这种查询偏移和间距的方法适用于任何布局方式。当然对于共享布局来说这是唯一的办法。显而易见的是这样搞我们得写很多代码来准确无误地将数据放置到缓冲中。这也是我们为什么推荐使用标准布局。标准布局可以使得我们依据一套标准的规则将数据放置到缓冲中。这套规则对于所有的OpenGL实现来说都是通用的,所以我们可以无需查询任何东西从而使用这套规则(如果你乐意也可以查询偏移和间距,结果一定是正确的)。有时候我们会牺牲一点着色器性能从而换取易用性,但从中节省出的代码复杂性和应用性能是值得的。

无论我们选择哪种数据打包模式,我们都可以把程式(program)中填充数据的缓冲和一致区块进行绑定。在此之前我们得查询到一致区块的索引。程式(program)中的每个一致区块都有一个编译器赋予的索引。单个程式(program)中可用的一致区块有一个上限,每个着色器阶段也各有上限。我们可以调用glGetIntegerv()获取这些上限值,使用参数GL_MAX_UNIFORM_BUFFERS获取单个程式的一致区块上限,还可使用GL_MAX_VERTEX_UNIFORM_BUFFERSGL_MAX_GEOMETRY_UNIFORM_BUFFERSGL_MAX_TESS_CONTROL_UNIFORM_BUFFERSGL_MAX_TESS_EVALUATION_UNIFORM_BUFFERSGL_MAX_FRAGMENT_UNIFORM_BUFFERS分别获取顶点、图形、细分曲面控制、细分曲面运算和片段着色器的上限。我们可以使用

GLuint glGetUniformBlockIndex(GLuint program, const GLchar* uniformBlockName);

来获取具名一致区块(named uniform block)的索引。在我们的一致区块声明示例中,uniformBlockName应该为"TransformBlock"。

有一系列的缓冲绑定点可以用来将缓冲和一致区块绑定。将缓冲和一致区块绑定大致分为两个步骤。一致区块被赋予绑定点,然后缓冲可以绑定到绑定点上,由此缓冲便与一致区块进行了配对。通过这种绑定方式(使用绑定点解耦一致区块和缓冲),不同的程式(program)可以换入换出而毋需更改缓冲绑定,固定集合中的一致变量即可为新的程式(program)所见。与缺省区块中的一致变量对比而言,缺省区块的一致变量的值是关联于某个程式(program)的。就算两个程式(program)中的一致变量具有同样的名字,但它们的值都必须分别进行设置,而且一旦当前程式(active program)变化了它们的值也将变化。

调用

void glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);

给一致区块绑定一个绑定点。其中,program是一致区块所属的程式(program)。uniformBlockIndex是我们要赋予绑定点的一致区块索引(调用glGetUniformBlockIndex()获得)。uniformBlockBinding是一致区块绑定点的索引。一个特定的OpenGL实现有固定的绑定点上限,我们可以调用glGetIntegerv()时传递GL_MAX_UNIFORM_BUFFER_BINDINGS参数来获知这一上限。

另外,我们可以在着色器代码中直接给一致区块指定绑定点索引。我们要再次用到布局指示器,这次用到的关键字是binding。举个例子,给TransformBlock赋予绑定点索引2,我们可以如下声明:

layout(std140, binding = 2) uniform TransformBlock
{
...
} transform;

binding布局指示器可以和std140(或者其他)指示器同时指定。在着色器代码中赋予绑定可以避免调用glUniformBlockBinding(),甚至还可以避免调用glGetUniformBlockIndex()获知一致区块的索引。总的来说,在着色器代码中赋予绑定是更好的方法。

一旦我们为程式(program)中的一致区块赋予了绑定点(不管是通过glUniformBlockBingding()函数或者使用布局指示器),我们就可以将缓冲绑定到同一绑定点上,以此让缓冲中的数据为一致区块所用。我们可以调用

glBindBufferBase(GL_UNIFORM_BUFFER, index, buffer);

来达此目的。GL_UNIFORM_BUFFER指示OpenGL我们将要把一个缓冲绑定到一致区块绑定点上。index是绑定点的索引,这个索引必须与我们在着色器代码中指定的索引或者调用glUniformBlockBinding()指定的索引一致。buffer则是我们要我们要绑定的缓冲对象名字。图示5.1刻画了缓冲、一致区块、绑定点三者之间的关系

figure 5.1

在图示5.1中,一个程式(program)和三个一直区块(Harry, Bob, Susan)以及三个缓冲对象(A, B, C)。Harry被赋予绑定点1,并且缓冲C和绑定点1关联,于是缓冲C的数据将供应给Harry。同样的Bob被赋予绑定点3,然后缓冲A与绑定点3关联,于是缓冲A的数据将供应给Bob。同理Susan、绑定点0和缓冲B绑定在一起。值得注意的是绑定点2并没有被用到。这不要紧。有空着的绑定点不用很正常。

设置如上绑定关系可以用清单5.20的代码:

// Get the indices of the uniform blocks using glGetUniformBlockIndex
GLuint harry_index = glGetUniformBlockIndex(program, "Harry");
GLuint bob_index = glGetUniformBlockIndex(program, "Bob");
GLuint susan_index = glGetUniformBlockIndex(program, "Susan");

// Assign buffer bindings to uniform blocks, using their indices
glUniformBlockBinding(program, harry_index, 1);
glUniformBlockBinding(program, bob_index, 3);
glUniformBlockBinding(program, susan_index, 0);

// Bind buffers to the binding points
// Binding 0, buffer B, Susan's data
glBindBufferBase(GL_UNIFORM_BUFFER, 0, buffer_b);
// Binding 1, buffer C, Harry's data
glBindBufferBase(GL_UNIFORM_BUFFER, 1, buffer_c);
// Note that we skipped bingding 2
// Binding 3, buffer A, Bob's data
glBindBufferBase(GL_UNIFORM_BUFFER, 3, buffer_a);

清单5.20:指定一致区块的绑定

如果我们在着色器代码中用binding布局指示器就可以避免调用glUniformBlockBinding()。清单5.21将进行展示:

layout (binding = 1) uniform Harry
{
//...
};

layout (binding = 3) uniform Bob
{
//...
};

layout (binding = 0) uniform Susan
{
//...
};

清单5.21:一致区块使用binding布局指示器

包含清单5.21代码的着色器编译链接到一个程式(program)对象后,一致区块HarryBobSusan将会和执行清单5.20后设置的绑定一样。在着色器中设置一致区块绑定有几个原因。首先,这样可以降低应用中调用OpenGL函数的数量。然后,这样可以使得应用在不知道一致区块的名字(索引)下和特定绑定点进行绑定。在某些情况这很有用,比如我们有一些数据以标准布局形式存放在一个缓冲中,但我们要对不同的着色器用不同的名字来引用这个缓冲。

一致区块的一个典型用例是用来分隔常态和暂态。如果用一个标准的惯例来设置我们所有程式(program)的绑定,那我们改变当前程式(program)的时候可以不用改动缓冲的绑定。举个例子,我们有一些相对比较固定的状态(比如投影矩阵、视口大小,以及一些其他一些变化的东西),那我们可以把这些信息存放到一个缓冲中,并将这个缓冲绑定到绑定点0。然后我们把所有程式(program)的固定状态与绑定点0绑定,那任何时候我们用glUseProgram()切换程式(program),缓冲中存储的一致变量都会可用。

假设我们有一个要模拟各种材料(衣物、金属)的片段着色器,我们可以把材料的参数放到一个缓冲中。将我们着色各种材料的程式(program)中包含材料参数的一致区块绑定到绑定点1.每一个对象维护一个与之相关表面参数的缓冲对象。当我们渲染每个对象时,使用公共的材料着色器,并且简单地将相关的缓冲对象绑定到缓冲绑定点1即可。

使用一致区块还有最后一个好处是它们可以非常大。一致区块的最大尺寸可以调用glGetIntegerv()传递GL_MAX_UNIFORM_BLOCK_SIZE参数获取。OpenGL保证一致区块最小有64K字节,一个程式(program)最少可以使用14个一致区块。将上个段落的示例扩展一点,我们可以将所有材料的属性打包在一个大的一致区块中。当我们渲染场景中各个对象时,我们只需要使用不同的数组索引来使用不同的材料即可。比如这个索引我们可以使用静态顶点属性或者传统的一致变量。这种方式一定会比替换缓冲对象的内容或者切换不同对象的一致区块缓冲绑定要快。

推荐阅读更多精彩内容