文章目录
可编程管线
4.5版本的图形管线有4个处理阶段,还有1个通用计算阶段,每个阶段都需要由一个专门的着色器进行控制。
- 顶点着色阶段(vertex shading stage)
- 细分着色阶段(tessellation shading stage)
- 几何着色阶段(geometry shading stage)
- 片元着色阶段(fragment shading stage)
- 计算着色阶段(compute shading stage)
着色阶段之间,输入和输出的所有数据,都是通过着色器中的特殊全局变量传递。
着色语言
1. 概述
# version 450 core
void
main()
{
// 在这里编写代码
}
- 在程序起始位置,总是要使用#version来声明所使用的版本
- 输入点事main()函数,没有参数,没有返回值
- //和/**/,为注释符号
- 结尾必有分号
2. 变量
-
初始化
所有变量尽可能声明即初始化。
-
构造函数
GLSL比c++更注重类型安全,因此支持的数值隐式转换更少。
所需的类型 可以从这些类型隐式转换 uint int float int、uint double int、uint、float 上述类型转换可以应用于标量、向量以及矩阵,不能应用于数组和结构体。
其他数值的转换,需要使用显式的转换构造函数。
float f = 10.0; int ten = int(f); -
聚合类型
基本类型 2D向量 3D向量 4D向量 矩阵类型 float vec2 vec3 vec4 mat2 mat3 mat4 mat2×2 mat2×3 mat2×4 mat3×2 mat3×3 mat3×4 mat4×2 mat4×3 mat4×4 double dvec2 dvec3 dvec4 dmat2 dmat3 dmat4 dmat2×2 dmat2×3 dmat2×4 dmat3×2 dmat3×3 dmat3×4 dmat4×2 dmat4×3 dmat4×4 int ivec2 ivec3 ivec4 —— uint uvec2 uvec3 uvec4 —— bool bvec2 bvec3 bvec4 —— -
初始化过程,与它们标量部分类似:
vec3 velocity = vec3(0.0,2.0,3.0); -
类型转换
ivec3 steps = ivec3(velocity); -
较长向量传递给较短向量的构造函数,向量自动取短
vec4 color; vec3 RGB = vec3(color); //现在RGB只有三个分量了 -
向量加长
vec3 white = vec3(1.0); //white = (1.0,1.0,1.0) vec4 translucent = vec4(white,0.5); -
矩阵初始化,可以用少量的参数,进行对角矩阵或完全填充的矩阵初始化。
m = m a t 3 ( 4.0 ) = ( 4.0 0.0 0.0 0.0 4.0 0.0 0.0 0.0 4.0 ) m=mat3(4.0)= \begin{pmatrix} 4.0 & 0.0 & 0.0 \\ 0.0 & 4.0 & 0.0 \\ 0.0 & 0.0 & 4.0 \end{pmatrix} m=mat3(4.0)= 4.00.00.00.04.00.00.00.04.0 -
矩阵初始化,可以是指定每一个元素,也可以标量和向量的集合混合传入,数据量够即可。初始化需遵循主序的原则,传入数据先为列,后为行。
mat3 M = mat3(1.0,2.0,3.0 4.0,5.0,6.0, 7.0,8.0,9.0); vec3 column1 = vec3(1.0,2.0,3.0); vec3 column2 = vec3(4.0,5.0,6.0); vec3 column3 = vec3(7.0,8.0,9.0); mat3 M = mat3(column1,column2,column3); //亦可 vec2 column1 = vec2(1.0,2.0); vec2 column2 = vec2(4.0,5.0); vec2 column3 = vec2(7.0,8.0); mat3 M = mat3(column1,3.0, column2,6.0, column3,9.0); -
元素访问
向量可以使用分量名称,或者数组访问的形式。
矩阵可以使用二维数组的形式。
向量分量的访问符 分量访问符 符号描述 (x,y,z,w) 与位置相关的分量 (r,g,b,a) 与颜色相关的分量 (s,t,p,q) 与纹理坐标相关的分量 这种分量访问符一个常见应用叫swizzle,对于颜色的处理,比如颜色空间的转换时可能会用到它。例如,基于输入颜色的红色分量来设置一个亮度值:
vec3 luminance = color.rrr;如果需要改变向量中分量各自的位置,可以这样做:
color = color.abgr; //反转color的每个分量限制条件,一条语句的一个变量中,只能使用一中类型的访问符;访问元素,也不能超出变量类型的范围。
-
-
结构体
从逻辑上可将不同类型的数据组合到一个结构体中。结构体可以简化多组数据传入函数的过程。如果定义一个结构体,会自动创建一个新类型,并隐式定义一个构造函数。
struct Particle{ float lifetime; vec3 position; vec3 velocity; }; Particle p = Particle(10.0,pos,vel); -
数组
GLSL支持任意类型的数组,包括结构体数组。GLSL4.3中,数组的元素,也可以是另一个数组。
数组定义可以有大小,亦可没有大小。可以使用没有大小的数组作为一个数组变量的前置声明,然后重新用一个合适的大小来声明它。
float coeff[3]; //有3个float元素的数组 float[3] coeff; //与上面相同 int indices[]; //未定义维数,稍后可以重新声明它的维数数组属于GLSL中的第一等(first-class)类型,也就是说它有构造函数,并且可以用作函数的参数和返回类型。
-
静态初始化数组值
float coeff[3] = float[3](2.38,3.14,42.0);构造函数的维数值可不填。
-
隐式方法length(),返回元素的个数
for(int i = 0;i < coeff.length();++i){ coeff[i] *= 2.0; } -
向量和矩阵类型,也可用length()方法。向量的长度为分量个数,矩阵长度为列个数。
mat3x4 m; int c = m.length(); //m包含的列数为3 int r = m[0].length(); //第0个列向量中分量的个数为4 -
对于所有向量和矩阵,大部分数组来说,长度值在编译时已知,所以length()方法会返回一个编译时常量,可以在需要常量的场合直接使用。
mat4 m; float diagonal[m.length()]; //设置数组的大小与矩阵大小相等 float x(gl_in.length()); //设置数组的大小与几何着色器的输入顶点数相等对于某些数组,length()的值在链接之前可能都是未知。例如,使用链接器来减少同一阶段中多个着色器的大小,可能导致这种情况发生;着色器中保存的缓存对象,length()的值直到渲染时才能得到。
-
多维数组相当于数组中创建数组,可以使用任何类型或形式构成。如果需与应用程序共享,则最内层(最右侧)维度的数据,在内存布局中变化最快。
3.存储限制符
GLSL的类型修饰符 类型修饰符 描述 const 将一个变量定义为只读形式。如果它初始化时用的是一个编译时常量,那么它本身也会成为编译时常量。 in 设置这个变量为着色器阶段的输入变量 out 设置这个变量为着色器阶段的输出变量 uniform 设置这个变量为用户应用程序传递给着色器的数据,它对于给定的图元而言是一个常量 buffer 设置应用程序共享的一块可读写的内存。这块内存也作为着色器中的存储缓存(storage buffer)使用 shared 设置变量是本地工作组(local work group)中共享的。它只能用于计算着色器中 -
-
uniform 存储限制符
在着色器运行之前,uniform修饰符可以指定一个在应用程序中设置好的变量,它不会在图元处理的过程中发生变化。
uniform变量在所有可用的着色阶段之间共享,因此必须为全局变量。
任何类型的变量(包括结构体和数组)都可设为uniform变量。
着色器无法写入uniform值,也无法改变。
uniform vec4 BaseColor;在着色器中,可根据名字BaseColor来引用这个变量。GLSL编译器会在链接着色器程序时创建一个uniform变量列表。
-
若需在应用程序中,设置uniform变量的值,则首先需要获得变量在列表中的索引。
GLint glGetUniformLocation(GLuint program,const char* name);说明:返回着色器程序中uniform变量name对应的索引值。
参数:
name:一个以NULL结尾的字符串,不存在空格。
若name在列表中找不到,或为内部保留变量名,则返回-1。
name可以是:单一变量名称;
数组,指定数组名获取第一个元素(“arrayName”),也可以指定索引值获取元素(“arrayName[0]”);
结构体的域变量,结构体变量名之后添加"."符号,再添加域变量名称。
-
获得索引值后,就可以设置uniform变量的值了。
void glUniform{1234}{fdi ui}(GLint location,TYPE value); void glUniform{1234}{fdi ui}v(GLint location,GLsizei count,const TYPE* values); void glUniform{234}{fd}v(GLint location,GLsizei count,GLboolean transpose,const GLfloat *values); void glUniformMatrix{2x3,2x4,3x2,3x4,4x2,4x3}{fd}v(GLint location,GLsizei count,GLboolean transpose,const GLfloat* values);说明:设置与location索引位置对应的uniform变量的值。
对于glUniformMatrix{234}*()系列函数来说,可以从values中读入2x2、3x3或者4x4个值来构成矩阵。
对于glUniformMatrix{2x3,2x4,3x2,3x4,4x2,4x3}*()系列函数来说,可以从values中读入对应矩阵维度的数值并构成矩阵。
参数:
location:变量索引值。
count:指定函数,载入数据的数量;
向量形式的函数,会载入指定个数的数据集合(根据glUniform*的调用方式,读入1~4个值),写入向量中。
若location是数组的起始索引值,那么数组之后的连续count个元素都会被载入。
value:设置的单个数据
values:设置的count个数据集
transpose:对于矩阵变量的载入,GL_TRUE,则以行主序的顺序读入,GL_FALSE,以列主序的顺序读入数据。
-
-
buffer存储限制符
buffer修饰符指定"块"作为着色器与应用程序共享的内存缓存。这块内存缓存对于着色器,可读可写。缓存大小可在着色器编译和程序链接完成后设置。
4.语句
-
流控制语句
GLSL除了break、continue、return[结果]外,还有discard语句,它只适用于片元着色器中,片元着色器的运行会在discard语句的位置上立即终止,不过这也取决于具体的硬件实现。
-
函数
-
声明
与c语言相似,只是变量名需要添加访问修饰符:
returnType functionName([accessModifier] type1 variable1, [accessModifier] type2 variable2, ...) { //函数体 return returnValue; }在使用一个函数前,必须声明它的原型或直接给出函数体。如果函数的定义和使用,不再同一个着色器对象中,那么必须声明一个函数原型,如:
float HornerEvalPolynomial(float coeff[10],float x); -
参数限制符
GLSL没有指针和引用的概念,需要使用参数限制符,来表明它是否传入数据,还是返回数据。
参数的访问修饰符 访问修饰符 描述 in 将数据拷贝到函数中(如果没有指定修饰符,默认这种形式) const in 将只读数据拷贝到函数中 out 从函数中获取数值(因此输入函数的值是未定义的) inout 将数据拷贝到函数中,并且返回函数中修改的数据 如果需要在编译时,验证函数是否修改了某个输入变量,可以添加一个const in修改符阻止函数对变量的修改。如果仅仅写入一个in类型的变量,相当于对变量的局部拷贝进行了修改,仅能在函数范围内起作用。
-
5.计算的不变性
GLSL无法保证在不同的着色器中,两个完全相同的计算式,会得到一个完全一样的结果。
这与CPU端应用程序进行计算时的问题相同,即不同的优化方式可能会导致结果非常细微的差异。
这些细微的差异对于多通道的算法会产生问题,因为各个着色器阶段可能需要计算得到完全一致的结果。
需要考虑计算的不变性主要在以下几种情况:
-
跨着色器间的一致性
当需要在不同的着色器(如顶点着色器与片段着色器)之间传递计算结果,并期望这些结果在不同着色器中的计算保持一致时,需要确保计算的不变性。由于不同的着色器可能采用不同的优化策略,导致即使是相同的计算式和输入,也可能产生细微的差异。
-
调试与验证
在调试着色器程序时,为了验证着色器代码的正确性,可能需要确保某些计算结果在不同运行或不同优化级别下保持一致。这时,使用不变性限定符(如GLSL中的
invariant)可以帮助开发者确保这种一致性。 -
跨平台与兼容性
在不同的硬件平台或驱动程序上运行着色器程序时,由于硬件和驱动的差异,可能导致相同的着色器代码产生不同的输出。为了确保跨平台的一致性,可能需要考虑使用不变性限定符来限制这种差异。
-
精确计算需求
在某些需要高精度计算的场景中(如物理模拟、光线追踪等),即使是很小的计算差异也可能导致显著的结果偏差。因此,在这些场景中,需要特别关注计算的不变性,确保结果的准确性。
-
避免优化导致的错误
某些编译器优化可能会改变着色器的行为,导致不符合预期的结果。在这种情况下,使用不变性限定符可以阻止这些优化,从而避免潜在的错误。
GLSL有两种方式确保计算不变性,即invariant或者precise关键字。
这两种方法都需要在图形设备上完成计算过程,来确保同一表达式的不变性(重复性);
uniform float ten; //假设应用程序设置这个值为10.0
const float f = sin(10.0); //常量表达式,宿主机的编译器负责计算
float g = sin(ten); //图形硬件负责计算
void main(){
if(f == g); //f和g不一定相等
}
-
invariant 限制符
可以设置任何着色器的输出变量。确保如果两个着色器的输出变量使用了相同的表达式,并且表达式中的变量也是相同值,那么计算结果也相同。
可用于声明内置的输出变量,也可用于声明用户自定义的变量。例如:
invariant gl_Position; invariant centroid out vec3 Color;调试过程中,可能需要将着色器中的所有可变变量都设置为invariance。可以通过顶点着色器的预编译命令pragma来完成这项工作。
#pragma STDGL invariant(all)全局设置为invariance可以帮助我们解决调试问题;但是,着色器性能会受到影响,一些优化工作会被迫停止。
-
precise限制符
可以设置任何计算中的变量或者函数返回值。它的用途并不是增加数据精度,而是增加计算的可复用性。
通常在细分着色器中用它来避免造成几何形状的裂缝。
总体上说,如果必须保证某个表达式产生的结果是一致的,即使表达式中的数据发生了变化(但在数学上并不影响结果)也是如此,那么就应使用precise而非invariant。例如:
Location = a * b + c * d;上面的表达式中,即使a和b发生了变换,c和d发生了变换,都应得到同样的计算结果。
precise限制符可以设置内置变量、用户变量、或者函数的返回值。
precise gl_Position; precise out vec3 Location; precise vec3 subdivide(vec3 P1,vec3 P2){ ... }着色器中,关键字precise可以在使用某个变量之前的任何位置上,设置这个变量,并且可以修改之前已经声明过的变量。
编译器使用precise的一个实际影响是,类似上面的表达式不能再使用两种不同的乘法命令来同时参与计算。例如,第一次相乘使用普通的乘法,而第二次相乘使用混合乘加运算(fused multiply-and-add,fma)。这是因为这两个命令对同一组的计算结果,可能会存在微小的差异。而这种差异是precise所不允许的,因此编译器会直接阻止你在代码中这样做。
由于混合乘加运算对于性能的提升非常重要,因此不能完全禁止使用它们。所以GLSL提供了一个内置的函数fma(),让用户使用这个函数代替原先的操作,
precise out float result; float f = c * d; float result = fma(a,b,f);当然,如果不需要考虑交换a和c的值,那么没有必要使用这种写法,也没有必要使用precise了。
6.预处理的命令
-
宏定义
GLSL预处理器可以采取与C语言预处理器类似的宏定义方式,不过它不支持字符串替换以及预编译连接符。
宏可以定义单一值或带参数值,例如
#define NUM_ELEMENTS 10 #define LPos(n) gl_LightSource[(n)].positionGLSL预处理器中的预定义宏 宏命名 描述 __LINE__ 行号,默认为已经处理的所有换行符的个数加一,也可以通过#line命令修改 __FILE__ 当前处理的源字符串编号 __VERSION__ OpenGL着色器版本的整数形式表示
7.编译器的控制
#pragma 命令可以向编译器传递附加信息,并在着色器代码编译时设置一些额外属性。
-
编译器优化选项
优化选项用于启用或者禁用着色器的优化,它会直接影响该命令所在的着色器源代码。
#pragma debug(on) #pragma debug(off)默认开启优化选项。
-
编译器调试选项
调试选项可以启用或者禁用着色器的额外诊断信息输出。
#pragma debug(on) #pragma debug(off)默认情况下,所有着色器都会禁用调试选项。
8.全局着色器的编译选项
-
STDGL
#pragma STDGL选项,可用于启用所有输出变量值的不变性检查。 -
扩展功能处理
GLSL与OpenGL类似,可以通过扩展的方式来增加功能。设备生产商也可以在自己的OpenGL实现中加入特殊的扩展,因此需要对着色器中,可能用到的扩展功能进行编译级别的控制。
GLSL预处理器提供了#extension命令,用于提示着色器的编译器在编译时如何处理可用的扩展内容。对于任何扩展,或者全部扩展,都可以在编译器编译过程中设置它们的处理方式。
#extension extension_name : <directive> //这只是调用一个扩展功能 #extension all : <directive> //影响所有扩展行为GLSL扩展命令修饰符 命令 描述 require 如果无法支持给定的扩展功能,或者被设置为all,则提示错误 enbale 如果无法支持给定的扩展功能,则给出警告;如果设置为all,则提示错误 warn 如果无法支持给定的扩展功能,或者在编译过程中使用了任何扩展,则给出警告 disable 禁止支持给定的扩展(即强制编译器不提供对扩展功能的支持),或者如果设置为all,则禁止所有的扩展支持,之后当代码中涉及这个扩展使用时,提示警告或错误
数据块接口
着色器与应用程序之间,或者着色器各个阶段之间共享的变量,可以组织为变量块的形式,并且有的时候必须采用这种形式。
uniform变量可以使用uniform块,输入和输出变量可以使用in和out块,着色器的存储缓存可以使用buffer块。
uniform b{ //限定符可以为uniform、in、out或者buffer
vec4 v1; //块中的变量列表
bool v2; //...
}; //访问匿名块成员时使用v1、v2
uniform b{ //限定符可以为uniform、in、out或者buffer
vec4 v1; //块中的变量列表
bool v2; //...
} name; //访问有名块成员时使用name.v1、name.v2
块(block)开始部分的名称(上面的b)对应于外部访问时的接口名称,而结尾部分的名称(上面的name)用于着色器代码中访问具体成员变量。
uniform块
如果着色器程序变化得比较复杂,那么其中用到的uniform变量的数量也会上升。通常会在多个着色器程序中用到同一个uniform变量。由于uniform变量的位置是着色器链接的时候产生的(也就是调用glLinkProgram()的时候),因此它在应用程序中获得的索引可能会有变化,即使我们给uniform变量设置的值可能是完全相同的。
而uniform缓存对象(uniform buffer object)就是一种优化uniform变量访问,以及在不同的着色器程序之间共享uniform数据的方法。
-
声明方式
uniform Matrices{ mat4 ModelView; mat4 Projection; mat4 Color; };着色器中的数据类型有两种:不透明的和透明的;其中不透明类型包括采样器、图像和原子计数器。一个uniform块中只可以包含透明类型的变量,uniform块必须在全局作用域中声明。
-
布局控制
使用不同的限制符来设置变量的布局方式。这些限制符可以用来设置单个的uniform块,也可以用来设置所有后继uniform块的排列方式。
uniform的布局限制符 布局限制符 描述 binding = N 设置缓存的绑定位置,需要用到OpenGL API shared 设置uniform块是多个程序间共享的(这是默认的布局方式,与shared存储限制符不存在混淆) packed 设置uniform块占用最小的内存空间,但是这样会禁止程序间共享这个块 std140 使用标准布局方式来设置uniform块或者着色器存储的buffer块 std430 使用标准布局方式来设置uniform块 offset=N 强制设置成员变量位于缓存的N字节偏移处 align = N 强制设置成员变量的偏移设置是N的倍数 row_major 使用行主序的方式来存储uniform块中的矩阵 column_major 使用列主序的方式来存储uniform块中的矩阵(默认顺序) 例如:
//共享一个uniform块,以行主序的方式存储数据,多个限制符通过圆括号中逗号分隔 layout(shared,row_major) uniform {...} //对所有后续的uniform块设置同一种布局 layout(packed,column_major) uniform;使用全局的设置方式,除非再次改变全局的布局,或者对某个块的声明单独设置专属的布局方式。
对于着色器和应用程序之间共享的一块缓存,由于两这都需要确认成员变量所处的内存偏移地址,因此需要明确布局设置,这就是std140和std430所提供的功能。
std140和std430仅是提供了一块比较合理的显式缓存布局,如果希望更好的缓存布局控制方式。可以通过offset限制符来控制成员的精确位置,或者用align限制符来设置一个模糊的对齐方式。
连续的无限制符成员变量会自动进行偏移位置的对齐,这也是std140和std430的标准。
#version 440 layout (std140) uniform b{ float size; //默认从0字节位置开始 layout(offset=32) vec4 color; //从32字节开始 layout(align=1024) vec4 a[12]; //从下一个1024倍数的字节位置开始 vec4 b[12]; //从a[12]之后的偏移地址开始 }在用户程序设置缓存结构体的时候,可以使用C/C++ struct结构体形式的语言工具,也可以直接向缓存的偏移地址写入数据。这里唯一的问题就是偏移值和对齐方式的可读性。
成员变量的地址偏移值是逐渐增加的,并且必须按照std140或者std430的规则对齐。
对于浮点数和双精度浮点数的结构体来说,对齐过程是自然的,只不过std140需要对类似vec4这样的类型,增加一个额外的16字节对齐的限制。
N的定义:GLSL的布局限制符在任何时候都是layout(ID = N)的形式,这里的N必须是一个非负整数。对于#version 430或者更早版本来说,它必须是一个字面整数值。不过从#version 440开始,N也是一个常整数的表达式了。
-
访问uniform块中变量
uniform块的名称并不能作为uniform变量的父名称,因此在两个不同名的uniform块中声明同名变量会在编译时报错。同时,访问一个uniform变量的时候,也不一定非要使用块的名称。
-
应用程序访问uniform块
uniform变量是着色器与应用程序直接共享数据的桥梁,因此如果着色器中的uniform变量是定义在命名的uniform块中,那么就有必要找到不同变量的偏移值。获取了这些变量的具体位置,就可以使用数据对它们进行初始化。
-
找到块在着色器程序中的索引位置
GLuint glGetUniformBlockIndex(GLuint program,const char* uniformBlockName);返回program中名称为uniformBlockName的uniform块的索引值。如果uniformBlockName不是一个合法的uniform程序块,那么返回GL_INVALID_INDEX。
-
绑定缓存
获取uniform块的索引之后,需要将一个缓存对象与块相关联。最常见的方法为
glBindBUfferRange(),块全部使用缓存来存储,可以用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:必须是支持索引的某个缓存绑定目标。
index:uniform块的索引。
offset:指定uniform缓存映射的起始索引。
size:指定uniform缓存映射的大小。
注:调用
glBindBufferBase()等价于调用glBindBUfferRange()并设置offset为0,size为缓存对象大小。下列情况调用函数,可能会产生OpenGL错误GL_INVALID_VALUE:size小于0;offset+size大于缓存大小;offset或size不是4的倍数;index小于0或者大于等于target设置的绑定目标所支持的最大索引数。
-
数据初始化或修改
当建立了命名uniform块和缓存对象之间的关联之后,只要使用缓存相关的命令即可对块内的数据进行初始化或者修改。
-
直接绑定方式,避免不同着色器同块不同号
可以直接设置某个命名uniform块和缓存对象之间的绑定关系,而不使用链接器内部自动绑定对象并且查询关联结果的方式。
对于多个着色器程序共享一个uniform块,则需要使用这种方法。可以避免对于不同的着色器程序同一个块有不同的索引号。
如果需要显式地控制一个uniform块的绑定方式,在调用
glLinkProgram()之前调用glUniformBlockBinding()函数。GLint glUniformBlockBinding(GLuint program,GLuint uniformBlockIndex,GLuint uniformBlockBinding); // 显示地将块uniformBlockIndex 绑定到 uniformBlockBinding -
获取uniform块中变量的偏移量和数据存储大小
一个命名的uniform块中,uniform变量的布局是通过各种布局限制符在编译和链接时控制的。若使用默认的布局方式,则需要判断每个变量在uniform块中的偏移量和数据存储大小。
需要调用两个命令:
glGetUniformIndices()负责获取指定名称uniform变量的索引位置,而glGetActiveUniformsiv()可以获得指定索引位置的偏移量和大小。void glGetUniformIndices(GLuint program,GLsizei uniformCount,const char** uniformName,GLuint* uniformIndices);返回所有uniformCount个uniform变量的索引位置,变量的名称通过字符串数组uniformNames来指定,程序返回值保存在数组uniformIndices当中。
在uniformNames中的每个名称都是以NULL来结尾的,并且uniformNames和uniformIndices的数组元素数都应该是uniformCount个。
如果在uniformNames中给出的某个名称不是当前启用的uniform变量名称,那么uniformIndices中对应的位置将会记录为GL_INVALID_INDEX。
-
-
uniform块例程
顶点着色器,uniform.vert
#version 330 core uniform Uniforms{ vec3 translation; float scale; vec4 rotation; bool enabled; }; in vec2 vPos; in vec3 vColor; out vec4 fColor; void main(){ vec3 pos = vec3(vPos,0.0); float angle = radians(rotation(0)); vec3 axis = normalize(rotation.yzw); mat3 I = mat3(1.0); mat3 S = mat3(0,-axis.z,axis.y, axis.z,0,-axis.x, -axis.y,axis.x,0); mat3 uuT = outerProduct(axis,axis); mat3 rot = uuT + cos(angle)*(I - uuT) + sin(angle)*S; pos *= scale; pos *= rot; pos += translation; fColor = vec4(scale,scale,scale,1); gl_Position = vec4(pos,1); }片元着色器,uniform.frag
#version 330 core uniform Uniforms{ vec3 translation; float scale; vec4 rotation; bool enable; }; in vec4 fColor; out vec4 color; void main(){ color = fColor; }C++源代码,uniform.cpp
//用于将GLSL类型转换为存储大小的辅助函数 size_t TypeSize(GLenum type){ size_t size; #define CASE(Enum,Count,Type)\ case Enum: size = Count * sizeof(Type);break switch(type){ CASE(GL_FLOAT, 1,GLfloat); CASE(GL_FLOAT_VEC2, 2,GLfloat); CASE(GL_FLOAT_VEC3, 3,GLfloat); CASE(GL_FLOAT_VEC4, 4,GLfloat); CASE(GL_INT, 1,GLint); CASE(GL_INT_VEC2, 2,GLint); CASE(GL_INT_VEC3, 3,GLint); CASE(GL_INT_VEC4, 4,GLint); CASE(GL_UNSIGNED_INT, 1,GLuint); CASE(GL_UNSIGNED_INT_VEC2, 2,GLuint); CASE(GL_UNSIGNED_INT_VEC3, 3,GLuint); CASE(GL_UNSIGNED_INT_VEC4, 4,GLuint); CSAE(GL_BOOL, 1,GLboolean); CASE(GL_BOOL_VEC2, 2,GLboolean); CASE(GL_BOOL_VEC3, 3,GLboolean); CASE(GL_BOOL_VEC4, 4,GLboolean); CASE(GL_FLOAT_MAT2, 4,GLfloat); CASE(GL_FLOAT_MAT2x3, 6,GLfloat); CSAE(GL_FLOAT_MAT2x4, 8,GLfloat); CASE(GL_FLOAT_MAT3, 9,GLfloat); CASE(GL_FLOAT_MAT3x2, 6,GLfloat); CASE(GL_FLOAT_MAT3x4, 12,GLfloat); CASE(GL_FLOAT_MAT4, 16,GLfloat); CASE(GL_FLOAT_MAT4x2, 8,GLfloat); CASE(GL_FLOAT_MAT4x3, 12,GLfloat); #undef CASE default: fprintf(stderr,"Unknown type:0x%x\n",type); exit(EXIT_FAILURE); break; } return size; } void init() { GLuint program; glClearColor(1,0,0,1); ShaderInfo shaders[] = { {GL_VERTEX_SHADER,"uniform.vert"}, {GL_FRAGMENT_SHADER,"uniform.frag"}, {GL_NONE,NULL} }; program = LoadShaders(shaders); glUseProgram(program); /*初始化 uniform块 "Uniforms" 中的变量*/ GLuint uboIndex; GLint uboSize; GLuint ubo; GLvoid *buffer; /*查找“Uniforms”的uniform缓存索引,并判断整个块的大小*/ uboIndex = glGetUniformBlockIndex(program,"Uniforms"); glGetActiveUniformBlockiv(program,uboIndex,GL_UNIFORM_BLOCK_DATA_SIZE,&uboSize); buffer = malloc(uboSize); if(buffer == NULL){ fprintf(stderr,"Unable to allocate buffer\n"); exit(EXIT_FAILURE); } else{ enum {Translation,Scale,Rotation,Enabled,NumUniforms}; /*准备存储在缓存对象中的值*/ GLfloat scale = 0.5; GLfloat translation[] = {0.1,0.1,0.0}; GLfloat rotation[] = {90,0.0,0.0,1.0}; GLboolean enabled = GL_TURE; /*我们可以建立一个变量名称数组,对应块中已知的uniform变量*/ const char* names[NumUniforms] = { "translation", "scale", "rotation", "enabled" }; /*查询对应的属性,以判断向数据缓存中写入数值的位置*/ GLuint indices[NumUniforms]; GLint size[NumUniforms]; GLint offset[NumUniforms]; GLint type[NumUniforms]; glGetUniformIndices(program,NumUniforms,names,indices); glGetActiveUniformsiv(program,NumUniforms,indices,GL_UNIFORM_OFFSET,offset); glGetActiveUniformsiv(program,NumUniforms,indices,GL_UNIFORM_SIZE,size); glGetActiveUniformsiv(program,NumUniforms,indices,GL_UNIFORM_TYPE,type); /*将uniform变量拷贝到缓存中*/ memcpy(buffer + offset[Scale],&scale,size[Scale]*TypeSize(type[Scale])); memcpy(buffer + offset[Translation],&translation,size[Translation]*TypeSize(type[Translation])); memcpy(buffer + offset[Rotation],&rotation,size[Rotation]*TypeSize(type[Rotation])); memcpy(buffer + offset[Enable],&enabled,size[Enabled]*TypeSize(type[Enabled])); /*建立uniform缓存对象,初始化存储内容,并且与着色器程序建立关联*/ glGenBuffers(1,&ubo); glBindBuffer(GL_UNIFORM_BUFFER,ubo); glBufferData(GL_UNIFORM_BUFFER,uboSize,buffer,GL_STATIC_RAW); glBindBufferBase(GL_UNIFORM_BUFFER,uboIndex,ubo); } }
buffer块
GLSL中的buffer块,是着色器的存储缓存对象(shader storage buffer object)。它的功能比uniform块的功能更为强大。
首先,着色器可以写入buffer块,修改其中的内容并呈现给其他的着色器调用或者应用程序本身。
其次,可以在渲染之前再决定它的大小,而不是编译和链接的时候。如:
buffer BufferObject{ //创建一个可读写的buffer块
int mode; //序言(preamble)成员
vec4 points[]; //最后一个成员可以是未定义大小的数组
};
如果在着色器中没有给出,上面的数组大小,那么可以在应用程序中编译和连接之后,渲染之前设置它的大小。着色器中可以通过length()方法获取渲染时的数组大小。
写入操作对着色器存储缓存对象的修改,对于其他着色器调用都是可见的。
设置着色器存储缓存对象的方式与设置uniform缓存的方式类似,不过glBindBuffer()、glBindBufferRange()和glBindBufferBase()需要使用GL_SHARED_STORAGE_BUFFER作为目标参数。
如果不需要写入缓存中,建议使用uniform块,因为硬件设备可能没有足够的资源空间来支持buffer块,但uniform块通常是足够的。buffer块只可以使用std430布局,而uniform块可以选择std140或者std430布局。
in/out块、位置和分量
-
in/out块
着色器变量从一个阶段输出,然后再输入到下一个阶段中,这一过程可以使用块接口来表示。
例如:
//一个顶点着色器的输出可能为: out Lighting{ vec3 normal; vec2 bumpCoord; }; //与片元着色器的输入匹配 in Lighting{ vec3 normal; vec2 bumpCoord; }; -
位置
layout(location = N)被用于每个独立的输入和输出变量,它也可以作用于输入和输出块的成员,显式地设置它们的位置:
#version 440 in Lighting{ layout(location=1) vec3 normal; layout(location=2) vec2 bumpCoord; };无论location位置信息是否在块中,都是可以等价于一个vec4的。
-
分量
如果用户希望把多个小的对象设置到同一个位置上,那么也可以使用分量(component)关键字:
#version 440 in Lighting{ layout(location=1,component=0) vec2 offset; layout(location=2,component=2) vec2 bumpCoord; };与其声明一个vec4 combined,然后使用combined.xy和combined.zw来模拟offset和bumpCoord,这个方法显然要好很多。它在块的外部也是可以使用的。
着色器的编译
对于每一个着色器程序,我们都需要再应用程序中,通过下面的步骤进行设置。
对于每个着色器对象:
- 创建一个着色器对象。
- 将着色器源代码编译为对象。
- 验证着色器的编译是否成功。
然后将多个着色器对象链接为一个着色器程序,包括:
- 创建一个着色器程序。
- 将着色器对象关联到着色器程序
- 链接着色器程序
- 判断着色器的链接过程是否成功完成
- 使用着色器来处理顶点和片元
-
创建着色器对象
为什么要创建多个着色器对象?创建的通用函数可以在多个着色器中得到复用。因此不需要使用大量的通用代码来编译大量的着色器资源,只需要将合适的着色器对象链接为一个着色器程序即可。
调用
glCreateShader()来创建着色器对象。GLuint glCreateShader(GLenum type);说明:分配一个着色器对象
参数:type必须是GL_VERTEX_SHADER、GL_FRAGMENT_SHADER、GL_TESS_CONTROL_SHADER、GL_TESS_EVALUATION_SHADER、GL_GEOMETRY_SHADER或GL_COMPUTE_SHADER中的一个。
返回值:可能是一个非零的整数值,如果为0则说明发生了错误。
-
关联源代码
创建了着色器对象之后,就可以将着色器的源代码关联到对象。这一步需调用
glShaderSource()函数。void glShaderSource(GLuint shader,GLsizei count,const GLchar** string,const GLint* length);说明:将着色器源代码关联到一个着色器对象shader上。
参数:
string:是一个由count行GLchar类型的字符串组成的数组,用来表示着色器的源代码数据。string中的字符串可以是NULL结尾的,也可以不是。
length:可以是以下三种值,若是NULL,代表string给出的每行字符串都是NULL结尾的;否则,length中必须有count个元素,分别表示string中对应行的长度;数组中的某个值是一个整数,它表示对应的字符串中的字符数,如果某个值是负数,对应行假设为NULL结尾。
-
编译着色器对象的源代码
void glCompileShader(GLuint shader);编译着色器的源代码。结果查询可以调用
glGetShaderiv(),并且参数为GL_COMPILE_STATUS。这里与C语言程序的编译类似,需要自己判断编译过程是否正确地完成。
调用
glGetShaderiv()并且参数为GL_COMPILE_STATUS,返回的就是编译过程的状态:返回GL_TRUE,那么编译成功,下一步可将对象链接到一个着色器程序中。
编译失败,可以通过调取编译日志来判断错误的原因。
-
获取编译日志
glGetShaderInfoLog()函数会返回一个与具体实现相关的信息,用于描述编译时的错误。错误日志的大小可以通过调用glGetShaderiv()(带参数GL_INFO_LOG_LENGTH)来查询。void glGetShaderInfoLog(GLuint shader,GLsizei bufSize,GLsizei* length,char* infoLog);说明:返回shader的最后编译状态信息。
参数:
infoLog:返回一个以NULL结尾的字符串的日志信息。
length:返回的字符串长度;若设置为NULL,则不会返回infoLog的大小。
bufSize:定义日志可以返回的最大值。
-
创建程序
当创建并编译了所有必要的着色器对象之后,下一步链接它们以创建一个可执行的着色器程序。
GLuint glCreateProgram(void);说明:创建一个空的着色器程序。
返回值:一个非零的整数,如果为0则说明发生了错误。
-
程序关联着色器对象
得到着色器程序之后,下一步将它关联到必要的着色器对象上,以创建可执行的程序。
void glAttachShader(GLuint program,GLuint shader);将着色器对象shader关联到着色器程序program上。着色器对象可以在任何时候关联到着色器程序,但是它的功能只有经过程序的成功链接之后才是可用的。着色器对象可以同时关联到多个不同的着色器程序上。
-
程序移除着色器对象
如果需要从程序中移除一个着色器对象,从而改变着色器的操作,可以调用
glDetachShader()函数,设置对应的着色器对象标识来解除对象的关联。void glDetachShader(GLuint program,GLuint shader);移除着色器对象shader与着色器程序program的关联。如果着色器已经被标记为要删除的对象(调用glDeleteShader()),然后又被解除了关联,那么它将被即时删除。
-
链接程序
当所有着色器对象关联到着色器程序之后,就可以链接对象来生成可执行程序了。
void glLinkProgram(GLuint program);说明:处理所有与program关联的着色器对象来生成一个完整的着色器程序。
链接操作的结果可以调用glGetProgramiv(),且参数为GL_LINK_STATUS。如果返回GL_TRUE,那么链接成功;否则,返回GL_FALSE。
-
查询链接日志信息
由于着色器对象可能存在问题,因此链接过程依然可能会失败。可以调用glGetProgramiv()(带参数GL_LINK_STATUS)来查询链接操作的结果。
返回GL_TRUE,则链接操作成功,可以指定着色器程序来处理顶点和片元数据。
返回GL_FLASE,则链接失败,可以通过调用glGetProgramInfoLog()函数,获取程序链接的日志信息并判断错误原因。
void glGetProgramInfoLog(GLuint program,GLsizei bufSize,GLsizei* length,char* infoLog);说明:返回program的最后链接日志信息。
参数:
infoLog:返回一个以NULL结尾的字符串的日志信息。
length:返回的字符串长度;若设置为NULL,则不会返回infoLog的大小。
bufSize:定义日志可以返回的最大值。
-
设置当前使用程序
void glUseProgram(GLuint program);使用链接过的着色器程序program。
如果program为零,那么所有当前使用的着色器都会被清除。如果没有绑定任何着色器,那么OpenGL的操作结果是未定义的,但是不会产生错误。
如果已经启用了一个程序,而它需要关联新的着色器对象,或者解除之前关联的对象,则需重新链接。
链接成功,新的程序会直接替代之前启用的程序。链接失败,当前绑定的着色器程序依然可用,不会被替代,直到成功的重新链接,或者使用glUseProgram()指定了新的程序为止。
-
删除着色器对象
void glDeleteShader(GLuint shader);删除着色器对象shader。如果shader当前已经链接到一个或者多个激活的着色器程序上,那么它将被标识为"可删除",当对应的着色器程序不再使用的时候,就会自动删除这个对象。
-
删除着色器程序
void glDeleteProgram(GLuint program);立即删除一个当前没有在任何环境中使用的着色器程序program,如果程序正在被某个环境使用,那么等到它空闲时再删除。
-
检查对象或程序的是否存在
GLboolean gllsShader(GLuint shader);如果shader是一个通过glCreaterShader()生成的着色器对象的名称,并且没有被删除,那么返回GL_TRUE。如果shader是零或者不是着色器对象名称的非零值,则返回GL_FALSE。
GLboolean gllsProgram(GLuint program);如果program是一个通过glCreateProgram()生成的程序对象的名称,并且没有被删除,那么返回GL_TRUE。如果program是0或者不是着色器程序名称的非零值,则返回GL_FALSE。
着色器子程序
GLSL允许在着色器中定义函数,但这些函数的调用过程是静态的。如果需要动态地调用不同的函数,可以创建两个不同的着色器,或者用if语句来进行运行时的选择。
#version 330 core
void func_1(){}
void func_2(){}
uniform int func;
void main()
{
if(func == 1)
func_1();
else
func_2();
}
在着色器中,可预先声明一个可用子程序的集合,然后动态地指定子程序的类型。然后,通过设置一个子程序的uniform变量,从预设的子程序中选择一个并加以执行。
子程序设置
需要三步来设置一个子程序池:
-
通过关键字subroutine 来定义子程序的类型:
subroutine returnType subroutineType(type param, ...);returnType可以是任何类型的函数返回值,而subroutineType是一个合法的子程序名称。
由于它相当于函数的原型,因此我们只需要给出参数的类型,不一定给出参数的名称。
-
使用subroutine定义子程序集合内容
使用刚才定义的subroutineType,通过subroutine关键字来定义这个子程序集合的内容。
某个子程序函数原型如下:
subroutine (subroutineType) returnType functionName(...); -
选择子程序变量
指定一个子程序uniform变量,其中保存了相当于"函数指针"的子程序选择信息,以在应用程序中更改:
subroutine uniform subroutineType variableName;
例如,实现环境光照和漫反射光照方式的动态选择
subroutine vec4 LightFunc(vec3); //第1步
subroutine (LightFunc) vec4 ambient(vec3 n) //第2步
{
return Materials.ambient;
}
subroutine (LightFunc) vec4 diffuse(vec3 n) //第2步(重复)
{
return Materials.diffuse *
max(dot(normalize(n),LightVec.xyz),0.0);
}
subroutine uniform LightFunc materialShader; //第3步
也可以定义多种类型的子程序,设置一个子程序属于多个类型。
subroutine void Type_1();
subroutine void Type_2();
subroutine void Type_3();
subroutine (Type_1,Type_2) Func_1();
subroutine (Type_1,Type_2) Func_2();
subrountine uniform Type_1 func_1;
subrountine uniform Type_2 func_2;
subrountine uniform Type_3 func_3;
选择子程序
以下例子演示顶点着色器的调用过程
GLint materialShaderLoc;
GLuint ambientIndex;
GLuint diffuseIndex;
glUseProgram(program);
materialShaderLoc = glGetSubroutineUniformLocation(program,GL_VERTEX_SHADER,"materialShader");
if(materialShaderLoc < 0){
// 错误:materialShader 不是着色器中启用的子程序 uniform
// uniform in the shader
}
ambientIndex = glGetSubroutineIndex(program,GL_VERTEX_SHADER,"ambient");
diffuseIndex = glGetSubroutineIndex(program,GL_VERTEX_SHADER,"diffuse");
if(ambientIndex == GL_INVALID_INDEX ||
diffuseIndex == GL_INVALID_INDEX){
// 错误:指定的子程序在GL_VERTEX_SHADER阶段当前绑定的程序中没有启用
}
else{
GLsizei n;
glGetIntegerv(GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS,&n);
GLuint *indices = new GLuint[n];
indices[materialShaderLoc] = ambientIndex;
glUniformSubroutinesuiv(GL_VERTEX_SHADER,n,indices);
delete[] indices;
}
-
获取子程序的uniform的位置
GLint glGetSubroutineUniformLocation(GLuint program,GLenum shadertype,const char* name);说明:返回名为name的子程序uniform的位置,相应的着色阶段通过shadertype来指定。
参数:
name:是一个以NULL为结尾的字符串;
shadertype:值必须是GL_VERTEX_SHADER、GL_TESS_CONTROL_SHADER、GL_TESS_EVALUATION_SHADER、GL_GEOMETRY_SHADER或者GL_FRAGMENT_SHADER中的一个。
返回值:
如果name不是一个激活的子程序,则返回-1。如果program不是一个可用的着色器程序,那么会生成一个GL_INVALID_OPERATION错误。
-
获取子程序在着色器中的索引号
GLuint glGetSubroutineIndex(GLuint program,GLenum shadertype,const char* name);说明:从程序program中返回name所对应的着色器函数的索引,相应的着色器阶段通过shadertype来指定。
参数:
name:是一个以NULL结尾的字符串;
shadertype: 值必须是GL_VERTEX_SHADER、GL_TESS_CONTROL_SHADER、GL_TESS_EVALUATION_SHADER、GL_GEOMETRY_SHADER或者GL_FRAGMENT_SHADER中的一个。
返回值:如果name不是shadertype着色器的一个活动子程序,那么会返回GGL_INVALID_INDEX。
-
指定着色器执行哪一个子程序函数
GLuint glUniformSubroutinesuiv(GLenum shadertype,GLsizei count,const GLuint* indices);说明:设置shadertype指定的着色器阶段,所有count个着色器子程序uniform使用indices数组中的值。
参数:
shadertype:值必须是GL_VERTEX_SHADER、GL_TESS_CONTROL_SHADER、GL_TESS_EVALUATION_SHADER、GL_GEOMETERY_SHADER或者GL_FRAGMENT__SHADER中的一个。
indices:对应每个子程序uniform的设置值。
返回值:
如果count不等于当前绑定程序的着色器阶段shadertype的GL_ACTIVE_SUBROUTINE_UNIFORM_LOCATOINS值,那么会产生一个GL_INVALID_VALUE错误;
indices中的所有值都必须小于GL_ACTIVE_SUBROUTINES,都则会产生一个GL_INVALID_VALUE错误。
独立的着色器对象
在OepnGL 4.1版本之前(不考虑扩展功能),在应用程序中,同一时间只能绑定一个着色器程序。如果程序需要多个片元着色器处理来自同一个顶点着色器的几何体变换数据,那么会变得很不方便。此时只能将同一个顶点着色器复制多份,并且多次绑定到不同的着色器程序,从而造成资源的浪费。
独立的着色器对象,可以将不同程序的着色阶段(例如顶点着色)合并到同一个程序管线中。
-
创建用于着色器管线的着色器程序。调用
glProgramParameteri()函数并设置参数为GL_PROGRAM_SEPARABLE,然后再链接着色器程序。这样该程序被标识为在程序管线中使用。还可以直接使用新增的
glCreateShaderProgramv()来封装着色器编译过程,并且将程序标识为可共享,然后链接最终对象。 -
将着色器程序集合合并之后,就需要用这个新的着色器管线结构来合并多个程序中的着色阶段。着色器管线的创建可以调用
glCreateProgramPipelines(),即创建一个未使用的程序管线标识符;然后将它传入
glBindProgramPipeline(),使得该程序可以自由编辑(例如,添加或者替换着色阶段)和使用;通过
glDeleteProgramPipelines()来删除程序管线对象。 -
当绑定了一个程序管线之后,调用
glUseProgramStages()将之前标记为独立的程序对象关联到管线上,它通过位域的方式来描述该管线处理几何体和着色器片源时,给定着色器程序所处的着色阶段。 -
为了确保管线可以使用,着色器阶段之间的接口——in和out变量——必须是匹配的。使用独立程序对象的着色器管线,不能在链接时检查接口匹配情况,只能在绘制—调用过程中进行检查。接口没有正确匹配,那么所有的可变变量(out变量)都未定义。
-
内置的gl_PerVertex块必须重新声明,以便显式地制定固定管线接口中的哪些部分可以使用。如果管线用到了多个程序,这一步是必须的。
out gl_PerVertex{ vec4 gl_Position; //设置gl_Position在接口中可用 float gl_PointSize; //设置gl_PointSize在接口中可用 }; //不再使用gl_PerVertex的其他成员如果不同的着色器程序都用到了同一个内置的块接口,那么所有的着色器都必须使用相同的方式重新声明这个内置块。
因为独立的着色器对象,可以有各自独立的程序uniform集合,所以有两种方法设置uniform变量的值。
448

被折叠的 条评论
为什么被折叠?



