OpenGL Shader Language,OpenGL着色器语言,是在C语言基础上设计的语言,它和C语言是兼容的。由GPU编译并运行,包含一些针对向量和矩阵操作的有用特性。
一个典型的着色器程序如下:
// 支持C语言形式的注释
#version 330 core // 开头必须声明版本,和OpenGL版本号相同。
in type in_var; // 输入变量
// 如果是顶点着色器,这也叫作顶点属性(Vertex Attribute),数量有上限
// OpenGL规定至少是16个4分量。
// GL_MAX_VERTEX_ATTRIBS表示支持的最大属性数量。
out type out_var; // 输出变量,供下一阶段使用。
uniform type uniform_var; // uniform常量
void main() // 一个主体main函数,void,空参数,不需要返回值。
{
...... // 处理输入数据,进行一些图形操作
out_var = ... // 生成一些输出数据,下一阶段的着色器程序将使用它。
}
一、变量
和C一样,强类型语言,变量必须先声明,并且给出类型。
命名规范和C一样(字母、数字、下划线),但是数字下划线不能在第一位,不能有连续的下划线(GLSL保留)。
1、基本数据类型
基本数据类型 | 描述 |
---|---|
bool | 布尔值 |
int | 有符号二进制补码32位整型 |
uint | 无符号32位整型 |
float | 32位浮点型 |
double | 64位浮点型 |
声明时,必须初始化。
int i, numParticles = 1500;
uint ui1, ui2 = 1500u, 1500U; // 无符号整型
float force, g = −9.8;
float shit = 12345f; // 单精度浮点型后缀,f、F都可以。
bool falling = true; // 布尔变量:false、true
double pi = 3.1415926LF; // 双精度浮点型后缀,lf、LF都可以。
double fuck = 3E-7; // 浮点型:科学计数法
转换构造函数。在类型转换上,GLSL比C更严格,只支持以下几种隐式转换,其他情况都必须是显式转换。
GLSL支持的隐式转换 | |
---|---|
类型 | 被隐式转换类型 |
uint | int |
float | int、uint |
double | int、uint、float |
int i = 1;
float f = i; // int -> float的隐式转换
int fuck = false; // 编译错误,不支持的隐式转换
float f = 10.0;
int ten = int(f); // int类型同名的转换构造函数,uint、float、double、bool都有同名的转换构造函数
// 这些构造函数支持重载(GLSL特性),接受不同其他类型参数。
// 向量也有同名的转换构造函数。
2、聚合类型_向量
向量是一个可以包含1、2、3、4个分量的容器,分量类型可以是所有基本数据类型。
GLSL支持的向量 | |||
---|---|---|---|
基本类型 | 2D向量 | 3D向量 | 4D向量 |
float | vec2 | vec3 | vec4 |
double | dvec2 | dvec3 | dvec4 |
int | ivec2 | ivec3 | ivec4 |
uint | uvec2 | uvec3 | uvec4 |
bool | bvec2 | bvec3 | bvec4 |
// ************************************************************************
// ************** 向 量 类 型
// ************************************************************************
vecn v; // v是一个包含n个float分量的默认向量,大部分情况都用这个。
bvecn v; // v是一个包含n个bool分量的向量
ivecn v; // v是一个包含n个int分量的向量
uvecn v; // v是一个包含n个unsigned int分量的向量
dvecn v; // v是一个包含n个double分量的向量
// 所有向量都有与类型同名的转换构造函数
vec3 velocity = vec3(0.0, 2.0, 3.0);
ivec3 steps = ivec3(velocity); // vec3 -> ivec3的转换构造函数。
// 截短/加长一个向量
vec4 color;
vec3 RGB = vec3(color); // 截短:vec4向量截短成vec3向量。
vec3 white = vec3(1.0); // 加长:while = (1.0, 1.0, 1.0)
vec4 translucent = vec4(white, 0.5); // 加长
// ************************************************************************
// ************** 向 量 访 问
// ************************************************************************
// 位置向量的分量访问符: x y z w
// 颜色向量的分量访问符: r g b a
// 纹理坐标向量的分量访问符: s t p q
// 这只是约定俗成的规则,当然你也可以用rgba去访问位置向量的分量。
float red = color.r; // 等价
float red = color[0]; // 等价
float v_y = velocity.g; // 等价
float v_y = velocity.y; // 等价
float v_y = velocity.t; // 等价
float v_y = velocity[1]; // 等价
// ************************************************************************
// ************** 向 量 重 组(Swizzling)
// ************************************************************************
vec2 vec_2;
vec4 vec_4 = vec_2.xyxx;
vec3 vec_4_1 = vec_4.zyw;
vec4 vec_4_1 = vec_2.xxxx + vec_4_1.yxzy;
vec3 luminance = color.rrr; // 红色分量设置亮度值luminance
color = color.abgr; // 调换color的分量
vec4 color = otherColor.rgz; // 错误,z不是color的访问符
vec2 pos;
float zPos = pos.z // 错误:pos没有z分量
vec2 vec_2 = vec2(0.5, 0.7);
vec4 vec_4 = vec4(vec_2, 0.0, 0.0);
vec4 vec_4_1 = vec4(vec_4.xyz, 1.0);
int count = vec_4_1.length(); // 4个分量
3、聚合类型_矩阵
只支持float、double类型的矩阵。
GLSL支持的向量和矩阵 | |
---|---|
基本类型 | 矩阵类型 |
float | mat2、mat3、mat4 |
mat2x2、mat2x3、mat2x4 | |
mat3x2、mat3x3、mat3x4 | |
mat4x2、mat4x3、mat4x4 | |
double | dmat2、dmat3、dmat4 |
dmat2x2、dmat2x3、dmat2x4 | |
dmat3x2、dmat3x3、dmat3x4 | |
dmat4x2、dmat4x3、dmat4x4 | |
int | |
uint | |
bool |
// ************************************************************************
// ************** 矩 阵 初 始 化
// ************************************************************************
// 可通过标量、向量来初始化矩阵,或者两者混合初始化矩阵,只要数量足够即可,
// 但是GLSL的矩阵是遵循列主序的原则,先填充列,再填充行的顺序,这和C的二维数组初始化是相反的。
mat3 m3 = mat3(4.0) = { 4.0, 0.0, 0.0, // 第一列
0.0, 4.0, 0.0, // 第二列
0.0, 0.0, 4.0 // 第三列
}
// (标量)初始化矩阵M
mat3 M = mat3( 1.0, 2.0, 3.0, // 第一列
4.0, 5.0, 6.0, // 第二列
7.0, 8.0, 9.0 ); // 第三列
// (向量)初始化矩阵M
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 );
// (标量+向量混合)初始化矩阵M
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 ); // 第三列
// M的最终结果是:
// 1.0, 4.0, 7.0
// 2.0, 5.0, 8.0
// 3.0, 6.0, 9.0
// ************************************************************************
// ************** 矩 阵 访 问
// ************************************************************************
mat4 m = mat4(2.0); // 对角矩阵
mat4 m = mat4(1.0); // 单位矩阵(对角为1)
vec4 zVec = m[2]; // 获取矩阵的第3列
float yScale = m[1][1]; // m[1].y,第2列第二个元素。
mat3x4 m; // 3列4行矩阵
int columnNum = m.length(); // m包含的列数为3
int rowNum = m[0].length(); // 第0个列向量中分量的个数为4
4、聚合类型_结构体
struct Particle {
// Particle(float, vec3, vec3),隐式定义了这个构造函数
float lifetime;
vec3 position;
vec3 velocity;
};
Particle p = Particle(10.0, pos, vel); // pos, vel均为vec3类型
// p.lifetime,.号访问运算符,访问成员
// p.position
// p.velocity
5、聚合类型_数组
支持任意数据类型的数组,如结构体数组、数组的数组(GLSL 4.3)。
用法与C的数组相同,负数形式的索引和超出范围的索引值是不允许的。
数组属于第一类型值(first-class),有自己的构造函数。
float coeff[3]; // C形式定义数组
float[3] coeff; // 同上
int indices[]; // 未定义维数,可以稍后重新声明它的维数
float coeff[3] = float[3](2.38, 3.14, 42.0); // 第一类型值,和基本数据类型一样看待
float coeff[3] = float[](2.38, 3.14, 42.0); // 同上
// ****************** 多维数组(数组的数组),与C类似
float coeff[3][5]; // 大小为3的数组,每个数组是大小为5的数组。
coeff[2][1] *= 2.0; // 内层索引设置为1,外层设置为2
coeff.length(); // 返回3
coeff[2]; // 大小为5的数组(float[5])
coeff[2].length(); // 返回5
// ****************** 向量、矩阵、数组都有length方法
// 所有向量、矩阵、大部分数组的length方法返回值是编译时常量。
// 少部分数组length值在链接前(link)都是未知的,
// 比如如果使用链接器来减少同一阶段多个着色器的大小,对于着色器中保存的缓存对象,
// length值直到渲染时才可能得到。
for (int i = 0; i < coeff.length(); ++i) { // length是所有数组默认有的方法,返回元素个数
coeff[i] *= 2.0;
}
mat4 m;
float diagonal[m.length()]; // 设置数组的大小与矩阵大小相等,length返回值是编译时常量,
// 长度是编译时已知,
float x[gl_in.length()]; // 设置数组的大小与几何着色器的输入顶点数相等
6、作用域
和C++一样,
函数之外声明,有全局作用域。
在大括号、函数定义块、循环、if语句内声明,作用域在这些块内。
循环迭代变量,只能在循环体内。
二、存储限制符
数据类型还可添加修饰符来改变行为。
- none
- 默认的,可读可写,函数的输入参数既是这种类型
- const
- 与C类似,变量只读,初始化值是编译时常量,则该变量是编译时常量。
- 声明变量或函数的参数为只读类型。
- attribute
- 全局只读,只能在顶点着色器中使用。
- 只能修饰浮点、向量、矩阵变量。
- 一般attribute变量用来放置程序传递来的模型顶点,法线,颜色,纹理等数据它可以访问数据缓冲区。
- in
- out
- uniform
- application(CPU)传递数据给shader(GPU)的方式。
- varying
- 主要负责在vertex 和 fragment 之间传递变量。
- buffer
- shared
const、attribute、uniform、varying是全局限定符,一次只能使用其中一个。
in\out
虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。
顶点着色器
顶点着色器的输入特殊在,它从顶点数据中接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。
//---------------------------顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
void main()
{
gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}
片段着色器
变量为当前着色器的输入变量,可是最开始的顶点属性,也可以是上一个着色阶段的输出变量。
//---------------------------片段着色器
#version 330 core
out vec4 FragColor; // 输出一个颜色
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
void main()
{
FragColor = vertexColor;
}
out
变量为当前着色器阶段的输出变量
当前着色器说:这个变量的数据是我处理好的数据,我把它发出去(out)给下一个人处理。
uniform
从CPU中的应用向GPU中的着色器发送数据的方式,这也就意味着比通过顶点缓冲(VBO)数传数据要慢得多。
对于所有着色器,它的uniform变量的值不会发生改变,而且也不能修改,也就当做是常量。
在所有着色器阶段共享的,必须定义为全局变量。
任意类型(向量、矩阵)都可以uniform修饰。
// -------------------------------片元着色器
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在应用程序代码中设定了这个变量
// 无需通过顶点着色器传入给片元着色器
void main()
{
FragColor = ourColor;
}
// ------------------------------- 在应用程序中(C++)通过opengl指令设置这个uniform变量
float timeValue = glfwGetTime(); // 获取运行秒数
float greenValue = (sin(timeValue) / 2.0f) + 0.5f; // 颜色值在[0.1,1.0]之间改变。
// 找到属性的位置值
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
// 更新之前uniform值之前,必须调用shaderProgram
// 确保是设置shaderProgram中的uniform变量。
glUseProgram(shaderProgram);
// 设置值4f,表示接受4个float分量的数组/向量。
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
varying
主要负责在vertex 和 fragment 之间传递变量。
// ************************************************************
// ************** 顶 点 着 色 器
// ************************************************************
varying vec4 v_Color;
void main(){
...
v_Color = vec4(1.,1.,1.,1); // 在顶点着色器中修改值
}
// ************************************************************
// ************** 片 段 着 色 器
// ************************************************************
varying vec4 v_Color;
void main() {
gl_FragColor = v_Color; // 在片段着色器中使用值,注意不能修改否则出错。
}
...
buffer
在应用程序中共享一块可读写内存给着色器,作为着色器的存储缓存(storage buffer)。
与uniform类似,但是在着色器中可读可写。
shared
变量是本地工作组(local work group)中共享的,只能用于计算着色器。
// 在着色器中
uniform vec4 BaseColor; // 在链接阶段,创建一个uniform变量列表,客户端可以通过索引获取uniform变量并设置值。
// 在客户端应用程序中
// glGetUniformLocation(GLuint program, const char* uniformname);
GLint timeLoc;
GLfloat timeValue;
timeLoc = glGetUniformLocation(program, "time");
glUniform1f(timeLoc, timeValue);
三、语句
操作符优先级
操作符重载
大部分操作符都经过重载,重载的操作符支持不同类型之间的数据操作,比如矩阵与向量的乘法。
// **********向量 x 矩阵
// 本质还是矩阵x矩阵,比如k维向量x(mxn)矩阵,相当于(1xk)矩阵x(mxn)矩阵,
// 根据矩阵乘法特性,前矩阵的列数=后矩阵的行数,所以k = m
// 综上,k维向量 x (mxk)矩阵才意义
vec3 v3;
mat3 m3;
vec3 result = v * m; // 矩阵和向量的维度必须匹配。
// 向量 x 向量
vec2 a, b, c
c = a * b; // c = (a.x*b.x, a.y*b.y)
// 矩阵1 x 矩阵1
// 矩阵1每行元素 x 矩阵2每列元素 = 新矩阵的每行元素
// 总结:“行” x “列” = “行”
mat2 m, u, v;
m = u * v; // m = (u00*v00 + u01*v10 u00*v01 + u01*v11
// u10*v00 + u11*v10 u10*v01 + u11*v11)
// u = (u00 u01 v = (v00 v01
// u10 u11) v10 v11)
条件控制
if(true){ //条件为true的分支
......
}
else{ //else 分支可选
......
}
switch(int_value){ // 从GLSL1.30开始与C的switch类似。
case n:
break; // break
case m: // case m分支没有break,也会走到case k分支,和C相同意义。
case k:
break;
default:
break;
}
循环控制
for(int i = 0; i < 10; ++i){ // for循环
......
break; // 与C含义相同:终止当前循环,继续外层循环
continue; // 与C含义相同:终止当前迭代,立即进行下一个迭代。
return; // 与C含义相同:从当前子例程返回,返回值类型必须与声明匹配。
discard; // 片元着色器特有:丢弃当前片元,不会再执行后续的管线。
}
while(condition){ // while循环
......
}
do{ // do while 循环
......
}while(condition)
四、函数
支持自定义函数,也有一些内置函数,可在单个着色器中定义,在多个着色器中复用。
函数必须先声明后调用,这和C一样。
不能嵌套,不能递归调用。
函数声明
// **************** 函数声明(大致和C相同)
returnType funcName( // 函数名称不能是连续下划线、gl_开头。
(访问)修饰符 类型 var1, // 每个参数必须有修饰符,这是和C不同的地方
(访问)修饰符 类型 var2, // 类型可以是任何类型(数组必须显式指定大小)
...... )
{
// 函数体
......;
return returnValue; // returnType是void,可以不用return
// returnValue可以是内置类型、自定义类型、数组类型。
// 返回数组类型,必须显式指定大小。
}
参数访问修饰符
GLSL没有C中指针、引用的概念,所以涉及到函数的参数传递问题(值传递、引用传递),为了解决这个问题,GLSL规定参数类型前还必须加访问修饰符,具体类型如下:
- in(值传递)
- 叫输入变量(参数)
- 数据拷贝到函数中,默认修饰符。
- 函数内修改这个输入变量,只在函数内有效,函数外无效,因为是副本。
- const in
- 类似C的const声明,数据不可修改。
- out(非in)
- 叫输出变量(参数)
- 使用场景:这个参数是从函数传递给外部使用,相当于外部传入了一个数据载体。
- 这个参数的初始值一般没什么用,很可能是未定义,不是in型。
- inout(又in又out)
- 又是in型,又是out型
- 将数据拷贝至函数中,并且返回函数中修改的数据。
可以通过声明成const in来验证某个输入变量(参数)是否被修改。
计算不变性(重复性)
GLSL无法保证在不同的着色器中,两个完全相同的计算式会得到完全一样的结果。这一情形与CPU端应用程序进行计算时的问题相同,即不同的优化方式可能会导致结果非常细微的差异。这些细微的差异对于多通道的算法会产生问题,因为各个着色器阶段可能需要计算得到完全一致的结果。GLSL通过invarant或者precise关键字来确保着色器之间的计算不变性。
invariant限制符
可以修饰任何着色器的输出变量(包括内置的)。它可以确保两个着色器的输出变量的表达式相同,变量值也相同,则结果也相同。
invariant gl_Position; // 修饰内建输出变量
invariant out vec3 Color; // 修饰自定义的输出变量
// 在调试过程中,可能需要将着色器中的所有可变量都设置为invariant。可以通过顶点着色器预编译命令pragma来完成这项工作。
// 但是会影响编译优化,影响性能。
#pragma STDGL invariant(all)
precise限制符
可以修饰任何变量(包括内置的)或函数返回值。注意它的作用不是增加精度而是增加计算的可复用性,比如调用变量值位置,结果不变。precise可以在变量前的任意位置设置这个变量。并且可以修改之前已经声明过的变量。
precise gl_Position; // 修饰内建变量
precise out vec3 Location; // 修饰自定义变量
precise vec3 subdivide (vec3 Pl, vec3 P2){} // 修饰返回值‘
通用在细分着色器中用来避免造成几何形状的裂缝(细分裂缝问题)。
注意
总体上说,如果必须保证某个表达式即使其中的变量值发生变化,只要数学意义上值不变则实际计算结果也不变(比如交换位置)。应该选择precise而非invariant。
需要注意,这两种方法都需要在图形设备上完成计算过程,来确保同一表达式的结果可以保证重复性(不变性)。对于宿主计算机和图形硬件各自的计算,这两种方法都无法保证结果是完全一致的。
着色器编译时的常量表达式是由编译器的宿主计算机计算的,因此我们无法保证宿主机计算的结果与图形硬件计算的结果完全相同。
uniform float ten; // 在应用程序中设置这个值为10.0
const float f = sin(10.0); // 常量表达式由编译器的宿主计算机完成计算
float g = sin(ten); // 图形硬件负责计算
void main(){
if(f == g) ; // 不一定相等。
}
五、预处理
作用与C的预处理类似,但是没有C的#include命令。GLSL支持的预处理命令如下:
预处理命令
// **********************************************************
// ******************* 宏 定 义
// **********************************************************
// 与C类似:控制常量、宏的定义。
#define // 定义宏
#undef // 取消宏,对内置宏无效
// **********************************************************
// ******************* 预 定 义 宏
// **********************************************************
__LINE__ // 行号,#line可以修改,默认是已经处理的换行符数+1
__FILE__ // 当前处理的源字符串编号
__VERSION__ // GLSL版本(整数表示形式)如100 = v1.00
GL_ES // 如果是OpenGL ES环境,则置为1
GL_FRAGMENT_PRECISION_HIGH // 如果当前片段着色器支持高精度浮点,则置为1,用于检查着色器精度
#ifdef GL_ES //
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float; // // 如果系统支持高精度,则设置高精度浮点
#else
precision mediump float;
#endif
#endif
// **********************************************************
// ******************* 条 件 编 译
// **********************************************************
#if condi // condi条件值只能是整数表达式、#define定义的值。
#ifdef
#ifndef
#lese
#elif
#endif
// **********************************************************
// ******************* 其 他 指 令
// **********************************************************
#error text // 强制编译器将text文字内容插入到着色器的信息日志当中,遇到换行符结束。
#pragma options // 控制编译器的特定选项,编译时设置一些额外属性。
#extension options // 设置编译器支持特定GLSL扩展功能
#version number // 设置当前使用的GLSL版本名称
#line options // 设置诊断行号
预处理示例_宏定义
#define FUCKER 10 // 正确。
#define func(n) gl_LightSource[(n)].position
#define FUCKER "YOU" // 错误,不支持字符串替换。
预处理示例_条件编译
#ifdef NUM_ELEMENTS
......
#endif
#if defined(NUM_ELEMENTS) && NUM_ELEMENTS > 3
......
#elif NUM_ELEMENTS < 7
......
#endif
预处理示例_编译选项控制
// 编译器优化选项:启用/禁用着色器的优化,默认是开启优化。
#pragma optimize(on) // 开启优化,命令应该在函数体之外
...... // 代码块,
#pragma optimize(off) // 关闭优化
// 编译器调试选项:启用/禁用着色器的额外诊断信息输出。
#pragma debug(on)
......
#pragma debug(off)
#pragma STDGL // 用于启用所有输出变量值的不变性检查。
#pragma STDGL invariant(all) // 将着色器中所有变量设置为invariant,一般用于调试,性能会降低。
预处理示例_着色器扩展控制
#extension extension_name : <directive> // extension_name = glGetString(GL_EXTENSIONS)
#extension all : <directive> // 影响所有扩展的行为
// <directive>可用选项如下:
// require: 扩展是必需的,如果无法支持给定的扩展功能,或设置为all,则报错
// enable: 如果无法支持给定的扩展功能,则给出警告;如果设置为all,则提示错误。
// warn: 如果无法支持给定的扩展功能,或编译中使用了任何扩展,则给出警告。
// disable: 禁止支持给定的扩展,设置为all则禁止所有扩展,代码中使用到扩展,则提示警告或者错误。
六、数据块
uniform块(待完成)
buffer块(待完成)
in/out块
一个着色器阶段的out块与下一着色阶段的in块相对应, 方便检查两个阶段的数据对接是否正确。GLSL有很多的内置in/out
块,比如gl_PerVertex块,包含了内置变量gl_Position。
out Lighting { // 顶点着色器的输出变量块,out块
vec3 normal;
vec2 bumpCoord;
};
in Lighting{ // 片元着色器的输入变量块,in块,接收来自顶点着色器的数据。
vec3 normal;
vec2 bumpCoord;
}
七、着色器的编译
整体类似C/C++程序的编译过程,只不过glsl的编译器和链接器都是OpenGL API的一部分。
// *******************shader 编译过程伪代码
shader = glCreateShader(FLAG); // 1、创建一个初始shader对象,可以创建多个shader
glShaderSource(shader, srcCodeStr, ...); // 2、载入shader源代码
glCompileShader(shader); // 3、编译shader,生成目标代码
program = glCreateProgram(); // 4、创建program,相当于shader的容器
glAttachShader(program, shader); // 5、program绑定shader,可以绑定多个shader
glLinkProgram(program); // 6、链接所有的shader,打包成一个可执行程序
glUseProgram(program); // 7、运行shader
八、着色器动态选择
静态选择着色器
#version 330 core
void func_1(){......} // 着色器逻辑一
void func_2(){......} // 着色器逻辑二
uniform int funcFlag;
void main(){
if( funcFlag == 1)
func_1();
else
func_2();
}
动态选择着色器
subroutine
子例程、子程序 。可以理解为着色器中的函数。这些函数可以根据函数原型进行分类,即返回类型、参数列表相同的即是同一种函数类型,这个函数类型就是一个subroutine type,子例程类型,这和C++中的std::function类似。
使用模板
// 第一步:定义一种函数原型,相当于函数声明,没有定义。
subroutine returnType subroutineType( type param, ... ); // subroutineType代表了一种函数类型
// returnType: 函数返回值,任意类型。
// subroutineType: 合法的函数名称
// type: 参数类型
// param: 参数名称,不一定需要,这里只是原型。
// 第二步:定义subroutineType类型的一个函数实例
subroutine (subroutineType) returnType functionName(...){ // 返回值、参数列表和subroutineType必须相同
......
}
subroutine uniform subroutineType variableName;
注意
每个着色器函数都可以用多个subroutine type。
// 三种子程序类型
subroutine void Type_1();
subroutine void Type_2();
subroutine void Type_3();
// 一个子程序可以有多个子程序类型。
subroutine (Type_1, Type_2) Func_1();
subroutine (Type_1, Type_3) Func_2();
subroutine uniform Type_1 func_1; // 可以是Func_1、Func_2
subroutine uniform Type_2 func_2; // 可以是Func_1
subroutine uniform Type_3 func_3; // 可以是FUnc_2
代码示例
// **************************** 在着色器中定义着色器的几套逻辑(每个逻辑就是一个子例程subroutine)
subroutine vec4 LightFunc(vec3); // 第一步:定义函数原型LightFunc
subroutine (LightFunc) vec4 ambient(vec3 n) { // 第二步:定义着色器逻辑一,ambient,
return Materials.ambient;
}
subroutine( LightFunc) vec4 diffuse(vec3n) { // 第二步:定义着色器逻辑二:diffuse
return Materials.diffuse*
max(dot(normalize(n), LightVec.xyz),0.0);
}
subroutine uniform LightFunc materialShader; // 第三步,materialShader最终会指向上面其中一个函数
// 会指向哪个,在客户端的逻辑中决定。
// *******************************************************************************
// **************************** 客户端:动态选择着色器逻辑
GLint materialshaderLoc; // 上面materialShader变量的索引。
GLuint ambientIndex; // 着色器逻辑函数的索引
GLuint diffuseIndex; // 着色器逻辑函数的索引
glUseProgram(program); // 会重新设置所有子程序的uniform值,具体顺序与硬件有关。
// 获取索引
materialShaderloc = glGetSubroutineUniformLocation(program, GL_VERTEX_SHADER, "materialShader");
if (materialShaderLoc < 0){ // 返回值检测
//错误: materialshader不是着色器中启用的子程序uniform
}
// 获取着色器逻辑函数(子程序)的索引,注意必须是活动的子程序,否则返回GL_INVALID_INDEX
ambientIndex = glGetSubroutineIndex(program, GL_VERTEX_SHADER, "ambient");
diffuseIndex = glGetSubroutineIndex(program, GL_VERTEX_SHADER, "diffuse");
if (ambientIndex == GL_INVALID_INDEX || diffuseIndex == GL_INVALID_INDEX){
//错误:指定的子程序在GL_ ERTEX SHADER阶段当前绑定的程序中没有启用
}
else { // 成功获取着色器函数ambient、diffuse,且它们已经启用。
GLsizei n;
glGetIntegerv(GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS, &n);
GLuint *indices = new GLuint[n];
indices[materialShaderLoc]= ambientIndex; // 子例程uniform
// 指定着色器中各个子例程uniform变量的函数索引。
// 设置上面materialShader的值,也就是在这里动态选择了着色器逻辑。
glUniformSubroutinesuiv(GL_VERTEX_SHADER, n, indices);
delete [] indices;
}