本章主要内容:
(1)数据、变量和变量类型。
(2)矢量、矩阵、结构体、数组、采样器(纹理)
(3)运算、程序流、函数
(4)attribute、uniform和varying变量
(5)精度限定词
(6)预处理和指令
WebGL并不支持
GLSL ES 1.00
的所有特性。实际上,它支持的是1.00版本的一个子集,其中只包括WebGL需要的那些核心特性。GLEL ES编程语言是在OpenGL着色器语言(GLSL)的基础上,删除和简化一部分后形成的,降低了硬件消耗,减少了性能开销。
基础:
(1)程序式大小写敏感的
(2)每一个语句都应该以一个英文分号结束执行次序
从main函数开始执行。
着色器程序有且仅有一个main()函数,而且该函数不能接受任何参数。
main函数前的void关键字表示这个函数不返回任何值。注释
单行注释: // int kp = 496;
多行注释: /* haha */数据值类型(数值和布尔值)
GLSL支持两种数据值类型
(1)数值类型:整数(没有小数点)和浮点数(有小数点)
(2)布尔值类型:true 和 false
不支持字符串类型变量
规则:
(1)只包括a-z,A-Z,0-9和下划线_
(2)变量名的首字母不能是数字
(3)不能是关键字和保留字,但是变量名的一部分可以是它们
(4)不能以gl_
,webgl_
,或_webgl_
开头,这些前缀已经被OpenGL ES保留了
GLSL ES是强类型语言
(1)GLSL ES要求具体指明变量的数据类型: <类型> <变量名>
如:vec4 a_Position
(2)定义函数时,必须指定函数的返回值
(3)在进行赋值操作(=)的时候,等号左右两侧的数据类型也必须一样,否则就会出错基本类型
为变量指定类型有利于WebGL系统检查代码错误,提高程序的运行效率。
如: float klimt // 浮点数变量
- 赋值和类型转换
=
用于赋值,赋值时要保证左侧变量的类型和右侧的值类型一致
float f2 = 8.0;
可以使用内置函数进行类型转换,如:
float f3 = float(8);
- 运算符
说明:
[1] 在进行逻辑与(&&)运算时,只有第一个表达式的计算值为true时才会计算第二个表达式。同样,在进行逻辑或(||)运算时,只有第一个表达式的值为false时才会计算第二个表达式。
[2] 逻辑异或(^^)运算的含义是:只有当左右两个表达式中有且仅有一个为true时,运算结果才是true,否则为false。
- 矢量和矩阵
(1)矢量和矩阵类型的变量都包含多个元素,每个元素是一个数值(整型数、浮点数和布尔值)
矢量将这些元素排成一列,可以用来表示顶点坐标或颜色值等,而矩阵将元素划分成行和列,可以用来表示变换矩阵。
(2)赋值和构造
(a) =
等号用于赋值,如:vec4 position = vec4(1.0, 2.0, 3.0, 4.0);
(b)构造函数:专门创建指定类型的变量的函数,构造函数的名称和其创建的变量类型名称总是一致的。
(3)矩阵构造函数
(a)想矩阵构造函数中传入矩阵的每一个元素的数值来构造矩阵,注意传入值的顺序必须是列主序的
(b)向矩阵构造函数中传入一个或多个矢量,按照列主序使用矢量里的元素值来构造矩阵。
// 使用两个vec2对象来创建mat2对象
vec2 v2_1 = vec2(1.0, 3.0);
vec2 v2_2 = vec2(2.0, 2.0);
mat2 m2_1 = mat2(v2_1, v2_2); // 1.0 2.0
// 3.0 4.0
// 使用一个vec4对象来创建mat2对象
vec4 v4 = vec4(1.0, 3.0, 2.0, 4.0);
mat2 m2_2 = mat2(v4); // 1.0 2.0
// 3.0 4.0
(c)向矩阵构造函数中出阿奴矢量和数值,按照列主序使用矢量里的元素值和直接传入的数值来构造矩阵
// 使用两个浮点数和一个vec2对象来创建mat2对象
mat2 m2 = mat2(1.0, 3.0, v2_2); // 1.0 2.0
// 3.0 4.0
(d)向矩阵构造函数中传入单个数值,这样将生成一个对角线上元素都是该数值,其他元素为0.0的矩阵
mat4 m4 = mat4(1.0); // 1.0 0.0 0.0 0.0
// 0.0 1.0 0.0 0.0
// 0.0 0.0 1.0 0.0
// 0.0 0.0 0.0 1.0
与矢量构造函数类似,如果传入的数值的数量大于1,有没有达到矩阵元素的数量,就会出错
mat4 m4 = mat4(1.0, 2.0, 3.0); // 错误。mat4对象需要16个元素
(3)访问元素
为了访问矢量或矩阵中的元素,可以使用.
或[]
运算符
(a).
运算符
任何适量的x,r或s分量都会返回第一个分量,y,g,t分量都会返回第二个分量。
如:
vec3 v3 = vec3(1.0, 2.0, 3.0); // 将v3设为(1.0, 2.0, 3.0)
float f;
f = v3.x; // 设f为 1.0
f = v3.y; // 设f为 2.0
f = v3.z; // 设f为 3.0
f = v3.r; // 设f为 1.0
f = v3.s; // 设f为 1.0
将(同一个集合的)多个分量名共同置于点运算符后,就可以从矢量中同时抽取出多个分量。这个过程乘坐混合(swizzling)
如: v2 = v3.xz
此时的多个分量必须属于同一个集合,比如说,你不能使用v3.was
(b)[]
运算符
矩阵中的元素从下标0开始按照列主序读取。
限制:[]
中只能出现的索引值必须是常量索引值
常量索引值
定义如下:
(a)整型字面量(0或1)
(b)用 const
修饰的全局变量或局部变量。不包括函数参数。
(c)循环索引
(d)由前述三条中的项组成的表达式
const int index = 0 // const 关键字表示变量是只读的
vec4 v4a = m4[index] // 同m4[0]相同
注意,你不能使用未经const
修饰的变量作为索引值,因为它不是一个常量索引值(除非它是循环索引)。
int index1 = 0
vec4 v4c = m4[index2] // 错误:index不是常量索引
(4)运算符
对于矢量和矩阵,只可以使用比较运算符中的==
和 !=
,不可以使用>
、<
、>=
和<=
。
如果想要比较矢量和矩阵的大小,应该是用内置函数,比如lessThan()
。
如果你想逐分量比较,可以使用内置的函数equal()
或notEqual()
- 矢量和浮点数的运算
(1) 矢量运算
(2) 矩阵和浮点数的运算
(3)矩阵右乘矢量
(4) 矩阵左乘矢量
(5)矩阵与矩阵相乘
- 结构体
(1)结构体:用户自定义的类型,使用关键字 struct,将已存在的类型聚合到一起,就可以定义为结构体。如:
struct light { // 定义结构体light
vec4 color; // 光的颜色
vec4 position; // 广元位置
}
light 11, 12; // 声明了light类型的变量11和12
也可以在定义结构体的同时声明该结构体类型类型的变量,如:
struct light { // 定义结构体和定义变量同时进行
vec4 color; // 光的颜色
vec4 position; // 广元位置
} 11; // 该结构体类型的变量11
(2)赋值和构造
结构体有标准的构造函数,其名称与结构体名一致。构造函数的参数的顺序必须与结构体定义中的成员顺序一致。
(3)访问成员
在结构体变量名后跟点运算符(.),然后再加上成员名,就可以访问变量的成员。如:
vec4 color = 11.color;
vec3 position = 11.position;
- 数组
(1)
ELSL ES 只支持一维数组,而且数组对象不支持pop()和push()等操作,创建数组时也不需要使用new运算符。
声明数组,只需要在变量名后加上中括号和数组长度,如:
float floatArray[4]; // 声明含有4个浮点数元素的数组
vec4 vec4Array[2]; // 声明含有两个vec4对象的数组
数组的长度必须是大于0的整型常量表达式,定义如下:
(a)整型字面量(如0或1)
(b)用const限定字修饰的全局变量或局部变量,不包括函数参数
(c)由前述两条中的项组成的表达式
举例:
int size = 4;
vec4 vec4Array[size]; // 错误。如果第一行为const int size = 4;则不会报错
注意,你不可以用const限定字来修饰数组本身。
只有整型常量表达式和uniform变量可以被用作数组的索引值。
数组不能在声明时被一次性地初始化,而必须显式地对每个元素进行初始化。如:
vec4Array[0] = vec4(4.0, 3.0, 6.0, 1.0);
vec4Array[1] = vec4(3.0, 2.0, 0.0, 1.0);
数组本身只支持[]运算符,但数组的元素能够参与其自身类型支持的任意运算。如:
// 将floatArray的第二个参数乘以3.14
float f = floatArray[1] * 3.14;
// 将vec4Array的第一个参数乘以vec4(1.0, 2.0, 3.0 ,4.0)
vec4 v4 = vec4Array[0] * vec4(1.0, 2.0, 3.0 ,4.0);
- 取样器(纹理)
必须通过取样器
(sampler)类型变量访问纹理。
有两种基本类型的取样器类型:sampler2D
和samplerCube
取样器变量只能是uniform变量,或者需要访问纹理的函数,如texture2D()
函数的参数,如:
uniform sampler2D u_Sampler;
只有纹理单元编号可以给取样器变量,而且必须使用gl.uniformli()来进行赋值。
除了=
、==
和!=
,取样器变量不可以作为操作数参与运算。
取样器变量受到着色器支持的纹理单元的最大数量限制。
mediump是一个精度限定字
- 运算符优先级
-
程序流程控制:分支和循环
(1)if 和 if-else
如:
if (distance < 0.5) {
gl_fragColor = vec4(1.0, 0.0, 0.0, 1.0);
} else {
gl_fragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
(2)for语句
如:
for (int i = 0; i < 3; i++) {
sum += i;
}
注意:循环变量i
只能在初始化表达中定义,条件表达式可以为空,如果这样做,空的条件表达式返回true
。
for语句的其他限制:
(a)只允许有一个循环变量,循环变量只能是int
或float
类型。
(b)循环表达式必须是以下的形式:i++
,i--
,i+=
常量表达式或i-=
常量表达式
(c)条件表达式必须是循环变量与整型常量的比较
(d)在循环体内,循环变量不可被赋值
这些限制的存在是为了使编译器就能够对for循环进行内联展开
(3)continue、break和discard语句
(a)continue终止包含该语句的最内层循环和执行循环表达式(递增/递减循环变量),然后执行下一次循环
(b)break中止包含该语句的最内层循环,并不在继续执行循环。
如:
// continue case
for (int i = 0; i < 10; i++) {
if (i == 8) {
continue; // 跳过循环体余下的部分,继续下次循环
}
// 当i==8时,不会执行到这里
}
// break case
for (int i = 0; i < 10; i++) {
if (i == 8) {
break; // 跳出for循环
}
// 当i>=8时,不会执行这里
}
// 当i==8时,执行这里
关于discard,它只能在片元着色器中使用,表示放弃当前片元直接处理下一片元。
- 函数
(1)
可以没有return语句,但是返回类型必须是void
也可以将自己定义的结构体指定为返回类型,但是结构体的成员中不能有数组。
示例:
// RGBA颜色值转为亮度值函数
float luma(vec4, color) {
return 0.2126 * color.r + 0.7162 * color.g + 0.0722 * color.b;
}
// 调用
attribute vec4 a_Color // 传了(r, g, b, a)的值
void main() {
...
float brightness = luma(a_Color);
...
}
注意,如果调用函数时传入的参数类型与生命函数时指定的参数类型不一致,就会出错。
如:
float square(float value) {
return value * value;
}
void main() {
...
float x2 = square(10); // 错误。应用10.0
...
}
因为函数声明时的参数是float类型,而调用时却传入了int类型的值。
(2)规范声明
如果函数定义在其调用之后,那么我们必须在进行调用之前先声明该函数的规范。
规范会预先告诉WebGL系统函数的参数、参数类型、返回值等等
如:
float luma(vec4, color); // 规范声明
main() {
...
float brightness = luma(a_Color); // luma在定义之前就被调用了
...
}
float luma(vec4, color) {
return 0.2126 * color.r + 0.7162 * color.g + 0.0722 * color.b;
}
(3)参数限定词
GLSL ES中,可以为参数指定限定自,以控制参数的行为。
我们可以将函数参数定义成:
(a)传递给函数的
(b)将要在函数中被复制的
(c)既是传递给函数的,也是将要在函数中被赋值的。
其中(b)和(c)都有点类似于C语言中的指针
(4)内置函数
- 全局变量和局部变量
attribute、varying和uniform变量都必须声明为全局变量
(1)存储限定字
在GLSL ES中,我们经常使用attribute
、varying
和uniform
限定字来修饰变量,如下图所示。此外,有时也会使用const限定字,它表示着色器中的某个变量是恒定的常量。
(2)const变量
const变量写在类型之前,声明的同时必须对它进行初始化,声明之后就不能再去改变它们的值了。
如:
const int a = 3232
(3)Attributr变量
只能出现在顶点着色器中,只能被声明为全局变量,被用来表示逐顶点的信息。
顶点着色器中能够容纳的attribute变量的最大数目与设备有关,你可以通过访问内置的全局常量来获取最大数目的值。
但是不管设备如何,支持WebGL的环境都支持至少8个attribute
变量。
(4)uniform变量
可以用在顶点着色器和片元着色器中,且必须是全局变量。
uniform变量只读,可以是除了数组或结构体之外的任意类型。
如果在顶点着色器和片元着色器中声明了同名的uniform变量,那么它就会被两种着色器共享。
uniform变量包含了一致(非逐顶点/逐片元的,各顶点或各片元公用)的数据,JS应该向其传递此类数据。
比如,变换矩阵就不是逐定点的,而是所有顶点共用的,所以它在着色器中是uniform变量。
uniform mat4 u_ViewMatrix
(5)varying变量
必须是全局变量
从顶点着色器向片元着色器传输数据。
必须在两种着色器中生命同名、同类型的varying变量
varying vec2 v_TexCoord
varying vec4 v_Color
varying变量只能是以下类型:float
、vec2
、vec3
、vec4
、mat2
、mat3
和mat4
顶点着色器中赋给varying变量的值并不是直接传给了片元着色器的varying变量,这其中发生了光栅化的过程:根据绘制的图形,对前者(顶点着色器varying变量)进行内插,然后再传递个后者(片元着色器varying变量)
正是因为varying变量需要被内插,所以我们需要限制它的数据类型
设备至少支持8个varying变量
- 精度限定字
帮助着色器程序提高运行效率,削减内存开支。
可选,不确定精度可以使用适中的默认值:
#ifdef GL_ES
precision mediump float;
#endif
WebGL中支持的3种精度
注意:
(1)在某些WebGL环境中,片元着色器可能不支持highp
精度
(2)数值范围和精度实际上也是与系统环境相关,可以使用gl.getShaderPrecisionFormet()
来检查
如:
mediump float size; // 中精度浮点型变量
highp vec4 position; // 具有高精度浮点型的vec4对象
lowp vec4 color; // 具有低精度浮点型的vec4对象
声明着色器的默认精度,这行代码必须在顶点着色器或片元着色器的顶部:
precision
精度限定自 类型名称
表示接下来所有不以精度限定自修饰的该类型标量,其精度就是默认精度,如:
precision mediump float;
只有片元着色器
中的float类型没有默认精度,我们需要手动指定。
- 预处理指令
用来在真正编译之前对代码进行预处理,#
开始
#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float; // 支持高精度,限定浮点型为高精度
#else
precision mediump float; // 不支持高精度,限定浮点型为中精度
#endif
#endif
可以只是用#version number
来指定着色器使用的GLSL ES版本
可以接受的版本包括100(GLSL ES 1.00)和101(GLSL ES 1.01)。如果不使用#version
命令,默认版本为100
。
指定版本代码:
#version 101
#version
指令必须在着色器顶部,在它之前只能有注释和空白。