GLSL ES(OpenGL ES着色器语言)_WebGL笔记9

写在前面

上一篇笔记后,我们就结束了WebGL最最基础的部分,本篇笔记全面介绍GLSL ES语法,相对独立,然后就是矩阵矩阵矩阵(变换、视角、光照、控制复杂模型等等,全都是在搞矩阵)

一.概述

GLSL ES是在GLSL(OpenGL着色器语言)的基础上,删除和简化了一部分功能后形成的,目标平台是消费电子产品和嵌入式设备,比如智能手机、游戏主机等等,ES版本主要降低了硬件功耗,减少了性能开销

P.S.实际上WebGL并不支持GLSL ES的所有特性,支持的是GLSL ES 1.00版本的一个子集

二.基本语法规则

  1. 大小写敏感

  2. 语句末尾必须要有分号

  3. 从main函数开始执行

  4. 函数声明中不能省略返回值类型(无返回值就是void,C语言可以省略,但这里不行)

  5. 注释语法和C语言一致(单行//,多行/**/)

三.变量和基本数据类型

1.基本数据类型

只支持2种基本数据类型:

  • 数值类型:整数,浮点数

  • 布尔类型:true和false2个布尔常量

注意:不支持字符串

2.变量

  1. 变量声明

    和C语言一样,类型+变量名,变量命名规则也一样,基本类型只有int、float和bool

  2. 类型转换

    没有隐式类型转换,也不支持3f这样的类型后缀,但提供了类型转换函数:int()、float()和bool(),都只接受其余2种基本数据类型

  3. 运算符

    不支持位运算,其它和C语言一致,也支持3目选择,逻辑与(&&)和逻辑或(||)也有短路特性

  4. 作用域

    与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.xv4.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循环进行内联展开

continuebreak用法与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编程指南》

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code