前言
GLSL ES(Open Graphics Library Shading Language for Embedded Systems )是一门着色器语言,是在GLSL的基础上,删除和简化一部分功能之后形成的。用于在嵌入式和移动系统(包括游戏主机、手机、家电和车辆)上渲染高级 2D 和 3D 图形。它包含一个经过良好定义的桌面 OpenGL 子集,适合低功耗设备,并为软件和图形加速硬件之间提供了灵活且强大的接口。
本章将从以下几个方面介绍GLSL ES
- 数据、变量和变量类型
- 矢量、矩阵、结构体、数组、采样器(纹理)
- 运算、程序流、函数
- attribute、uniform和verying变量
- 精度限定词
- 预处理和指令
想要学习GLSL可以安装glsl-canvas插件,该扩展通过提供Show glslCanvas命令在VSCode中打开GLSL着色器的实时WebGL预览。
1 数据值类型(数值和布尔值)
****GLSL支持两种数据值类型
数值类型:GLSL ES支持整型数和浮点数,没有小数点的值被认为整型数,有小数点的被认为浮点数
布尔值类型:GLSL ES支持布尔值类型,包括true和false两个布尔常量
2 变量
****变量命名规范
- 只包括a-z、A-Z、0-9和下划线_
- 变量名的首字符不能是数字
- 不能是关键字,保留字
- 不能以gl_、webgl_或_webgl_开头,这些前缀以及被OpenGL ES保留了
以下是 GLSL ES 中的保留字和关键字的表格:
关键字
attribute | bool | break | bvec2 | bvec3 | bvec4 |
---|---|---|---|---|---|
const | continue | discard | do | else | false |
float | for | hightp | if | in | inout |
int | invariant | ivec2 | ivec3 | ivec4 | lowp |
mat2 | mat3 | mat4 | medium | out | precision |
return | sampler2D | sampler3D | struct | true | uniform |
varying | vec2 | vec3 | vec4 | void | while |
保留字
asm | cast | class | default |
---|---|---|---|
double | dvec2 | dvec3 | devc4 |
enum | extern | external | fixed |
flat | fvec2 | fvec3 | fvec4 |
goto | half | hvec2 | hvec3 |
hvec4 | inline | input | interface |
long | namespace | noinline | output |
packed | public | sampler1D | sampler1DShadow |
sampler2DRect | sampler2DRectShadow | sampler2DShadow | sampler3D |
sampler3DRect | short | sizeof | static |
superp | switch | template | this |
typedef | union | unsigned | using |
volatile |
GLSL ES是一门强类型语言,不像JavaScript,使用var、let或者const来声明变量,GLSL ES要求具体的指明变量类型。
3 矢量和矩阵
GLSL ES支持矢量和矩阵,这两种数据类型非常适合用来处理计算机图形,比如前面的片段着色器的颜色值计算,就用到了矩阵和矢量,以及高阶变换中也用到了矩阵计算顶点位置
矢量和矩阵类型
类别 | 数据类型 | 描述 |
---|---|---|
矢量 | vec2、vec3、vec4 | 具有2、3、4个浮点数元素的矢量 |
ivec2、ivec3、ivec4 | 具有2、3、4个整型数元素的矢量 | |
bvec2、bvec3、bvec4 | 具有2、3、4个布尔值元素的矢量 | |
矩阵 | mat2、mat3、mat4 | 2阶、三阶、四阶浮点数矩阵 |
3.1 矢量构造函数
glsl
vec3 v3 = vec3(1.0, 2.0, 3.0)
vec2 v3 = vec2(v3) // 使用v3前两个元素
vec4 v4 = vec4(1.0) // 将v4设为 (1.0,1.0,1.0,1.0)
vec4 v4b = vec4(v2,v4) // 矢量组合,使用v2,和v4矢量填充
3.2 矩阵构造函数
向矩阵构造函数传入矩阵的 每一个元素的数值来构造矩阵,传入顺序必须是列主序(真实矩阵的转置格式)的。
glsl
mat4 m4 = mat4(
1.0, 2.0, 3.0, 4.0,
5.0, 6.0, 7.0, 8.0,
9.0, 10.0, 11.0, 12.0,
13.0, 14.0, 15.0, 16.0
)
向矩阵中传入一个或多个矢量
glsl
vec2 v2_1 = vec2(1.0,3.0)
vec2 v2_2 = vec2(2.0,4.0)
// 1.0 2.0
// 3.0 4.0
mat2 mat2_1 = mat2(v2_1,v2_2)
vec4 v4 = vec4(1.0,3.0,2.0,4.0)
// 1.0 2.0
// 3.0 4.0
mat2 mat2_2 = mat2(v4)
向矩阵构造函数中传入矢量和数值,剩余空间数值用矢量补齐
glsl
// 1.0 2.0
// 3.0 4.0
mat2 mat2_2 = mat2(1.0, 3.0 ,v2_2)
向矩阵构造函数传入单个数值
glsl
mat4 m4 = mat4( 1.0) // 会使用1.0自动填充整个矩阵
3.2.1 访问元素
为了访问矢量和矩阵中的元素,可以使用 . 或者[]运算符,下面将分节描述。
3.2.2 点运算符
在矢量变量名后接点运算符(.),然后接上分量名,就可以访问矢量元素了。
分量名
类别 | 描述 |
---|---|
x,y,z,w | 用来获取顶点坐标分量 |
r,g,b,a | 用来获取颜色分量 |
s,t,p,q | 获取纹理坐标分量 |
示例
glsl
highp vec3 v3 = vec3(1.0,2.0,3.0);
highp float f;
f = v3.x; // 设f为1.0
f = v3.y; // 设f为2.0
f = v3.z; // 设f为3.0
// x、r和s虽然名称不同,但都访问的是第一个分量
f = v3.r; // 设f为1.0
f = v3.s; // 设f为1.0
// 超出矢量长度的分量访问会报错
f = v3.w;
将多个分量名置于点运算符后,就可以从矢量中抽取出多个分量,这个过程称为混合
glsl
vec2 v2;
v2 = v3.xy; //设置 v2为(1.0,2.0)
v2 = v3.yz; //设置 v2为(2.0,3.0)
v2 = v3.xz; //设置 v2为(1.0,3.0)
3.2.3 []运算符
除了点运算符,还可以使用[]运算符通过数组下标来访问矢量或者矩阵的元素。注意,矩阵中的元素任然是按照列主序读取的。
示例
glsl
mat4 m4 = mat4(
1.0, 2.0, 3.0, 4.0,
5.0, 6.0, 7.0, 8.0,
9.0, 10.0, 11.0, 12.0,
13.0, 14.0, 15.0, 16.0
)
// 获取m4第一列元素 [1.0,2.0,3.0,4.0]
vec4 v4 = m4[0]
// 连续使用两个[]运算符可以获取某列第几个元素
vec4 v4 = m4[1][2] // 7.0
// []也可以结合矢量运算符使用
vec4 v4 = m4[2].y // 10.0
但是[]的使用有一个限制,[]中出现的索引值必须是一个常量,数值常量或者通过const声明的常量。
3.2.3 运算符
矩阵和矢量的运算符与基本类型运算符很类似,但是对于比较运算符只能用 == 和!=,
运算符 | 运算 | 适用数据类型 |
---|---|---|
* | 乘法 | 适用于vec[234]和mat[234] |
/ | 除法 | |
+ | 加法 | |
- | 减法 | |
++ | 自增 | 适用于vec[234]和mat[234] |
-- | 自减 | |
= | 赋值 | 适用于vec[234]和mat[234] |
+=、-=、*=、/= | 运算赋值 | 适用于vec[234]和mat[234] |
==、!= | 比较运算符 | 适用于vec[234]和mat[234],如果两个操作数的分量都相等赋予true,否则false |
示例
矢量和浮点数运算
glsl
v3b = v3a + f
// b3b.x = v3a.x + f
// b3b.y = v3a.y + f
// b3b.z = v3a.z + f
矢量运算
glsl
v3c = v3a +v3b
// b3c.x = v3a.x + v3b.x
// b3c.y = v3a.y + v3b.y
// b3c.z = v3a.z + v3b.z
矩阵和浮点数运算
glsl
m3b = m3a * f
// m3b.x = m3a[0].x * f;
// m3b.y = m3a[0].y * f;
// m3b.z = m3a[0].z * f;
矩阵右乘矢量
glsl
v3b = m3a * v3a
// v3b.x = m3a[0].x * v3a.x + m3a[1].x * v3a.y + m3a[2].x * v3a.z
// v3b.y = m3a[0].y * v3a.x + m3a[1].y * v3a.y + m3a[2].y * v3a.z
矩阵右乘矢量结果还是一个矢量。
矩阵左乘矢量
glsl
v3b = v3a * m3a
// v3b.x = m3a[0].x * v3a.x + m3a[0].x * v3a.y + m3a[0].x * v3a.z
// v3b.y = m3a[1].y * v3a.x + m3a[1].y * v3a.y + m3a[1].y * v3a.z
矩阵相乘
glsl
m3c = m3a * m3b
// m3c[0].x = m3a[0].x * m3b[0].x + m3a[1].x * m3b[0].y+ m3a[2].x * m3b[0].z
就是矩阵乘法
4 结构体
GLSL ES支持用户自定义的类型,**结构体,**使用关键字struct,将已存在的类型聚合在一起,就可以定义为结构体,例如
glsl
struct light{
vec4 color
vec3 position
}
light l1
light l2
上面这段代码声明了结构体light,它包含两个成员:color变量和position变量
此外为了方便还可以在定义结构体时,同时声明该结构体类型的变量:
glsl
struct light{
vec4 color
vec3 position
}l1
4.1 赋值和构造
构造体有标准的构造函数,其名称和结构体名一致。构造函数的参数的顺序必须与结构体中定义的成员顺序一致。
glsl
light l1 = light(vec4(0.0,1.0,2.0,3.0), vec3(0.0,1.0,2.0))
4.2 访问成员
在结构体变量后跟点运算符,然后加上成员名,就可以访问变量成员
glsl
vec4 = l1.color
4.3 运算符
结构体成员可以参与自身类型支持的任何运算,但是结构体本身只支持两种运算:赋值(=)和比较(==和!=),但是赋值和比较运算符不适用于含有数组与纹理成员的结构体。对于比较运算符,只有对于结构体内变量的所有成员都相等才为true。
4.4 数组
GLSL ES支持数组类型,与JavaScript数组类型不同,GLSL ES只支持一维数组,且数组对象不支持pop()、push()等操作,声明数组很简单,只需要在变量名后加上中括号([])和数组的长度。
glsl
float floatArray[4] // 声明含有4个浮点数的数组
数组长度必须是大于0的整型常量表达式,数组元素可以通过索引值来访问,和c语言一样,索引值从0开始,与JS和c不同的是,数组不能在声明时被一次性的初始化,而必须显示的对每个元素进行初始化
glsl
vec4 vec4Array[4]
vec4Array[0] = vec4(1.0,2.0,3.0,4.0)
数组本身只支持[]运算符,但是数组元素能够参与自身元素类型所支持的任意运算。
5 取样器(纹理)
GLSL ES支持一种名为取样器(sampler)的内置类型。我们必须通过该类型变量访问纹理。GLSL ES有两种基本的取样器类型:sampler2D和samplerCube。取样器变量只能是uniform变量,或者需要访问纹理的函数,如前面提到的texture2D函数参数,如:
glsl
uniform sampler2D u_Sampler;
此外唯一能赋值给取样器变量的就是纹理单元编号,且必须使用gl.uniformli()进行赋值,比如
glsl
// 将0号纹理传递给着色器
gl.uniform1i(u_Sampler, 0);
但是取样器类型变量受到着色器支持的纹理单元的最大数量限制。
5 continue、break和discard语句
continue和break跟其它语言定义一致,这里不做具体介绍。discard,它只能在片元着色器中起作用,表示放弃当前片元的处理,它就会保证片段不会被进一步处理,所以就不会进入颜色缓冲,discard应用。
应用举例:
一个场景就是我们需要一颗草(正方形图片)作为纹理,贴在一个2D四边形(Quad)上,然后将这个四边形放到场景中。如果不做处理的话,这颗草会完全贴在四边形上,且会覆盖四边形原有颜色(纹理是一个四边形,除了显示草的部分,其余部分都是透明的),当添加像草这样的植被到场景中时,不希望看到草的方形图像,而是只显示草的部分,并能看透图像其余的部分,也就是四边形自身颜色。因此就可以应用丢弃(Discard)显示纹理中透明部分的片段,不将这些片段存储到颜色缓冲中,可以通过判断处理当前片元时,颜色值的透明度小于0.1就舍弃。
6 函数
与js不同,GLSL ES对于函数的定义更加接近C语言,
glsl
返回类型 函数名(参数){
函数计算
return 返回值
}
如果函数定义在其调用之后,必须事先声明:
glsl
float luma(vec4); // 规范声明
main(){
...
float brightness = luma(color)
}
float luma (vec4 color){
return ...
}
6.1 参数限定词
在GLSL ES中可以为函数参数指定参数限定词,以控制参数的行为,我们可以将函数定义成:
- 传递给函数的
- 将要在函数中被赋值的
- 即是传递给函数的,也是将要在函数中被赋值的
类别 | 规则 | 描述 |
---|---|---|
in | 向函数传入值 | 参数传入函数,在函数中可以使用参数的值,也可以修改其值。但函数内部的修改不会影响传入的变量 |
const in | 向函数传入值 | 参数传入函数,在函数中可以使用参数的值,但不能被修改 |
out | 在函数中被赋值,并被传出 | 传入变量的引用,若在函数内部被修改,会影响到函数外部传入的变量,但在函数调用之前,该参数的值是未定义的 |
inout | 传入函数,同时在函数中被赋值,并被传出 | 传入变量的引用,若在函数内部被修改,会影响到函数外部传入的变量, 在函数调用之前,该参数可以有一个初始值 |
无 | 向函数传入值 | 同in |
举例:
out
glsl
void myFunction(out vec3 color) {
color = vec3(1.0, 0.0, 0.0); // 赋值给 color
}
inout
glsl
void myFunction(inout vec3 color) {
color += vec3(0.0, 1.0, 0.0); // 读取并修改 color
}
7 内置函数
以下是 GLSL ES 中的各种函数分类及其简要描述:
7.1 角度函数
radians(degrees)
: 将角度转换为弧度。degrees(radians)
: 将弧度转换为角度。
7.2 三角函数
sin(x)
: 返回x
的正弦值。cos(x)
: 返回x
的余弦值。tan(x)
: 返回x
的正切值。asin(x)
: 返回x
的反正弦值。acos(x)
: 返回x
的反余弦值。atan(y, x)
: 返回y/x
的反正切值。
7.3 指数函数
pow(x, y)
: 返回x
的y
次幂。exp(x)
: 返回e
的x
次幂。log(x)
: 返回x
的自然对数。exp2(x)
: 返回2
的x
次幂。log2(x)
: 返回x
的以2
为底的对数。
7.4 通用函数
abs(x)
: 返回x
的绝对值。min(x, y)
: 返回x
和y
中的最小值。max(x, y)
: 返回x
和y
中的最大值。clamp(x, minVal, maxVal)
: 将x
限制在minVal
和maxVal
之间。mix(x, y, a)
: 返回x
和y
的线性插值。
7.5 几何函数
length(v)
: 返回向量v
的长度。normalize(v)
: 返回单位向量,即将v
归一化。dot(x, y)
: 计算两个向量的点积。cross(x, y)
: 计算两个三维向量的叉积。
7.6 矩阵函数
mat2()
: 创建 2x2 矩阵。mat3()
: 创建 3x3 矩阵。mat4()
: 创建 4x4 矩阵。transpose(m)
: 返回矩阵m
的转置。inverse(m)
: 返回矩阵m
的逆矩阵。
7.7 矢量函数
vec2(x, y)
: 创建一个 2D 向量。vec3(x, y, z)
: 创建一个 3D 向量。vec4(x, y, z, w)
: 创建一个 4D 向量。dot(v1, v2)
: 计算向量的点积。cross(v1, v2)
: 计算向量的叉积(仅适用于 3D 向量)。
7.8 纹理查询函数
texture(sampler, coord)
: 从纹理中获取颜色值。texture2D(sampler, coord)
: 适用于二维纹理的采样。textureCube(sampler, coord)
: 从立方体纹理中获取颜色值。
这些函数为 GLSL ES 提供了丰富的数学运算和纹理处理能力,使得图形编程更加灵活和高效。
8 全局变量和局部变量
与js一样,GLSL ES也有全局变量和局部变量的概念,在函数外部声明的变量就是全局变量,在函数内部声明的变量就是局部变量。
9 存储限定词
9.1 const 变量
与js一样,通过const限定词声明的变量就不可更改,而且必须初始化。
9.2 attribute 变量
attribute变量只能出现在顶点着色器中,而且只能被声明为全局变量,被用来表示逐顶点的信息,这个逐顶点的意思是,比如线段有两个顶点(1.0,2.0,3.0)和(4.0,5.0,6.0),这两个坐标都会传递给attribute变量,而线段上的其它点比如中点,并没有传递给attribute变量,也未被顶点着色器处理过。attribute变量的类型只能是float、vec2、vec3、vec4、mat2、mat3、mat4。
顶点着色器中能够容纳的attribute变量的最大数目与设备有关,可以通过访问内置的全局常量来获取该值。在 GLSL 中,attribute
、uniform
和 varying
变量的数量限制因具体实现而异,通常取决于所用的 GPU 和驱动程序。以下是一些一般的指导原则:
在 GLSL ES 中,attribute
、uniform
和 varying
变量的数量限制通常为:
- Attribute 变量
- 限制 :通常在 8 到 16 之间。可以通过
gl.getIntegerv(gl.MAX_VERTEX_ATTRIBS, &value)
查询具体的限制。
- Uniform 变量
- 限制 :通常在 128 到 256 之间,具体数量可通过
gl.getIntegerv(gl.MAX_VERTEX_UNIFORM_VECTORS, &value)
查询。注意,这个值是以向量为单位的。
- Varying 变量
- 限制 :一般在 8 到 64 之间,具体数量可通过
gl.getIntegerv(gl.MAX_VARYING_VECTORS, &value)
查询。
查询限制的代码示例
可以在 WebGL 中使用以下代码查询这些限制:
javascript
const gl = canvas.getContext('webgl');
const maxAttributes = gl.getIntegerv(gl.MAX_VERTEX_ATTRIBS);
const maxUniformVectors = gl.getIntegerv(gl.MAX_VERTEX_UNIFORM_VECTORS);
const maxVaryingVectors = gl.getIntegerv(gl.MAX_VARYING_VECTORS);
console.log(`Max Attributes: ${maxAttributes}`);
console.log(`Max Uniform Vectors: ${maxUniformVectors}`);
console.log(`Max Varying Vectors: ${maxVaryingVectors}`);
这些限制可能会因硬件和驱动程序的不同而有所变化,因此在具体应用中最好进行动态查询。
总结
为了确保程序的兼容性,最好在代码中动态查询这些限制,而不是依赖于硬编码的值。这样可以确保你的着色器能够在不同的平台和设备上正常工作。
9.3 uniform变量
uniform变量可以用在顶点着色器和片元着色器中,且必须是全局变量,uniform变量是只读的,它可以是处理结构体和数组之外的任意类型。如果顶点着色器和片元着色器中声明了同名的uniform变量,那么它就会被这两种着色器共享。uniform包含了一致(非逐顶点/逐片元,各顶点或个片元共用)的数据,js应该向着色器传递该类数据。比如,变换矩阵就不是逐顶点的,而是所有顶点公用的,所以它在着色器中是uniform变量。
9.4 varying 变量
varying变量必须是全局变量,它的任务是从顶点着色器向片元着色器传输数据。我们可以在两种着色器中声明同名、同类型的varying类型变量。与attribute变量一样,varying变量的类型只能是float、vec2、vec3、vec4、mat2、mat3、mat4。需要注意的是,顶点着色器赋给片元着色器中的值,并不是直接传递给片元着色器中的varying变量,这其中发生了光栅化的过程:根据绘制的图形,对前者(顶点着色器varying变量)进行内插,然后再传递给后者(片元着色器varying变量)。正是应为varying变量需要内擦所以需要限制它的数据类型
10 精度限定词
GLSL ES 引入了精度限定词,目的是帮助着色器程序提高运行效率,消减内存开支。精度限定词用来表示每种数据具有的精度(比特数),高精度的程序需要更大的开销(包括内存和程序运行时间)。精度限定词是可选的。
默认值
glsl
#ifdef GL_ES
precision mediump float;
#endif
为什么引入精度限定词?WebGL应用程序是运行在不同硬件平台上。肯定存在某些情况下需要在低精度下运行程序,以提高内存使用效率,减少性能开销,以及更重要的降低能耗,延长移动设备的电池续航能力。但是在低精度下,WebGL程序运行结果比较粗糙或不准确,必须在程序效果和性能之间做出平衡。
精度限定词
精度限定词 | 描述 | 数值范围和精度(float,int) | |
---|---|---|---|
highp | 高精度,顶点着色器的最低精度 | (-2^62^,2^62^)精度2^-16^ | (-2^16^,2^16^) |
mediump | 中精度,介于高精度和低精度之间,片元着色器的最低精度 | (-2^14^,2^14^)精度2^-10^ | (-2^10^,2^10^) |
lowp | 低精度,低于中精度,可以表示所有颜色 | (-2,2)精度2^-8^ | (-2^8^,2^8^) |
示例
glsl
mediump float size //中精度浮点型变量
highp vec4 position //高精度浮点型vec4对象
为每一种变量声明精度很麻烦,可以通过precision来声明着色器精度,这行代码必须在顶点着色器或者片元着色器顶部。
其格式如下:
glsl
precision 精度限定词 类型名称
这句代码表示,在着色器中,某种类型的变量其默认精度由精度限定词指定。
着色器会指定数据类型的默认精度
着色器类型 | 数据类型 | 默认精度 |
---|---|---|
顶点着色器 | int | highp |
float | highp | |
sampler2D | lowp | |
samplerCube | lowp | |
片元着色器 | int | mediump |
float | 无 | |
sampler2D | lowp | |
samplerCube | lowp |
11 预处理指令
GLSL ES支持预处理指令。预处理指令用来在真正编译之前对代码进行预处理,都已#号开始。
比如:
glsl
#ifdef GL_ES
precision mediump float;
#endif
这段代码检查是否已经定义了GL_ES宏,如果是,那就执行#ifdef和#endif之间的代码:
GLSL ES中可能用到的三种预处理指令
glsl
#if 条件表达式
If 如果条件表达式为真,执行这里
#ednif
#ifdef 宏
如果定义了宏执行这里
#ednif
#ifndef 宏
如果没有定义宏执行这里
#ednif
12 宏
在 GLSL ES 中,宏的定义主要通过 #define
指令来实现。宏可以用来简化代码、提高可读性和重用性。下面是关于如何定义和使用宏的一些细节:
定义宏
可以使用 #define
语句来定义宏。语法如下:
glsl
#define 宏名 替换文本
示例
以下是一个简单的示例,展示如何在 GLSL ES 中定义和使用宏:
glsl
#define PI 3.14159265359
#define MAX(a, b) ((a) > (b) ? (a) : (b))
void main() {
float angle = 45.0 * PI / 180.0; // 使用宏 PI
float x = 5.0;
float y = 10.0;
float maxValue = MAX(x, y); // 使用宏 MAX
}
注意事项
- 宏展开: 在编译时,所有的宏都会被展开。这意味着,在代码中使用宏时,编译器会将其替换为定义时指定的文本。
- 参数宏 : 在宏定义中可以使用参数(如
MAX(a, b)
),这样可以创建更灵活的宏。 - 避免副作用: 使用参数宏时要小心,因为传递的参数会在宏展开时被多次计算,可能导致意想不到的副作用。
结束宏定义
可以使用 #undef
来取消定义一个宏。例如:
glsl
#undef PI