OpenGL ES 着色器语言(GLSL ES)是在 OpenGL 着色器语言(GLSL)的基础上,删除和简化一部分功能后形成的(好处是降低了硬件的功耗和性能开销),语法与 C 语言较为类似
WebGL 使用的 GLSL ES 版本是 1.00,但是请注意,WebGL 并不支持 GLSL ES 1.00 的所有特性,它支持的是 1.00 版本的一个子集,其中只包括 WebGL 需要的那些核心特性
基础
- 程序是大小写敏感的
- 每一个语句都应该以一个英文分号(
;
)结束 - 程序从
main()
函数开始执行,main()
函数无参数(()
)、无返回值(void
) - 注释
- 单行注释:
// 注释内容
- 多行注释:
/* 注释内容 */
- 单行注释:
变量
- 只包括
a-z
,A-Z
,0-9
和下划线(_
),数字不能开头 - 不能是关键字和保留字,但是你的变量名的一部分可以是它们
- 不能以
gl_
、webgl_
或_webgl_
开头,这些前缀已被保留
全局变量和局部变量
- 全局变量:声明在全局的变量
- 局部变量:声明在函数中的变量
存储限定符
attribute
变量只能出现在顶点着色器中,只能被声明为全局变量,被用来表示逐顶点的信息- “逐顶点”的含义:比如,一条线段有两个顶点,这两个坐标就会传递给
attribute
变量。而线段上的其他点(比如中点),虽然也被画了出来,但它不是顶点,坐标未曾传递给attribute
变量,也未曾被顶点着色器处理过。如果想要让顶点着色器处理它,就需要将它作为一个顶点添加到图形中来 attribute
变量的类型只能是float
、vec2
、vec3
、vec4
、mat2
、mat3
、mat4
- “逐顶点”的含义:比如,一条线段有两个顶点,这两个坐标就会传递给
uniform
变量可以用在顶点着色器和片元着色器中,只能被声明为全局变量且是只读的- 表示“一致”(非逐顶点/逐片元的,各顶点或各片元共用)的数据,如:变换矩阵等
- 如果顶点着色器和片元着色器中声明了同名的
uniform
变量,那么它就会被两种着色器共享 - 可以是除了数组或结构体之外的任意类型
varying
变量必须是全局变量,它的任务是从顶点着色器向片元着色器传输数据,所以必须在两种着色器中声明同名、同类型的varying
变量- 顶点着色器中赋给
varying
变量的值并不是直接传给了片元着色器的varying
变量,这其中发生了光栅化的过程:根据绘制的图形,对顶点着色器varying
变量进行内插,然后再传递给片元着色器varying
变量 - 正是因为
varying
变量需要被内插,所以需要限制它的数据类型。和attribue
变量一样,只能是float
、vec2
、vec3
、vec4
、mat2
、mat3
、mat4
- 顶点着色器中赋给
顶点着色器和片元着色器中能够容纳的这些存储限定符变量的最大数目与设备有关,可以通过访问内置的全局常量来获取该值(最大数目)。但是,不管设备配置如何,都有一个最小支持数量
变量类型 | 内置全局变量(表示最大数目) | 最小数目 |
---|---|---|
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 |
在 JS 中可以使用 gl.getParameter(gl.MAX_VERTEX_ATTRIBS)
(其他 GLSL ES 内置全局变量同理)访问
还有一个存储限定符是 const
,表示该变量的值不能被改变,所以声明同时必须对它进行初始化,声明之后就不能再去改变它们的值了
精度限定符
GLSL ES 引入了精度限定符,目的是帮助着色器程序提高运行效率,削减内存开支
精度限定符决定了每种数据数值范围和精度,简而言之,高精度的程序需要更大的开销(包括更大的内存和更久的计算时间),而低精度的程序需要的开销则小得多。使用精度限定符,就能精细地控制程序在效果和性能间的平衡
WebGL 程序支持三种精度:
highp
:高精度mediump
:中精度lowp
:低精度
其可以作用在 int
、float
、矢量、矩阵类型:
// 格式为:
// 存储限定符 精度限定符 类型 变量名
// 如:
lowp int idx; // 低精度整型变量
attribute mediump float size; // 中精度浮点型 attribute 变量
highp vec4 color; // 高精度浮点型元素的 vec4 对象
为每个变量都声明精度很繁琐,也可以使用关键字 precision
来声明着色器的默认精度,这行代码必须在顶点着色器或片元着色器的顶部:
// 格式为:
// precision 精度限定符 类型
// 如:
precision mediump float; // 所有浮点数默认为中精度(包括浮点型元素的矢量和矩阵)
precision highp int; // 所有整型数默认为高精度
lowp float size; // 当然之后也可以覆盖默认值
数据类型的默认精度:
着色器类型 | 数据类型 | 默认精度 |
---|---|---|
顶点着色器 | int | highp |
float | highp | |
sampler2D | lowp | |
samplerCube | lowp | |
片元着色器 | int | mediump |
float | 无 | |
sampler2D | lowp | |
samplerCube | lowp |
可以看到,只有片元着色器中的 float
类型没有默认精度,需要手动指定。如果不在片元着色器中限定 float
类型的精度,就会导致如下的编译错误:
Failed to compile shader: ERROR: 0:1 : No precision specified for (float).
注意:
- 在某些 WebGL 环境中,片元着色器可能不支持
highp
精度,如果其支持的话,那么着色器就会定义内置宏GL_FRAGMENT_PRECISION_HIGH
(详见) - 不同精度限定符的数值范围和精度实际上也是与系统环境相关的,可以使用
gl.getShaderPrecisionFormat(shadertype, precisiontype)
函数来检查shadertype
:指定要查询的着色器类型。可以是以下两个值之一:gl.VERTEX_SHADER
:顶点着色器gl.FRAGMENT_SHADER
:片元着色器
precisiontype
:指定要查询的精度类型。可以是以下值之一:gl.LOW_FLOAT
:低精度浮点数gl.MEDIUM_FLOAT
:中精度浮点数gl.HIGH_FLOAT
:高精度浮点数gl.LOW_INT
:低精度整数gl.MEDIUM_INT
:中精度整数gl.HIGH_INT
:高精度整数
- 返回一个
WebGLShaderPrecisionFormat
对象,包含以下属性:rangeMin
:表示可表示的最小有效指数rangeMax
:表示可表示的最大有效指数precision
:表示有效位数
基本类型
int
:整型数,如:-1
、0
、1
float
:单精度浮点数,如:1.0
、-3.14159
bool
:布尔值,true
或false
只有这三种,没有字符串等
GLSL ES 是强类型语言
- 变量在声明时就要确定类型,格式为
<类型> <变量名>
,如:vec4 a_Position
- 函数必须指定参数类型和返回值类型,格式为
<返回值类型> <函数名>(<参数类型> <形参名>, ...) {<函数体>}
,如:void main() {}
- 赋值操作(
=
)时,等号左右两侧的数据类型也必须一样
类型转换
- 转换为整型数
int(float)
:将小数部分删去,如:3.98
转换为3
int(bool)
:true
转换为1
,false
转换为0
- 转换为浮点数
float(int)
:添加小数部分,如:8
转换为8.0
float(bool)
:true
转换为1.0
,false
转换为0.0
- 转换为布尔值
bool(int)
:0
转换为false
,其他转换为true
bool(float)
:0.0
转换为false
,其他转换为true
运算符
矢量和矩阵
- 矢量
vec2
、vec3
、vec4
:具有 2、3、4 个浮点数元素的矢量ivec2
、ivec3
、ivec4
:具有 2、3、4 个整型数元素的矢量bvec2
、bvec3
、bvec4
:具有 2、3、4 个布尔值元素的矢量
- 矩阵
mat2
、mat3
、mat4
:2x2、3x3、4x4 的浮点数元素的矩阵(分别具有 4、9、16 个元素)
矢量构造函数
赋值语句左右类型必须相同(强类型语言):
vec4 a = 1.0; // 报错,赋值语句左右类型不符
GLSL ES 提供了丰富灵活的方式来创建矢量,可以传入数值和矢量:
// 只传一个数值,就会使用这个数值填充矢量的每个分量
vec3 v3a = vec3(1.0); // (1.0, 1.0, 1.0)
// 传入正好数量的数值
vec3 v3b = vec3(1.0, 2.0, 3.0); // (1.0, 2.0, 3.0)
// 传入更多数量的数值,多出的数值会被丢弃
vec3 v3c = vec3(1.0, 2.0, 3.0, 4.0); // (1.0, 2.0, 3.0)
// 但是不能数量大于一,但不够,这样会报错
vec3 v3d = vec3(1.0, 2.0); // 报错
// 可以直接传入矢量,多出的分量会被丢弃,但是不能少,少了报错
vec2 v2 = vec2(v3b); // (1.0, 2.0)
// 可以传入多个矢量
vec4 v4a = vec4(v2, v3a); // (1.0, 2.0, 1.0, 1.0)
// 也可以矢量、数值混用
vec4 v4b = vec4(6.6, v2, 8.8); // (6.6, 1.0, 2.0, 8.8)
矩阵构造函数
GLSL ES 中的矩阵是按列主序的,所以要注意传入值的顺序:
mat4 m4 = mat4(
1.0, 2.0, 3.0,
4.0, 5.0, 6.0,
7.0, 8.0, 9.0
);
// 1.0 4.0 7.0
// 2.0 5.0 8.0
// 3.0 6.0 9.0
向矩阵构造函数中传入矢量和数值,按照列主序使用矢量里的元素值来构造矩阵:
// 使用两个 vec2 来创建 mat2
vec2 v2a = vec2(1.0, 3.0);
vec2 v2b = vec2(2.0, 4.0);
mat2 m2a = mat2(v2a, v2b);
// 1.0 2.0
// 3.0 4.0
// 使用一个 vec4 来创建 mat2
vec4 v4 = vec4(1.0, 3.0, 2.0, 4.0);
mat2 m2b = mat2(v4);
// 1.0 2.0
// 3.0 4.0
// 使用两个浮点数和一个 vec2 来创建 mat2
mat2 m2c = mat2(1.0, 3.0, v2b);
// 1.0 2.0
// 3.0 4.0
// 只传一个数值,就是单位矩阵
mat4 m4a = 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
// 同样如果传入的数值的数量大于一,又没有达到矩阵元素的数量,就会报错
mat4 m4b = mat4(1.0, 2.0, 3.0); // 报错
访问元素
.
运算符
分量名:
x
、y
、z
、w
:用来获取顶点坐标分量r
、g
、b
、a
:用来获取颜色分量s
、t
、p
、q
:用来获取纹理坐标分量
不同类别的分量名可以增强程序的可读性。事实上,任何矢量的 x
、r
、s
分量都会返回第 1 个分量,y
、g
、t
分量都返回第 2 个分量,等等。如果愿意,可以随意地交换使用它们(尽量不要这样做,可读性很差):
vec3 v3 = vec3(1.0, 2.0, 3.0);
float f;
f = v3.x; // 1.0
f = v3.g; // 2.0
f = v3.q; // 报错,vec3 没有第 4 个分量
v3.z = 3.3; // 也可以作为 = 左值
将同一个集合的多个分量名共同置于点运算符后,就可以从矢量中同时抽取出多个分量,这个过程称作混合(swizzling):
vec2 v2;
v2 = v3.xy; // (1.0, 2.0)
v2 = v3.yx; // (2.0, 1.0)
v2 = v3.xx; // (1.0, 1.0)
// 其他类别分量名也可以
v2 = v3.rg; // (1.0, 2.0)
// 但不能跨类别了
v2 = v3.ry; // 报错
// 同样可以作为 = 左值
v3.yx = vec2(2.2, 1.1);
[]
运算符
索引值从 0 开始
vec3 v3 = vec3(1.0, 2.0, 3.0);
float f = v3[2]; // 3.0
mat3 m3 = mat3(
1.0, 2.0, 3.0,
4.0, 5.0, 6.0,
7.0, 8.0, 9.0
);
vec3 v3b = m3[0]; // (1.0, 2.0, 3.0),获取 m4 矩阵的第 1 列
float m11 = m3[1][1]; // 5.0
float m12 = m3[1].z; // 6.0
注意,[]
中只能出现的索引值必须是常量索引值(constant index):
- 整型字面量
- 用
const
修饰的全局变量或局部变量,不包括函数参数 - 循环索引
- 由前述三条中的项组成的表达式
const int idx = 0;
vec3 v3c = m3[idx]; // 同 m3[0]
vec3 v3d = m3[idx + 1]; // 同 m3[1]
int idx2 = 0;
vec3 v3e = m3[idx2]; // 报错:idx2 不是常量索引
运算符
矢量和矩阵不可以使用 >
、<
、>=
、<=
,如果你想比较矢量和矩阵的大小,应该使用内置函数,如 lessThan()
结构体
GLSL ES 支持用户自定义类型,即结构体(structures)。使用关键字 struct
,将已存在的类型聚合到一起,就可以定义为结构体:
struct light { // 定义了结构体类型 light
vec4 color;
vec3 position;
}
light l1, l2; // 声明了 light 类型的变量 l1 和 l2
和 C 语言不同的是,没有必要使用 typedef
关键字来定义结构体,因为结构体的名称会自动成为类型名
此外,为了方便,可以在同一条语句中定义结构体并声明该结构体类型的变量:
struct light { // 定义了结构体类型
vec4 color;
vec3 position;
} l1; // 声明了该结构体类型的变量 l1
赋值和构造
结构体有标准的构造函数,其名称与结构体名一致。构造函数的参数的顺序必须与结构体定义中的成员顺序一致:
l1 = light(vec4(0.0, 1.0, 0.0, 1.0), vec3(8.0, 3.0, 0.0));
// color position
访问成员
使用 .
运算符访问结构体变量的成员:
vec3 position = l1.position;
vec4 color = l1.color;
运算符
当且仅当两个结构体变量所对应的所有成员都相等时,==
运算符才会返回 true
,如果任意某个成员不相等,那么 !=
运算符返回 true
数组
GLSL ES 支持数组类型,但只支持一维数组
数组的长度声明后就不可改变,且必须是大于 0 的整型常量表达式(intergral constant expression),如下定义:
- 整型字面量
- 用
const
修饰的全局变量或局部变量,不包括函数参数 - 由前述两条中的项组成的表达式
float floatArr[4]; // 声明含有 4 个浮点数元素的数值
vec4 vec4Arr[2]; // 声明含有 2 个 vec4 对象元素的数值
int size = 4;
int intArr[size]; // 报错:长度不是整型常量表达式
注意,不能使用 const
限定字来修饰数组本身
数组元素可以通过索引值来访问(索引值从 0 开始):
float f = floatArr[2]; // 取第 3 个元素
只有整型常量表达式 和 uniform 变量可以被用作数组的索引值
此外,与 C 不同,数组不能在声明时被一次性地初始化,而必须显式地对每个元素进行初始化
数组本身只支持 []
运算符,但数组的元素能够参与其自身类型支持的任意运算
取样器
GLSL ES 支持的一种内置类型称为取样器(sampler),用于访问纹理,有两种基本的取样器类型:sampler2D
和 samplerCube
。取样器变量只能是 uniform
变量,或者需要访问纹理的函数(如 texture2D()
)的参数
此外,唯一能赋值给取样器变量的就是纹理单元编号,而且必须使用 WebGL 方法 gl.uniform1i()
来进行赋值
除了 =
、 ==
、!=
,取样器变量不可以作为操作数参与运算
取样器类型变量受到着色器支持的纹理单元的最大数量限制:
着色器 | 表示最大数量的内置常量 | 最小数量 |
---|---|---|
顶点着色器 | const mediump int gl_MaxVertexTextureImageUnits | 0 |
片元着色器 | const mediump int gl_MaxTextureImageUnits | 8 |
其中 mediump
是一个精度限定符
运算符优先级
流程控制:分支和循环
GLSL ES 的分支和循环与 C 的几乎无异
分支
- 只有
if
、if-else
、if-else if-else
- 没有
switch
循环
- 只有
for
- 没有
while
、do-while
for
循环
for (初始化表达式; 条件表达式; 循环步进表达式) {
循环体
}
注意(部分与 C 不同):
- 循环变量只能在初始化表达式中定义
- 只允许有一个循环变量,循环变量只能是
int
或float
类型 - 条件表达式可以为空,代表无限循环
- 条件表达式必须是循环变量与整型常量的比较
- 循环步进表达式必须是(假设
i
是循环变量):i++
、i--
、i+=常量表达式
、i-=常量表达式
- 在循环体内,循环变量不可被赋值
这些限制的存在是为了使编译器能够对 for
循环进行内联展开
continue
、break
、discard
语句
continue
- 中止包含该语句的最内层循环,循环步进表达式,然后执行下一次循环
- “跳过本次循环”
break
- 中止包含该语句的最内层循环,并不再继续执行循环
- “中止当前循环”
discard
:GLSL ES 特有,只能在片元着色器中使用- 表示放弃当前片元直接处理下一个片元
函数
GLSL ES 的函数与 C 的几乎无异
函数定义:
返回值类型 函数名(形参列表) {
函数体
return 返回值
}
函数调用:
函数名(实参列表)
返回值类型 变量名 = 函数名(实参列表)
其中参数列表(形参列表和实参列表)的形式:
参数类型 参数名1, ..., 参数类型 参数名n
注意(部分与 C 不同):
- 可以将自定义的结构体类型指定为返回类型,但是结构体的成员中不能有数组
- 不能在一个函数内部调用它本身,即:不允许递归调用
这些限制也是便于编译器对函数进行内联展开
规范声明
如果函数定义在其调用之后,需要在调用之前先声明该函数的规范
float luma(vec4); // 规范声明
void main() {
// ...
float brightness = luma(color); // luma() 函数在定义之前就被调用了
// ...
}
float luma(vec4 color) {
return 0.2126 * color.r + 0.7162 * color.g + 0.0722 * color.b;
}
参数限定词
GLSL ES 中没有指针,可以为函数参数指定限定词,以控制参数的行为
参数限定词 | 规则 |
---|---|
in | 函数内部可以读写该参数,但函数内部的修改不会影响外部传入的变量 |
const in | 函数内部可以读该参数,但不能修改 |
out | 函数内部可以写该参数,函数内部的修改会影响外部传入的变量(传入的是变量的引用) |
inout | 函数内部会用到变量的初始值,然后修改变量的值,会影响外部传入的变量(传入的是变量的引用) |
无,默认 | 同 in |
比如说,可以给 luma()
函数指定一个 out
参数,让其接收函数的计算结果:
void luma2(in vec4 color, out float brightness) {
brightness = 0.2126 * color.r + 0.7162 * color.g + 0.0722 * color.b;
}
// 函数结果存储在 brightness 中
// 和之前的 brightness = luma(color) 效果相同
内置函数
GLSL ES 提供了很多常用的内置函数,下表概括了 GLSL ES 的内置函数
预处理指令
GLSL ES 支持预处理指令,预处理指令用来在真正编译之前对代码进行预处理,以井号(#
)开头
#if 条件表达式
如果条件表达式为真,执行这里
#else
可以添加 else,也可以不加
#endif
// 注意:没有 #elseif,可以通过嵌套 #if #else 实现相同逻辑
#ifdef 某宏
如果定义了某宏,执行这里
#endif
#ifndef 某宏
如果没有定义某宏,执行这里
#endif
定义宏(和 C 语言不同,GLSL ES 中的宏没有宏参数):
// 宏定义
#define 宏名 宏内容
// 解除宏定义
#undef 宏名
宏的名称可以任意起,只要不和预定义的内置宏名称相同即可:
预定义的内置宏 | 描述 |
---|---|
GL_ES | 在 OpenGL ES 2.0 中定义为 1 |
GL_FRAGMENT_PRECISION_HIGH | 片元着色器支持 highp 精度 |
所以,可以这样使用宏来进行精度限定:
#ifedf GL_ES
#ifedf GL_FRAGMENT_PRECISION_HIGH
precision highp float; // 支持高精度,限定浮点型为高精度
#else
precision mediump float; // 不支持高精度,限定浮点型为中精度
#endif
#endif
使用 #version
来指定着色器使用的 GLSL ES 版本(必须在着色器顶部,在它之前只能有注释和空白):
#version 100
:默认值,使用 GLSL ES 1.00 版本#version 101
:使用 GLSL ES 1.01 版本