本节书摘来自华章出版社《opengl编程指南》一书中的第2章,第2.4节,作者 bill licea-kane ,更多章节内容可以访问云栖社区“华章计算机”公众号查看
着色器与应用程序之间,或者着色器各阶段之间共享的变量可以组织为变量块的形式,并且有的时候必须采用这种形式。uniform变量可以使用uniform块,输入和输出变量可以使用in和out块,着色器的存储缓存可以使用buffer块。
它们的形式都是类似的。首先了解一下uniform块的写法。

各种类型的块接口的详细介绍如下文所示。综合来说,块(block)开始部分的名称(上面的代码中为b)对应于外部访问时的接口名称,而结尾部分的名称(上面的代码中为name)用于在着色器代码中访问具体成员变量。
如果着色器程序变得比较复杂,那么其中用到的uniform变量的数量也会上升。通常会在多个着色器程序中用到同一个uniform变量。由于uniform变量的位置是着色器链接的时候产生的(也就是调用gllinkprogram()的时候),因此它在应用程序中获得的索引可能会有变化,即使我们给uniform变量设置的值可能是完全相同的。而uniform缓存对象(uniform buffer object)就是一种优化uniform变量访问,以及在不同的着色器程序之间共享uniform数据的方法。
正如你所知道的,uniform变量是同时存在于用户应用程序和着色器当中的,因此需要同时修改着色器的内容并调用opengl函数来设置uniform缓存对象。
访问一组uniform变量的方法是使用诸如glmapbuffer()的opengl函数(参见第3章),但是我们需要在着色器中对它们的声明方式略作修改。不再分别声明每个uniform变量,而是直接将它们成组,形成一个类似结构体的形式,也就是uniform块。一个uniform块需要使用关键字uniform指定。然后将块中所有需要用到的变量包含在一对花括号当中,如例2.3所示。
例2.3 声明一个uniform块
注意,着色器中的数据类型有两种:不透明的和透明的;其中不透明类型包括采样器、图像和原子计数器。一个uniform块中只可以包含透明类型的变量。此外,uniform块必须在全局作用域内声明。
uniform块的布局控制
在uniform块中可以使用不同的限制符来设置变量的布局方式。这些限制符可以用来设置单个的uniform块,也可以用来设置所有后继uniform块的排列方式(需要使用布局声明)。可用的限制符及其介绍如表2-12所示。
例如,如果需要共享一个uniform块,并且使用行主序的方式来存储数据,那么可以使用下面的代码来声明它:
多个限制符可以通过圆括号中的逗号来分隔。如果需要对所有后继的uniform块设置同一种布局,那么可以使用下面的语句:
这样一来,当前行之后的所有uniform块都会使用这种布局方式,除非再次改变全局的布局,或者对某个块的声明单独设置专属的布局方式。
访问uniform块中声明的uniform变量
虽然uniform块已经命名了,但是块中声明的uniform变量并不会受到这个命名的限制。也就是说,uniform块的名称并不能作为uniform变量的父名称,因此在两个不同名的uniform块中声明同名变量会在编译时造成错误。然而,在访问一个uniform变量的时候,也不一定非要使用块的名称。
uniform变量是着色器与应用程序之间共享数据的桥梁,因此如果着色器中的uniform变量是定义在命名的uniform块中,那么就有必要找到不同变量的偏移值。如果获取了这些变量的具体位置,那么就可以使用数据对它们进行初始化,这一过程与处理缓存对象(使用glbufferdata()等函数)是一致的。
首先假设已知应用程序的着色器中uniform块的名字。如果要对uniform块中的uniform变量进行初始化,那么第一步就是找到块在着色器程序中的索引位置。可以调用glgetuniformblockindex()函数返回对应的信息,然后在应用程序的地址空间里完成uniform变量的映射。
gluint glgetuniformblockindex(gluint program, const char * uniformblockname);
返回program中名称为uniformblockname的uniform块的索引值。如果uniformblockname不是一个合法的uniform程序块,那么返回gl_invalid_index。
如果要初始化uniform块对应的缓存对象,那么我们需要使用glbindbuffer()将缓存对象绑定到目标gl_uniform_buffer之上,如后文中的示例所示(第3章将会给出更详细的解释)。
当对缓存对象进行初始化之后,我们需要判断命名的uniform块中的变量总共占据了多大的空间。我们可以使用函数glgetactiveuniformblockiv()并且设置参数为gl_uniform_block_data_size,这样就可以返回编译器分配的块的大小(根据uniform块的布局设置,编译器可能会自动排除着色器中没有用到的uniform变量)。glgetactiveuniformblockiv()函数还可以用来获取一个命名的uniform块的其他一些相关参数。
在获取uniform块的索引之后,我们需要将一个缓存对象与这个块相关联。最常见的方法是调用glbindbufferrange(),或者如果uniform块是全部使用缓存来存储的,那么可以使用glbindbufferbase()。
void glbindbufferrange(glenum target, gluint index, gluint buffer, glintptr offset, glsizeiptr size);
void glbindbufferbase(glenum target, gluint index, gluint buffer);
将缓存对象buffer与索引为index的命名uniform块关联起来。target可以是gl_uniform_buffer(对于uniform块)或者gl_transform_feedback_buffer(用于transform feedback,参见第5章)。index是uniform块的索引。offset和size分别指定了uniform缓存映射的起始索引和大小。
调用glbindbufferbase()等价于调用glbindbufferrange()并设置offset为0,size为缓存对象的大小。
在下列情况下调用这两个函数可能会产生opengl错误gl_invalid_value:size小于0;offset + size大于缓存大小;offset或size不是4的倍数;index小于0或者大于等于gl_max_uniform_buffer_bindings的返回值。
当建立了命名uniform块和缓存对象之间的关联之后,只要使用缓存相关的命令即可对块内的数据进行初始化或者修改。
我们也可以直接设置某个命名uniform块和缓存对象之间的绑定关系,也就是说,不使用链接器内部自动绑定块对象并且查询关联结果的方式。如果多个着色器程序需要共享同一个uniform块,那么你可能需要用到这种方法。这样可以避免对于不同的着色器程序同一个块有不同的索引号。如果需要显式地控制一个uniform块的绑定方式,可以在调用gllinkprogram()之前调用gluniformblockbinding()函数。
glint gluniformblockbinding(gluint program, gluint uniformblockindex, gluint uniformblockbinding);
显式地将块uniformblockindex绑定到uniformblockbinding。
在一个命名的uniform块中,uniform变量的布局是通过各种布局限制符在编译和链接时控制的。如果使用了默认的布局方式,那么需要判断每个变量在uniform块中的偏移量和数据存储大小。为此,需要调用两个命令:glgetuniformindices()负责获取指定名称uniform变量的索引位置,而glgetactiveuniformsiv()可以获得指定索引位置的偏移量和大小,如例2.4所示。
void glgetuniformindices(gluint program, glsizei uniformcount, const char* uniformnames, gluint uniformindices);
返回所有uniformcount个uniform变量的索引位置,变量的名称通过字符串数组uniformnames来指定,程序返回值保存在数组uniformindices当中。在uniformnames中的每个名称都是以null来结尾的,并且uniformnames和uniformindices的数组元素数都应该是uniformcount个。如果在uniformnames中给出的某个名称不是当前启用的uniform变量名称,那么uniformindices中对应的位置将会记录为gl_invalid_index。
例2.4 初始化一个命名uniform块中的uniform变量
glsl中的buffer块,或者对于应用程序而言,就是着色器的存储缓存对象(shader storage buffer object),它的行为类似uniform块。不过两者之间有两个决定性的差别,使得buffer块的功能更为强大。首先,着色器可以写入buffer块,修改其中的内容并呈现给其他的着色器调用或者应用程序本身。其次,可以在渲染之前再决定它的大小,而不是编译和链接的时候。例如:
如果在着色器中没有给出上面的数组的大小,那么可以在应用程序中编译和连接之后,渲染之前设置它的大小。着色器中可以通过length()方法获取渲染时的数组大小。
着色器可以对buffer块中的成员执行读或写操作。写入操作对着色器存储缓存对象的修改对于其他着色器调用都是可见的。这种特性对于计算着色器非常有意义,尤其是对非图像的内存区域进行处理的时候。
有关buffer块的内存限制符(例如coherent)以及原子操作的相关深入讨论请参见第11章。
设置着色器存储缓存对象的方式与设置uniform缓存的方式类似,不过glbindbuffer()和glbufferdata()需要使用gl_shader_storage_buffer作为目标参数。我们可以在11.2节中看到一个更完整的例子。
如果你不需要写入缓存中,那么可以直接使用uniform块,并且硬件设备本身可能也没有足够的资源空间来支持buffer块,但是uniform块通常是足够的。
着色器变量从一个阶段输出,然后再输入到下一个阶段中,这一过程可以使用块接口来表示。使用逻辑上成组的方式来进行组织也更有利于判断两个阶段的数据接口是否一致,同样对单独程序的链接也会变得更为简单。
例如,一个顶点着色器的输出可能为:
顶点着色器可以输出材质和光照的信息,并且都分成独立的数据块。opengl着色语言中内置的接口同样也是以块的方式存在的,例如gl_pervertex,其中包含了内置变量gl_position等信息。我们可以在附录c中找到一个完整的内置变量列表。