《OpenGLES|OpenGLES Android篇零基础系列(五):GLSL着色器语言

一.概述
【《OpenGLES|OpenGLES Android篇零基础系列(五):GLSL着色器语言】GLSL(OpenGL着色器语言)在OpenGL2.x以上的可渲染管道编程中,非常重要。在之前固定管线里面的很多方法在OpenGL2.x以上不再存在的原因,就是因为其可移植到GLSL里面,这样可以降低硬件功耗,减少性能开销。
二.基本语法规则

  1. 大小写敏感
  2. 语句末尾 必须 要有分号
  3. 从main函数开始执行
  4. 函数声明中 不能省略返回值类型 (无返回值就是void,C语言可以省略,但这里不行)
  5. 注释语法和C语言一致(单行//,多行/**/)
三.变量和基本数据类型
1.基本数据类型 只支持2种基本数据类型:
  • 数值类型:整数,浮点数
  • 布尔类型:true和false2个布尔常量
    注意 :不支持字符串
2.变量
  • 变量声明
    和C语言一样,类型+变量名,变量命名规则也一样,基本类型只有int、float和bool
  • 类型转换
    没有 隐式类型转换,也 不支持 3f 这样的类型后缀,但提供了类型转换函数:int()、float()和bool(),都只接受其余2种基本数据类型
  • 运算符
    不支持位运算,其它和C语言一致,也支持3目选择,逻辑与(&&)和逻辑或(||)也有短路特性
  • 作用域
    与C语言一致,函数内部声明的是局部变量,外面声明的就是全局变量
四.复杂数据类型
1.矢量(vec) 支持2、3、4维矢量,按分量数据类型分为3类:
vec2、vec3、vec4:分量是浮点数
ivec2、ivec3、ivec4:分量是整数
bvec2、bvec3、bvec4:分量是布尔值
构造函数名 和类型名一致,比如 vec4(1.0) 返回4维向量 [1.0, 1.0, 1.0, 1.0] ,如果像这样只传入一个参数,会把所有分量都赋值为该值,如果传入的参数不止一个但比需要的参数数目少,就会 报错 。例如, vec4(1.0)和vec4(1.0, 1.0, 1.0, 1.0) 都没问题,而 vec4(1.0, 1.0)和vec4(1.0, 1.0, 1.0) 就会报错
此外,还可以传入矢量来构造新矢量,或者用现有矢量组合出新矢量,例如:
vec3 v3 = vec3(0.0, 0.5, 1.0); // [0.0, 0.5, 1.0] vec2 v2 = vec2(v3); // [0.0, 0.5],截取v3的前两个分量 vec4 v4 = vec4(v2, vec4(1.5)); // [0.0, 0.5, 1.5, 1.5],组合v2和新矢量[1.5, 1.5, 1.5, 1.5]

总之,参数可以来自基本值也可以来自其它矢量,但如果参数数量不够且数量不为1就 报错
访问矢量的分量有2种方式,如下:
v4 = vec4(1, 2, 3, 4); // .分量名 v4.x, v4.y, v4.z, v4.w // 齐次坐标 v4.r, v4.g, v4.b, v4.a // 色值 v4.s, v4.t, v4.p, v4.q // 纹理坐标 // []运算符 v4[0], v4[1], v4[2], v4[3]

点号分量名方式只是为了添上语义,等价于方括号运算符,可以理解为别名,例如 v4[0] 的别名是 v4.x 、 v4.r 以及 v4.s 。更有趣的是,还可以组合使用,比如 v4.xz 返回的2维向量,但此时分量名不能混用( v4.sz 是不对的)
注意 :方括号中的值必须是 常量索引值 ,要么是整数字面量,要么是const修饰的变量、循环索引(流程控制部分再解释),或者这3者组成的表达式
2.矩阵(mat) 只支持2、3、4维 方阵 ,只支持浮点数类型分量:mat2、mat3、mat4
特别注意 :矩阵元素是 列主序 的,例如:
mat4 m4 = mat4( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ); // 生成的矩阵是: 1 5 9 13 2 6 10 14 3 7 11 15 4 8 12 16

可能与想象的大不一样,但确实是这样,同样地,矩阵的构造函数也可以接受其它矢量或者矩阵,无论参数来自哪里,最终这组数都将 按列主序 来构造矩阵,例如:
vec2 v2_1 = vec2(1, 2); vec2 v2_2 = vec2(3, 4); mat2 m2 = mat2(v2_1, v2_2); // 生成的矩阵是: 1 3 2 4

同样,如果参数不够且参数数量不止1个,就会 报错
访问矩阵元素一般使用方括号运算符,例如:
m4[0]// 第1列元素,4维向量 m4[0][1]// 第1列第2行的元素,基本值 m4[0].y// 同上 注意 :同样,方括号中的值也必须是 常量索引值

3.结构体(struct) 类似于C语言的结构体,如下:
// 声明自定义结构体类型 struct light { vec4 color; vec3 pos; }; // 声明结构体类型变量 light l1, l2; // 等价于C语言的struct light l1, l2; // 也可以像C语言那样在声明结构体的同时声明结构体类型的变量 struct light { vec4 color; vec3 pos; } l3;

声明结构体后会自动生成 同名构造函数 ,参数顺序必须与结构体中的成员顺序一致,例如:
light l4 = light(vec4(1.0), vec3(0.0));

用点运算符可以直接访问结构体变量的成员,结构体本身只支持赋值(=)和2个比较运算符(==、!=),如果2个结构体成员及顺序都一样,则相等
4.数组(xArray) 只支持 1维数组 ,而且不支持pop、push等操作,数组声明方式和C语言一致,例如:
float a[10]; vec4 arr[3];

同样,方括号中的值只能是 常量索引值 ,而且数组 不能在声明的同时初始化 ,必须显示地对每个元素进行赋值
5.取样器(sampler) 可以通过取样器变量访问纹理,取样器变量只有2种:sampler2D和samplerCube,而且取样器变量 只能 是 uniform 变量,例如:
uniform sampler2D u_Sampler;

唯一能给取样器变量赋的值是 纹理单元编号 ,比如 GLES20.glUniform1i(u_Sampler, 0) 把纹理单元编号0传递给着色器,所以取样器变量 数量有限 ,片元着色器中最多8个,顶点着色器中没有取样器变量
但GLES20里面给出的纹理单元有32个,分别是GL_TEXTURE0至GL_TEXTURE31。为取样器变量赋值的纹理单元编号应与其所激活的纹理单元编号相对应,比如:
//激活第1个纹理单元 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); //GLES20.GL_TEXTURE31 //使之起作用 GLES20.glEnable(GLES20.GL_TEXTURE_2D); //toDo 生成纹理id,绑定纹理//为取样器变量赋值纹理单元编号 GLES20.glUniform1i(mInputImageTexture, 0); //如果是第32个纹理单元,则赋值为32

此外,除了 = 、 == 和 != 之外,取样器变量 不可以 作为操作数参与运算
五.矢量运算和矩阵运算
矢量和矩阵只支持比较运算符中的 == 和 != ,运算赋值(+=, -=, *=, /=)操作作用在矢量和矩阵上实际效果是对每一个分量进行运算赋值
1.矢量和浮点数运算
v2 + f; // v2[0] + f // v2[1] + f

2.矢量与矢量运算
v2_1 + v2_2; // v2_1[0] + v2_2[0] // v2_1[1] + v2_2[1]

3.矩阵和浮点数运算 【注】假设m2为[2x2]矩阵
m2 + f; // m2[0] + f // m2[1] + f // m2[2] + f // m2[3] + f

4.矩阵右乘矢量 【注】假设m3为[3x3]矩阵
m3 * v3; // m3[0][0] * v3[0] + m3[1][0] * v3[1] + m3[2][0] * v3[2] // m3[0][1] * v3[0] + m3[1][1] * v3[1] + m3[2][1] * v3[2] // m3[0][2] * v3[0] + m3[1][2] * v3[1] + m3[2][2] * v3[2]

5.矩阵左乘矢量
v3 * m3; // v3[0] * m3[0][0] + v3[1] * m3[0][1] + v3[2] * m3[0][2] // v3[0] * m3[1][0] + v3[1] * m3[1][1] + v3[2] * m3[1][2] // v3[0] * m3[2][0] + v3[1] * m3[2][1] + v3[2] * m3[2][2]

6.矩阵乘矩阵
m3a * m3b; // m3a[0][0] * m3b[0][0] + m3a[1][0] * m3b[0][1] + m3a[2][0] * m3b[0][2] // m3a[0][0] * m3b[1][0] + m3a[1][0] * m3b[1][1] + m3a[2][0] * m3b[1][2] // m3a[0][0] * m3b[2][0] + m3a[1][0] * m3b[2][1] + m3a[2][0] * m3b[2][2] // m3a[0][1] * m3b[0][0] + m3a[1][1] * m3b[0][1] + m3a[2][1] * m3b[0][2] // m3a[0][1] * m3b[1][0] + m3a[1][1] * m3b[1][1] + m3a[2][1] * m3b[1][2] // m3a[0][1] * m3b[2][0] + m3a[1][1] * m3b[2][1] + m3a[2][1] * m3b[2][2] // m3a[0][2] * m3b[0][0] + m3a[1][2] * m3b[0][1] + m3a[2][2] * m3b[0][2] // m3a[0][2] * m3b[1][0] + m3a[1][2] * m3b[1][1] + m3a[2][2] * m3b[1][2] // m3a[0][2] * m3b[2][0] + m3a[1][2] * m3b[2][1] + m3a[2][2] * m3b[2][2]

六、常用修饰符
GLSL中的变量可使用如下修饰符修饰:
修饰符 作用
const 指只读常量,在声明时必须初始化,即在GLSL里面就必须初始化,不能外部赋值
uniform 一致变量,指在着色器执行期间它的值是不变的,且它在顶点着色器与片元着色器中可以实现共享。
attribute 只读的顶点数据,用在顶点着色器中,一个attribute可以是浮点数类型的标量,向量,或者矩阵。不可以是数组或则结构体
varying 用于输出变量,只能用在顶点着色器中
centorid varying 【暂时还没有用过】在没有多重纹理时,它跟varying一个意思,如果有多重纹理,centorid varying在光栅化的图形内部进行求值而不是在片段中心的固定位置求值。
除了上面这些代表外,这里再提下精度,我们在写GLSL时,经常在片元着色器中加上这样一句:precision mediump float,这是什么意思呢?意思就是:
指定在片元着色器里面用到的所有float数据,都是中精度。(除了mediump,还有highp高精度,lowp低精度)
这样做的好处就是:能帮助着色器程序提高运行效率,减少内存开支。
片元着色器是否支持高精度需要设备支持,可以通过检查宏来检测: GL_FRAGMENT_PRECISION_HIGH
七、GLSL内置变量
这个内置变量,就像我们平时写代码时定义的局部变量,只能内部调用,内部赋值。
可以与固定函数功能进行交互。在使用前不需要声明。它有顶点着色器内置变量与片元着色器变量之分。
顶点着色器内置变量:
名称 类型 描述
gl_Position vec4 【很重要,必写】变换后的顶点的位置,用于后面的固定与裁剪等操作。所有的顶点着色器都必须写这个值。
gl_Color vec4 顶点主颜色
gl_SecondaryColor vec4 顶点辅助颜色
gl_Normal vec3 表示顶点的法线值
gl_PointSize float 点的大小,这个可以直接在GLSL里面给定值
gl_FrontColor vec4 正面的主颜色(用于varying输出)
gl_TexCoord[] vec4 【这个暂时还没用过】纹理坐标的数组varying输出
片元着色器内置变量: 片元着色器里面的内置变量用的最多的就是gl_FragColor,它是用于最后的像素显示操作。
几个重要的内置变量如下:
名称 类型 作用
gl_FragColor vec4 输出的颜色用于随后的像素操作
gl_Color vec4 包含主颜色的插值只读输入
gl_SecondaryColor vec4 包含辅助颜色的插值只读输入
gl_TexCoord[] vec4 包含纹理坐标数组的插值只读输入
gl_FogFragCoord float 包含雾坐标的插值只读输入
gl_FragCoord vec4 只读输入,窗口的x,y,z和1/w
gl_FrontFacing bool 只读输入,如果是窗口正面图元的一部分,则这个值为true
gl_PointCoord vec2 点精灵的二维空间坐标范围在(0.0, 0.0)到(1.0, 1.0)之间,仅用于点图元和点精灵开启的情况下。
gl_FragData[] vec4 使用glDrawBuffers输出的数据数组。不能与gl_FragColor结合使用。
gl_FragDepth float 输出的深度用于随后的像素操作,如果这个值没有被写,则使用固定功能管线的深度值代替
八.流程控制
1.分支 if-else 结构用法与C语言和js一致,但 没有switch 语句
2.循环 只支持 for循环,而且 只能 在初始化表达式(for(; ; )中第一个分号前面的位置)中定义循环变量,例如:
for (int i = 0; i < 10; i++) { //... }

只允许 有一个循环变量,而且循环变量 只能 是int或者float,而且条件表达式(for(; ; )中2个分号之间的位置) 必须 是循环变量与整形常量的比较,而且在循环体内部,循环变量 不能 被赋值
限制比较多,是为了让编译器能够对for循环进行内联展开
continue 、 break 用法与js一致,此外,还有一个 discard ,只能在片元着色器中使用,表示放弃当前片元,直接处理下一个片元
九.函数
与C语言基本一致,但无法返回数组,如果返回自定义结构体,结构体成员中也不能有数组,例如:
float luma(vec4 color) { return 0.2126 * color.r + 0.7162 * color.g + 0.0722 * color.b; }

同样,需要先声明,后调用,否则就要在调用之前声明函数签名,例如:
float luma(vec4); // 声明函数签名 void main() { luma(color); }

此外, 不允许递归 ,这个限制也是为了编译器能够对函数进行内联展开
十.预处理指令
类似于C语言,常用的3种预处理指令如下:
// 1 #if 条件表达式 如果条件表达式为真,执行这部分 #endif// 2 #ifdef 宏 如果宏存在,执行这部分 #endif // 3 #ifndef 宏 如果宏不存在,执行这部分 #endif// 定义宏 #define 宏名 宏内容 // 解除宏定义 #undef 宏名 比较有用的是: #version 101 可以指定使用GLSL ES1.01版本,该指令必须在着色器顶部, 之前只能是空白或者注释

    推荐阅读