写在前面
上一篇笔记后,我们就结束了WebGL最最基础的部分,本篇笔记全面介绍GLSL ES语法,相对独立,然后就是矩阵矩阵矩阵(变换、视角、光照、控制复杂模型等等,全都是在搞矩阵)
一.概述
GLSL ES是在GLSL(OpenGL着色器语言)的基础上,删除和简化了一部分功能后形成的,目标平台是消费电子产品和嵌入式设备,比如智能手机、游戏主机等等,ES版本主要降低了硬件功耗,减少了性能开销
P.S.实际上WebGL并不支持GLSL ES的所有特性,支持的是GLSL ES 1.00版本的一个子集
二.基本语法规则
大小写敏感
语句末尾必须要有分号
从main函数开始执行
函数声明中不能省略返回值类型(无返回值就是void,C语言可以省略,但这里不行)
注释语法和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;
唯一能给取样器变量赋的值是纹理单元编号,比如gl.uniformi(u_Sampler, 0)
把纹理单元编号0传递给着色器,所以取样器变量数量有限,片元着色器中最多8个,顶点着色器中没有取样器变量
此外,除了=
、==
和!=
之外,取样器变量不可以作为操作数参与运算
五.矢量运算和矩阵运算
矢量和矩阵只支持比较运算符中的==
和!=
,运算赋值(+=, -=, *=, /=)操作作用在矢量和矩阵上实际效果是对每一个分量进行运算赋值
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 + f; // m2[0] + f
// m2[1] + f
// m2[2] + f
// m2[3] + f
4.矩阵右乘矢量
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]
六.流程控制
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);
}
float luma...
此外,不允许递归,这个限制也是为了编译器能够对函数进行内联展开
在GLSL ES中有几个参数限定字,如下:
in 值传递,可以省略,默认就是值传递
const in 值传递,在函数内部无法修改参数
out 地址传递
inout 地址传递,传入的参数必须已经初始化过了
八.存储限定字
attribute、uniform、varying,区别如下:
attribute
只能出现在顶点着色器中,只能是全局变量,类型只能是float或者分量是float的矢量和矩阵。WebGL环境至少支持8个attribute变量,用来表示逐顶点的信息
uniform
只能是全局变量,可以用于顶点着色器和片元着色器,可以是除数组和结构体外的任意类型。WebGL环境至少支持片元着色器中出现16个uniform变量,顶点着色器中是128个,用来表示各顶点、各片元共用的数据
特殊的:如果顶点着色器和片元着色器中声明了同名的uniform变量,那么会被2个着色器共享
varying
只能是全局变量,类型只能是float或者分量是float的矢量和矩阵。WebGL环境至少支持8个varying变量,用来从顶点着色器向片元着色器传递数据,但不是直接传递,有内插的过程
此外,还有const限定字,但不常用
九.精度限定字
引入精度限定字是为了帮助着色器程序提高运行效率,减少内存开支。一般采用中精度:
#ifdef GL_ES
precision mediump float; // 还可以是highp和lowp
#endif
该语句表示后面遇到的所有没有声明精度的浮点数都用中精度,只有float类型没有设置默认精度,所以在着色器源程序中必须先设置float的默认精度再使用float类型
片元着色器是否支持高精度需要设备支持,可以通过检查宏来检测:GL_FRAGMENT_PRECISION_HIGH
十.预处理指令
类似于C语言,常用的3种预处理指令如下:
// 1
#if 条件表达式
如果条件表达式为真,执行这部分
#endif
// 2
#ifdef 宏
如果宏存在,执行这部分
#endif
// 3
#ifndef 宏
如果宏不存在,执行这部分
#endif
// 定义宏
#define 宏名 宏内容
// 解除宏定义
#undef 宏名
比较有用的是:#version 101
可以指定使用GLSL ES1.01版本,该指令必须在着色器顶部,之前只能是空白或者注释
参考资料
- 《WebGL编程指南》