GLSL之Uniform限制符

前言: 本文的主要内容是对存储限制符Uniform进行详细介绍,包括基本概念以及具体使用。
一、uniform限定符特性介绍 uniform修饰的变量为用户传递给着色器的数据,它在所有可用的着色阶段都是不变的,它必须定义为全局变量,并且存储在程序对象(Program Object)中。
uniform可以修饰任意类型以及任意类型的聚合类型,包括结构体、数组和不透明类型等。
在着色器中,无法写入到uniform变量,也无法改变它的值(对uniform变量进行写操作会导致编译错误),但是可以在定义时进行初始化操作,之后计算时就会用到初始化的值,直到这个值通过应用程序的API调用被修改。
注意:有一些平台可能不能正确实现初始化工作,所以尽量还是由应用程序传入数据。(我自己在windows上试了一下,如果我指定了一个初始化值,那么这个uniform变量虽然能够被正确初始化为给定的值,但是不能获得正确的位置,也就无法从应用程序中传递数据)
二、显式指定uniform变量的位置(Explicit uniform location) 1.指定变量
定义在uniform block之外的uniform变量都有一个位置,它可以在着色器中直接指定,例如:

layout (Location=2) uniform float scale=0.5f;

在上述代码中,就把scale变量的位置指定为了2。
在一个应用程序中,Location应当是唯一的,也就是我们不能给两个不同的uniform变量相同的Location。但是如果在不同的着色器阶段有两个相同类型相同名称的变量,那么它们的Location必须指定为相同的,否则将导致链接错误。
ARB_explicit_uniform_location:这是显示指定uniform位置的扩展库,只有包含该扩展库或者在OpenGL3.3版本以后才可以使用,需要在着色器中添加一下代码:

#extension GLARB_explicit_uniform_location : enable

也就是在该着色器中启用扩展,否则,编译器就会报错,Location qualifier not supported !
2.数组及结构体类型的变量的位置的指定与查询:除了数组和结构体类型的变量,其它变量每次只分配一个位置,而数组和结构体类型因为包含了更多的变量,所以它被分配一组位置。
数组和结构体类型的位置分配,将会被分配一组按顺序增长的位置,起始位置为自己指定的位置。
layout (Location=4) uniform float array[5];

上述代码,为数组array指定了起始位置4,剩余的元素将会依次递增,最终该数组将占据4到8共计5个位置,在查询的过程中我们可以直接使用数组名array查询,也可以使用各个元素的索引,也就是array[0]、array[1]等等。
对于结构体类型的变量,我们不能通过相关函数直接查询它的位置,只能够查询其变量内部各个元素的位置。
struct Obj{ vec4 array[3]; float scale; }; layout (Location=2) uniform Obj obj; layout (Location=2) uniform Obj some_objs[3];

上述代码中,obj对象占据4个位置,但是我们无法查询obj的位置(结果返回-1),但是可以查询内部元素obj.scale;some_objs一共占据了12个位置,因为一个结构体占据了4个位置,其中第一个数组占据前三个位置,第二个变量占据第四个位置,如果要查询结构体数组,我们就需要通过数组内每个元素的索引值查询,比如我们可以查询some_objs[1].scale,但是我们不能查询some_obj[1],因为some_obj[1]是一个结构体类型,不可以被查询。
注意:在应用过程中,由于两个变量不能拥有相同的Location,所以在为结构体和数组指定Location时要格外注意。
3.Location最大数量限制
在单个程序中,可用的Location数量是有限制的,最大数量为GL_MAX_UNIFORM_LOCATIONS(至少有1024个Location),所有变量位置的范围都应该在0到GL_MAX_UNIFORM_LOCATION-1之间。
//uniform数量限制 GLint uniformLocations; glGetIntegerv(GL_MAX_UNIFORM_LOCATIONS, &uniformLocations); cout << "uniformLocations: " << uniformLocations << endl; //uniformLocations: 4096

三、激活的uniform变量(Active Uniform) glsl的编译器和链接器为了保证效率,会尽量删除掉那些对阶段输出没有影响的代码,所以一个定义的uniform变量在链接后的程序中未必是可用的。如果一个uniform变量在链接后的程序中可用,那么我们称之为激活的,否则,它就是未激活的,未激活的变量在链接程序中将不会有任何用处,因为链接后的程序不会给未激活的变量分配一个位置。
layout (Location=2) uniform float scale=0.5f;

上述代码中,我们给scale分配了一个位置2,但是如果在着色器中没有用到scale,或者scale对着色器阶段输出没有任何影响,那么当我们在应用程序中获取scale位置时,将会返回-1,也就是无法找到该变量。
四、实现中的一些限制 可用的激活变量的最大数量会受到一系列限制,在不同的着色器阶段将会有不同的Component数量限制,每个阶段的限制如下,顶点着色器:GL_MAX_VERTEX_UNIFORM_COMPONENTS,几何着色器:GL_MAX_GEOMETRY_UNIFORM_COMPONENTS,片元着色器:GL_MAX_FRAGMENT_UNIFORM_COMPONENTS,可以通过glGetIntegerv来查询这些限制,它们通常都不少于1024。
注意:这些限制是指单个的float、int、uint和bool类型的变量的数量,一个双精度类型的变量占据的component是单精度类型的变量的两倍,一个vec3变量将会占3个components,一个mat4将会占据不多于4x4个component,一个数组占据的数量为数组大小乘以单个数组成员占据的components数量。
但是有时候,由于一些硬件原因,即使component的数量在限制范围之内,还是会出现链接失败,这时就需要假设每一个uniform分为4个component,例如一个uniform float变量占4个component,mat2x4占16个component(矩阵里每行占4个),mat4x2占8个component。
五、Uniform变量管理 在着色器程序的各个阶段,使用的都是同一个uniform变量集合,比如说:如果在顶点着色器和片元着色器都定义了相同的变量ViewMat,那么在程序对象中,就只会有一个ViewMat,修改这个值会同时影响顶点着色器和片元着色器,所以如果在两个着色器中有两个相同的变量名,但类型不同,链接器就会报错。
记住:uniform变量的位置是着色器链接的时候产生的(即使你是显式指定了位置),也就是调用了glLinkProgram()的时候。
每一个激活变量都有一个Location,如果你没有显式指定一个Location,那链接器就会自动分配,在这种情况下,即使定义了两个相同的uniform变量集,它们也不一定会有相同的Location,就需要通过API分别获取位置。
各种类型的变量的位置:
如果是基础类型,我们可以通过变量名直接查询,如果是数组类型,可以通过数组名得到起始位置,其它位置也是依次递增的,特殊的是结构体类型,如果没有为结构体类型变量指定位置,那么结构体类型内各个元素的位置是没有任何关联的(如果显式指定,各个元素位置将是递增的),需要分别查询,结构体数组也是如此。
六、Uniform数据传递 获取uniform变量的位置的目的就是为了修改该变量的值,它是通过相应的glUniform*函数来完成,细节可以参考:https://www.khronos.org/opengl/wiki/GLAPI/glUniform。
在修改值之前,需要使用glUseProgram绑定程序对象,因为glUniform*函数内并未包含程序对象,所以这个函数的操作将会应用于当前绑定的对象。
如果使用的是OpenGL4.1或者 ARB_separate_shader_objects扩展库,就可以使用glProgramUniform*来直接指定相应的程序对象,而无须提前绑定。
总结: 【GLSL之Uniform限制符】关于uniform限定符的使用,还有uniform block和uniform buffer object两个主要的概念没有阐述,在之后的文章里会逐渐完善这部分内容。

    推荐阅读