1、概述
着色器语言和C/C++
很类似,拥有C
语言的语法规范,还有C++
的结构类型,GLSL ES
语言是在OpenGL
的着色器语言GLSL
基础上,删减一部分后形成的,GLSL ES
的目标平台是电子产品和嵌入式设备。
着色器语言开头必须有一个入口main()
函数;
2、数据类型
支持两种数据类型
- 数值类型:整数、浮点数,没有小数点的是整数,有的则是浮点数;
- 布尔值类型:
true
和false
;
注意:
GLSL ES
不支持字符串类型
3、变量声明
GLSL ES
是强类型语言,因此声明变量时需要指定类型;
<类型><变量名>
如:vec4 a_Position;
因此,在定义和使用变量时需要时刻注意变量的类型;
4、基本类型
float
:单精度浮点数类型,该类型的变量表示一个单精度浮点数int
:整形术,该类型的变量表示一个整数bool
:布尔值,true
或false
5、矢量和矩阵
GLSL ES
支持矢量和矩阵类型,这两种类型很适合处理计算机图形,矢量和矩阵类型的变量都包含多个元素,每个元素是一个数值(整型数、浮点数或布尔值);
类别 | 数据类型 | 描述 |
---|---|---|
矢量 | vec2\vec3\vec4 ,ivec2\ivec3\ivec4 ,bvec2\bvec3\bvec4 | 分别具有2,3,4 个浮点、整形、布尔值元素的矢量 |
矩阵 | mat2\mat3\mat4 | 2*2,3*3,4*4 的浮点数元素的矩阵 |
1、赋值和构造
使用=
来对矢量和矩阵进行赋值操作,赋值运算符左右两边的变量和数据类型必须要一直,并且元素的个数也必须相同。
通常使用与数据类型同名的内置构造函数来生成变量,如下:
vec4 position = vec4(1.0, 2.0, 3.0, 1.0)
;
这种专门创建指定类型的变量的函数被称为 构造函数,构造函数的名称和其创建的变量的类型名称总是一致的。
2、访问元素
可以通过点号.
或者[]
运算符,来访问矩阵和矢量中的元素;
分两名
类别 | 描述 |
---|---|
x,y,z,w | 用来获取顶点坐标分量 |
r,g,b,a | 用来获取颜色的分量 |
s,t,p,q | 用来获取纹理坐标分量 |
例如访问矢量的元素
vec3 v3 = vec3(1.0, 2.0, 3.0);
float f = 0.1;
f = v3.x;//f被赋值为`1.0`
还可以使用[]
运算符来访问元素
vec3 v3 = vec3(1.0, 2.0, 1.5);
float f = v3[0];//将1.0赋值给f
在[]
中只能出现的索引值必须是 常量索引值
常量索引值定义如下
- 整型字面量
- 用
const
修饰的全局变量或局部变量 - 循环索引值
- 由前述三条中的项组成的表达式
注意:不能使用未经
const
修饰的变量作为索引值,因为它不是一个常量索引值(除非它是循环索引)
如下是错误的:
int index = 0;
vec4 v4c = m4[index];//错误:inde 不是常量索引值
1、混合
将同一个集合的多个分量名共同置于点运算符后,就可以从矢量中同时抽取出多个分量,这个过程称作 混合
vec3 position = vec3(1.0, 1.5, 2.0);//声明一个矢量
vec2 v2;
v2 = v3.xy;//将v2设置为(1.0, 1.0)
v2 = v3.yz;
3、运算符
对于矢量和矩阵所支持的运算,和基本类型的运算类似
注意:对于矢量和矩阵的比较,只可以使用比较运算符中的
==
和!=
如果需要比较矢量和矩阵的大小,应该使用内置函数,比如lessThan()
矢量和矩阵可用的运算符
运算符 | 运算 | 使用数据类型 |
---|---|---|
+、-、*、/ | 加减乘除 | 适用于vec[234] 和mat[234] |
++、– | 自增自减 | 适用于vec[234] 和mat[234] |
= | 赋值运算 | 同上 |
+=、-=、*=、/= | 运算赋值 | 同上 |
==、!= | 比较 | 对于== 如果两个操作数的每一个分量都相同,那么返回true ,对于!= 如果两个操作数的任何一个分量不同则返回true |
当运算赋值作用于,矢量或者矩阵时,实际上时逐分量的对矩阵或矢量的每一个元素进行独立的运算赋值;
下面是一些矢量和矩阵在运算时的常见情形
// 假如定义以下变量
vec3 v3a, v3b, v3c;
mat3 m3a, m3b, m3c;
float f;
1、矢量和浮点数的运算
// 加减乘除操作符效果一样
v3b = v3a + f; // 相当于
// v3b.x = v3a.x + f;
// v3b.y = v3a.y + f;
// v3b.z = v3a.z + f;
// 例如 v3a = vec3(1.0, 2.0, 3.0); f = 1.0,那么 v3b = (2.0, 3.0, 4.0);
2、矢量运算
// 矢量运算操作发生在矢量的每个分量上
// 加减乘除效果一样
v3c = v3a + v3b;//相当于
// v3a.x + v3b.x;
// v3a.y + v3b.y;
// v3a.z + v3b.z;
// 例如 v3a = vec3(1.0, 2.0, 3.0); v3b = (2.0, 4.0, 6.0),那么结果v3c = (3.0, 6.0, 9.0)
3、矩阵和浮点数的运算
矩阵与浮点数的运算发生在矩阵的每个分量上
// 加减乘除效果类似
m3b = m3a * f;
// 相当于把矩阵,m3b[0].x = m3a[0].x * f;每个分量都乘以f
1、矩阵右乘矢量
矩阵右成矢量的结果是 矢量 其中每个分量都是原矢量中对应分量,乘上矩阵对应行的每个元素的积的加和;
2、矩阵左乘矢量
矩阵左乘矢量也是可以的,但结果与右乘不一样;
3、矩阵与矩阵相乘
m3c = m3a * m3b;
6、结构体
结构体类似java
语言中的类class
但又有很大不同,GLSL ES
支持用户自定义的类型,使用关键子struct
将已存在的类型聚合到一起,就可以定义为 结构体;
例如顶一个灯的结构
struct light { // 定义了结构体类型 light,包含两个成员 color和position
vec4 color;
vec3 position;
}
light l1,l2;// 声明了 light 类型的变量 l1和l2
和C
语言不同的是,不需要使用typedef
关键子来定义结构体,因为结构体结构体的名称会自动称为类型;
可以在同一条语句中定义和声明改结构体类型的变量
struct light { //定义结构体和定义变量同时进行
vec4 color; //光的颜色
vec3 position; //光源的位置
} l1; //结构体类型的变量l1
1、赋值和构造
结构体有标准的构造函数,其名称与结构体名一致,构造函数的参数顺序必须与结构体定义中的成员顺序一致;
例如给刚才创建的light
类型l1
赋值
ls = light(vec4(0.0, 0.1, 0.1, 1,0),vec3(2.0, 4.0, 2.0));
// 上面的参数是灯光的颜色和光源的位置 light()是结构体构造函数
2、访问成员
在结构体变量名后跟点运算符,然后再加上成员名就可以访问变量的成员
vec4 color = l1.color;//点号访问结构体成员
vec3 position = l1.position;
3、运算符
结构体的成员可以参与其自身类型支持的任何运算,但是结构体本身只支持两种运算,赋值( = )和比较(==和!=)
当且仅当两个结构体变量所对应的所有成员都相等的时候,==
运算符才会返回true
,如果任意某个成员不相等那么!=
运算符返回true
7、数组
GLSL ES
只支持一维数组,声明数组只需要在变量名后加上中括号[]
和数组的长度;
1、数组声明
float floatArray[4];//声明含有四个浮点数元素的数组
vec4 vec4Array[2];//声明含有两个vec4对象的数组
数组的长度必须时大于0
的 整型常量表达式,满足条件如下:
- 整型字面量(如
0
或1
) - 用
const
限定字符修饰的全局变量或者局部变量,不包括函数参数 - 由前述两条中的项组成的表达式
因此下面的代码会报错
int size = 4;
vec4 vec4Array[size];//错误,因为变量`size`不是通过`const`修饰的,`const int size = 4;` 使用`const`修饰的值是一个常量值
2、访问元素
访问数组元素可以通过下标索引值来访问,只有整型常量表达式和uniform
变量,可以用作数组的索引值
float f = floatArray[1];//索引值0开始
3、数组初始化
与JavaScript
和C
不同,数组不能在声明时被一次性的初始化,而必须显示的对每个元素进行初始化;
vec4 vec4Array[2];
vec4Array[0] = vec4(1.0, 1.0, 1.0, 1.0);
vec4Array[1] = vec4(2.0, 2.0, 2.0, 1.0);
数组本身只支持[]
运算,但数组的元素能够参与其自身类型支持的任意运算
//将floatArray的第二个元素乘以 3.14
float f = floatArray[1] * 3.14;
8、取样器
将GLSL ES
支持的一种内置类型,称为 取样器,我们必须通过该类型变量来访问纹理;
两种基本取样器类型:
sampler2D
samplerCube
取样器变量只能是 uniform
变量,或者需要访问纹理的函数的参数;
uniform sampler2D u_Sampler;
唯一能赋值给取样器变量的就是纹理单元编号,而且必须使用webgl
方法gl.uniformli()
来进行赋值,如下
gl.uniformli(u_Sampler,0);// 将纹理单元编号0传给着色器
除了=、==和!=
取样器不可以作为操作数参与运算;
取样器类型的变量受到着色器支持的纹理单元的最大数量限制,
着色器 | 表示最大数量的内置常量 | 最小数量 |
---|---|---|
顶点着色器 | const mediump int gl_MaxVertexTextureImageUnits | 0 |
片元着色器 | const mediump int gl_MaxTextureImageUnits | 8 |
9、函数
GLSE ES
定义函数的方式更接近于C
语言
返回类型 函数名(type0 arg0,type1 arg1,...,typen argn){
函数计算
return 返回值
}
参数的type
必须为本章所述的类型之一,或者像main()
函数这样没有参数也是允许的,如果函数不返回值那么返回类型必须时void
,也可以将自定义的结构体类型指定为返回值类型,结构体的成员汇总不能有数组;
需要注意的时 不能在一个函数内部调用它本身,递归调用时不允许的,这项限制的目的也是为了便于编译器对函数进行内敛展开
1、规范声明
如果函数定义在其调用之后,那么我们必须在进行调用之前,先声明该函数的规范,规范声明会预先告诉webgl
系统的参数,参数类型,返回值等等
float luma(vec4);//规范声明
main(){
float brightness = luma(color);//luma()在定义之前就被调用了
}
float luma(vec4 color){
return 0.21 * color.r + 0.71 * color.g + 0.07 * color.b;
}
2、参数限定词
在GLSL ES
中,可以为函数参数指定限定字,以控制参数的行为。
类别 | 规则 | 描述 |
---|---|---|
in | 向函数中传入值 | 参数传入函数,函数内可以使用参数的值,也可 |
const in | 向函数中传入值 | 参数传入函数,函数内可以使用参数的值,但不能修改 |
out | 在函数中被赋值,并被传出 | 传入变量的引用,若其在函数内被修改,会影响到外部传入的变量 |
inout | 传入函数,同时在函数中被赋值,并被传出 | 纯如变量的引用,函数会用到变量的初始值,然后修改变量的值,会影响到函数外部传入的变量 |
<无:默认> | 将一个值传给函数 | 和 in 一样 |
10、存储限定字
1、const
- 被
const
修饰的值不能被改变 - 在声明
const
变量时,需要将const
写在类型之前 - 声明时必须进行初始化
- 声明之后不能去改变值
被const
修饰的变量就是所谓常量,常量的值是不允许被改变的,而且数组的索引也需要时常量值;
2、attribute
attribute
变量只能出现在顶点着色器中- 只能被声明为全局变量
- 用来表示逐顶点的信息
顶点着色器中,能够被容纳的attribute
变量的最大数目于设备有关,可以通过内置的全局常量,来获取最大数目,支持webgl
的环境都支持至少8
个attribute
变量;
3、uniform
- 可以用在顶点着色器和片元着色器中
- 必须是全局变量
uniform
变量是只读的,可以是除了数组或结构体之外的任意类型- 如果顶点着色器和片元着色器中声明了同名的
uniform
变量,那么它会被两种着色器共享 uniform
变量包含了 一致(非逐顶点/逐片元的,各顶点或各片元公用)的数据
变换矩阵就不是逐顶点的,而是所有顶点公用的
4、varying
varying
变量必须是全局变量- 作用是从顶点着色器向片元着色器传输数据
- 必须在两种着色器中声明同名、同类型的
varying
变量
顶点着色器中赋给varying
变量的值,并不是直接传给片元着色器中的varying
变量,这其中发生了光栅化的过程,根据绘制的图形,对前者(顶点着色器varying
变量)进行内插,然后再传递给后者(片元着色器中的varying
变量),正是因为varying
变量需要被内插,所以我们需要限制它的数据类型;
下表是attribute
变量、uniform
变量和varying
变量的数目限制
变量类型 | 内置全局变量(表示最大数量) | 最小值 |
---|---|---|
attribute 变量 | const mediump int gl_MaxVertexAttribs | 8 |
uniform 变量(顶点着色器) | const mediump int gl_MaxVertexUniformVectors | 128 |
uniform 变量(片元着色器) | const mediump int gl_MaxFragmentUniformVEctors | 16 |
varying 变量 | const mediump int gl_MaxVaryingVectors | 8 |
11、精度限定字
GLSL ES
新引入了精度限定字,目的是帮助着色器程序提高运行效率,消减内存开支;
精度限定字:用来表示每种数据具有的精度(比特数)
高精度的程序需要更大的开销更大的开销和更久的计算时间,而低精度的程序开销则小得多;
使用精度限定字,你就能精细的控制程序再效果和性能间的平衡;
精度限定字是可选的,如果不确定,可以使用下面这个适中的默认值
#ifdef GL_ES
precision mediump float;
#endif
webgl
支持三种精度,其限定字分别为highp
(高精度)、mediump
(中精度)和lowp
(低精度)
精度限定字 | 描述 |
---|---|
highp | 高精度,顶点着色器的最低精度 |
mediump | 中精度,片元着色器的最低精度 |
lowp | 低精度,可以表示所有颜色 |
注意:在某些
webgl
环境中,片元着色器可能不支持highp
精度,其次数值范围和精度实际上也是于系统环境相关的
声明变量精度的例子
mediump float size; // 中精度浮点型变量
highp vec4 position; // 具有高精度浮点元素的vec4对象
lowp vec4 color; // 具有低精度浮点元素的vec4对象
也可以使用关键字precision
来声明着色器的默认精度,在顶点或者片元着色器的顶部
precision 精度限定字 类型名称;
例如:
precision mediump float;// 所有浮点数默认为中精度
上面代码表示,在着色器中,某种类型的变量其默认精度由精度限定字限定,也就是说接下来所有不以精度限定字修饰的该类型变量,其精度就是默认精度
对于很多类型着色器已经实现了默认的精度,只有片元着色器中的float
类型没有默认精度;
数据类型的默认精度
着色器类型 | 数据类型 | 默认精度 |
---|---|---|
顶点着色器 | int | highp |
float | highp | |
sampler2D | lowp | |
samplerCube | lowp | |
片元着色器 | int | mediump |
float | 无 | |
sampler2D | lowp | |
samplerCube | lowp |
事实上 片元着色器中的float
类型没有默认精度,需要我们手动指定,如果我们不再片元着色器中限定float
类型的精度,就会导致如下的编译错误;
12、预处理指令
GLSL ES
支持预处理指令,用来在真正编译之前对代码进行预处理,都以井号开头;
#ifdef GL_ES
precision mediump float;
#endif
上面代码检查了是否已经定义了GL_ES
宏,如果是那就执行#ifdef
和#endif
之间的部分;
1、三种预处理指令
#if 条件表达式
如果条件表达式为真,执行这里
#endif
#ifdef 某宏
如果定义了某宏,则执行这里
#endif
#ifndef 某宏
如果没有定义某宏,执行这里
#endif
2、定义宏
可以使用 #define
指令进行宏定义,和C
语言不同的是,GLSL ES
中的宏没有宏参数;
#define 宏名 宏内容
3、解除宏定义
使用 #undef
指令可以解除宏定义
#undef 宏名
4、内置宏
可以使用 #else
指令配合 #ifdef
实现条件匹配
#define NUM 10
#if NUM == 100
如果宏NUM为100,执行这里
#else
否则,执行这里
#endif
预定义的内置宏
宏 | 描述 |
---|---|
GL_ES | 在OpenGL ES 2.0 中定义为1 |
GL_FRAGMENT_PRECISION_HIGH | 片元着色器支持highp 精度 |
所以可以这样使用宏来进行精度限定
#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float; // 支持高精度,限定浮点型为高精度
#else
precision mediump float;// 不支持高精度,限定浮点型为中精度
#endif
#endif