经过前面几个章节的学习,相信大家对 GLSL 的部分语法已经熟稔于心了,但我们终究没有对 GLSL 有系统的认知,所以开辟一小节对 GLSL 的常用语法做一个详细介绍。
你能学到什么?
- GLSL 中的数据类型
- GLSL 中的内置变量
- 向量、矩阵的表示与运算。
- GLSL 中的内置函数
GLSL 属于 GPU 层面的编程语言,因此它必须有能力支持图形相关的操作。除了绘制 API,还要支持各种数学运算,主要体现在向量和矩阵。
GLSL 主要是在 C 语言的基础上新增了一些内置变量
、数据类型
和数学函数
,因为是基于 C 语言的拓展,所以在语法规则上和 C 语言基本相同。
变量命名
GLSL 的语法和 C 语言类似,因此 GLSL 的变量命名方式和 C 语言基本一致。但由于 GLSL 新增了一些数据类型、内置属性和保留前缀,所以变量命名除了满足 C 语言的命名规则之外,还要满足 GLSL 的特殊规则:
- 不能以
gl_
作为前缀,gl_ 开头的变量被用于定义 GLSL 的内部变量,这是 GLSL 保留的命名前缀。 - GLSL 的一些保留名称也不能作为变量名称,比如
attribute
、uniform
、varying
等。
数据类型
GLSL 最突出的部分是新增了向量和矩阵相关的数据类型,比如存储向量的容器 vec{n}
,存储四阶矩阵的容器mat4
,2D纹理采样器 sampler2D
,3D纹理采样器samplerCube
等等。下面我们着重介绍向量和矩阵在 GLSL 中的用法。
向量
向量是 GLSL 中很重要的一种数据类型,着色器程序的很多地方都需要用到向量,用来储存包含多个分量的数据,比如颜色信息
、齐次坐标
、法向量
等。
向量按照维度分为2维
、3维
、4维
,按照存储的数据类型分为浮点向量vec{n}
,整型向量ivec{n}
,布尔向量bvec{n}
。
浮点向量
- vec2:存储2个浮点数。
- vec3:存储3个浮点数。
- vec4:存储4个浮点数。
浮点向量的赋值相对浮点数宽松一些,比如在为一个浮点变量赋值的时候,我们必须这样写:
- 正确
float size = 10.0;
像下面这样赋值就会报错,因为类型不匹配,size 是浮点变量,10 是一个整数,变量类型
和要赋值的类型
不匹配。
- 错误
float size = 10;
但是在为向量赋值时,就会宽松一些,比如我们构建一个2维浮点向量,分量都是 1。
我们可以这样写:
- 正确
vec2 texcoords = vec2(1.0, 1.0);
也可以这样写:
vec2 texcoords = vec2(1, 1);
vec 向量类型会自动对元素做类型转换。
之所以讲这些细节,有一个原因是 GLSL 程序的调试有一定局限性,没有特别好用的调试器能够让我们逐行逐变量地进行调试,所以我们只能减少低级失误。
怎样才能减少低级失误?只有夯实基础。但减少失误并不意味着消除,即使我们万般小心,也仍然会出现问题导致编译或者运行异常,这时只能用肉眼排查问题。拥有好的基础,也会促使你很快地找到问题产生的原因。
整型向量
- ivec2:存储2个整数。
- ivec3:存储3个整数。
- ivec4:存储4个整数。
整型向量和浮点向量类似,向量的各个元素都是整型数字,此处不做重复讲解。
布尔向量
- bvec2:存储2个布尔值。
- bvec3:存储3个布尔值。
- bvec4:存储4个布尔值。
布尔向量的各个元素都是布尔值true
或者false
,此处也不做重复讲解。
向量的使用技巧
每个向量我们都可以用 {s、t、p、q}
,{r、g、b、a}
,{x、y、z、w}
来表示。获取各个位置的元素,我们可以使用.
操作符。
比如一个 4 维向量:
vec4 v = vec(1, 2, 3, 4);
那么v.s
、v.r
、v.x
、v[0]
表示的是该向量第 1 个位置的元素。
同理:
v.t
、v.g
、v.y
、v[1]
表示的是该向量第 2 个位置的元素。v.p
、v.b
、v.z
、v[2]
表示的是该向量第 3个位置的元素。v.q
、v.a
、v.w
、v[3]
表示的是该向量第 4 个位置的元素。
除此之外,我们还可以使用这种方式对低维向量赋值,假设我们有一个 4 维向量 v,现在想以 v 的前两个元素创建一个 2 维向量 v1,那么我们可以这样赋值:
vec4 v = vec4(1, 2, 3, 4);
// xyzw 方式赋值
vec2 v1 = v.xy;
// stpq 赋值
vec2 v1 = v.st;
// rgba 赋值
vec2 v1 = v.rg;
// 构造函数式
vec2 v1 = vec2(v.x, v.y);
vec2 v1 = vec2(v.s, v.t);
vec2 v1 = vec2(v.r, v.g);
还可以这样使用:
vec4 v = vec4(1, 2, 3, 4)
vec2 v1 = vec2(v.xx);
通过 v.xx
的方式将 v1
的两个元素设置成 v
的第一个元素值,变成 (1, 1)
。
向量的运算规则
GLSL 中关于向量的另一个重要部分就是运算规则,向量的运算对象分为如下几类:
向量和基础数字类型的运算。
向量和基础数字类型之间的运算比较简单,规则是将数字和向量的各个分量进行运算,并返回新的向量。
假设有一个 4 维向量 v(x, y, z, w)
,浮点数 f
。
向量 v 和 f 之间的加减乘除运算,GLSL 会将各个分量分别和数字 f 进行加减乘除,并返回新的向量 v1。
// 加法
vec4 v1 = v + f = (x + f, y + f, z + f, w + f);
// 减法
vec4 v1 = v - f = (x - f, y - f, z - f, w - f);
// 乘法
vec4 v1 = v * f = (x * f, y * f, z * f, w * f);
// 除法
vec4 v1 = v / f = (x / f, y / f, z / f, w / f);
向量和向量之间的运算
向量和向量之间也可以进行运算,返回一个新的向量,前提是两个向量之间的维度必须相同。
运算规则是两个向量对应位置的元素分别进行运算。
假设向量 v1 (x1, y1, z1, w1),向量 v2(x2, y2, z2, w2),那么有:
// 加法
vec4 v3 = v1 + v2 = (x1 + x2, y1 + y2, z1 + z2, w1 + w2);
// 减法
vec4 v3 = v1 - v2 = (x1 - x2, y1 - y2, z1 - z2, w1 - w2);
// 乘法
vec4 v3 = v1 * v2 = (v1 * v2, y1 * y2, z1 * z2, w1 * w2);
// 减法
vec4 v3 = v1 / v2 = (x1 / x2, y1 / y2, z1 / z2, w1 / w2);
看起来很简单,但是有一点需要注意,向量之间乘法有三种,上面的 * 号乘法规则是为了使用方便。
在数学领域,向量之间还有两种乘法点乘
和叉乘
,具体区别我们在中级进阶 --- 数学:点、向量、矩阵章节详细介绍。
GLSL 中增加了两种内置函数,用来实现点乘
和叉乘
运算,它们分别是 dot
和cross
,使用起来也很简单:
// 点乘
float v3 = dot(v1, v2);
// 叉乘
vec3 v3 = cross(v1, v2);
在计算光照效果时,会经常使用这两个函数。
以上就是向量相关内容,大多数运算规则和线性代数一致。
矩阵
矩阵是 GLSL 中和数学相关的另一块重要内容,在着色器程序中,我们会经常使用矩阵来完成各种变换,比如坐标转换、计算光照时的法向量转换等。
矩阵分类
矩阵按照维度分为二阶、三阶、四阶,其中三阶和四阶矩阵用的较多,我们下面主要讲解四阶矩阵。
四阶矩阵
四阶矩阵,包含 4 行 4 列共 16 个浮点数,在着色器程序中初始化一个四阶矩阵有很多种方式。
- 用 16 个浮点数构造矩阵。
这是最简单最直观的构造方式:
mat4 m = mat4(
1, 2, 3, 4, //第一列
5, 6, 7, 8, //第二列
9, 10, 11, 12, //第三列
13, 14, 15,16 // 第四列
);
- 用 1 个浮点数构造对角线矩阵。
mat4 a = mat4(1.0);
mat4 传入一个浮点数构造出的矩阵,对角线上的值都是 1.0:
[
1.0, 0, 0, 0,
0, 1.0, 0, 0,
0, 0, 1.0, 0,
0, 0, 0, 1.0
]
- 利用列向量构造
四阶矩阵可以理解为四个列向量组合而成,所以 GLSL 提供了利用向量构造矩阵的方法。
//第一列
vec4 c0 = vec4(1, 2, 3, 4);
//第二列
vec4 c1 = vec4(5, 6, 7, 8);
//第三列
vec4 c2 = vec4(1, 2, 3, 4);
//第四列
vec4 c3 = vec4(5, 6, 7, 8);
mat4 m = mat4(c0, c1, c2, c4);
- 向量与浮点数混合构造。 当然除了纯数字构造、纯向量构造,GLSL 也允许向量和数字混合构造:
vec4 c0 = vec4(1, 2, 3, 4);
vec4 c1 = vec4(5, 6, 7, 8);
vec4 c2 = vec4(1, 2, 3, 4);
mat4 m = mat4(c0, c1, c2, 5, 6, 7, 8);
观察上面的构造方式,我们发现,mat4构造函数 中传入的数字只要满足 16 个就可以构造成四阶矩阵。
所以,大家还可以想到利用二维、三维向量进行构造的方式。
总之,GLSL 为矩阵提供的构造方式很灵活,毕竟,在 GLSL 中我们用的最多的除了向量
就是矩阵
了。
- 矩阵运算
乘法运算
我们用的最多的就是乘法运算了,在GLSL 中,矩阵乘法用 *
来表示,但大家要记住,由于 GLSL 中矩阵采用的是列主序
,所以,矩阵和向量相乘时,要置在乘号左侧,如下:
mat4 m = mat4(1.0);
vec4 v1 = m * vec4(1, 2, 3, 4);
还有一些其他的矩阵运算方法,比如转置
、求逆
等:
mat4 m0 = mat4(1.0);
// 转置
mat4 m1 = transpose(m0);
// 求逆
mat4 m2 = inverse(m0)
内置变量
内置属性在前面章节中已经学过一些了,比如大家很熟悉的 gl_Position
、gl_FragColor
等,除此之外,还有一些不常用的属性。
顶点着色器
- gl_Position:顶点坐标。
- gl_PointSize:点的尺寸。
- gl_Normal:顶点法线。
片元着色器
- gl_FragColor,当前片元的颜色,类型 vec4。
- gl_FragCoord,屏幕像素的x,y,z,1 / w。
- gl_FragDepth,片元的最终深度值,在后面的深度测试用到,在片元着色器中我们无法修改
x, y
值,但是可以修改z
值。
内置函数
GLSL 内置了很多数学函数,下面列举一些经常用到的。
向量函数
函数 | 作用 |
---|---|
cross | 计算两个向量的叉积 |
dot | 计算向量的点积。 |
normalize | 归一化向量,返回一个和原向量方向相同,但是长度为1的单位向量。 |
reflect | 根据入射向量和法线向量,计算出反射向量。 |
length | 计算向量的长度 |
distance | 计算两个向量之间的距离。 |
常用数学函数
函数 | 作用 |
---|---|
abs | 将某个数的绝对值 |
floor | 返回不大于某个数的最大整数。 |
round | 四舍五入值 |
ceil | 返回大于某个数的最小整数。 |
fract | 返回浮点数的小数部分 |
mod | 取模 |
min | 返回两个数中比较小的数 |
max | 返回两个数中比较大的数 |
三角函数
GLSL 提供了很多三角函数,方便我们进行角度求值:
函数 | 作用 |
---|---|
radians | 将角度(如90度)转化为弧度(PI/2)。 |
degrees | 将弧度(如PI / 2)转化为角度(90 度)。 |
sin | 求弧度的正弦 |
cos | 求弧度的余弦 |
tan | 求弧度的正切 |
asin | 根据正弦值求对应的弧度 |
acos | 根据余弦值求对应的弧度 |
atan | 根据正切值求对应的弧度 |
以上就是常用的三角函数,GLSL 还提供了一些更复杂但是不常用的函数,此处不一一列举了,大家感兴趣的话可以查查 GLSL 语法规范。
限定符
限定符在之前章节也已经陆续讲过了,再次做个总结。
attribute
attribute 变量只能定义在顶点着色器
中,它的作用是接收 JavaScript 程序传递过来的与顶点
有关的数据,比如在之前程序中定义的顶点颜色
、法线
、坐标
等,它们是顶点的属性。
也就是说,如果有一类数据,它是跟随顶点而存在的,每个顶点所对应的数据不尽相同,那么我们就需要用 attribute 限定符定义变量。
uniform
uniform 用来修饰全局变量,它既可以在顶点着色器中定义,也可以在片元着色器中定义,用来接收与顶点无关的数据。
比如,在之前程序中,我们定义了一个 uniform 变量 u_Matrix
,它用来接收 JavaScript 中传递过来的 模型视图投影矩阵,该数据与顶点无关,也就是每个顶点共用变换矩阵,所以我们应该用 uniform 修饰该变量。
varying
varying变量一般是成对定义的,即在顶点着色器中定义,在片元着色器中使用。它所修饰的变量在传递给片元着色器之前会进行插值化处理。
回顾
以上是对常用 GLSL 语法的一个总结,事实上还有一些比较罕见的函数我们没有列举,大家在用到的时候可以查阅 WebGL 官方文档。
接下来的章节我们开始 3D 数学知识的学习,因涉及到一些推导,建议线性代数基础不好的同学准备好纸笔。