前言
Vertex Shader
可以通过uniform
变量接受来自CPU的数据,数据可以是float
,int
,vec2
,vec3
,mat4
等类型,本文将会详细介绍mat
前缀的数据类型,这类数据我们称之为矩阵。
矩阵的定义
矩阵的概念来自与线性代数,可以理解为一个二维数组,比如一个mat4
类型的变量,它就代表这个矩阵是一个4x4的二维数组
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | 1 | 5 | 9 | 13 |
第二行 | 2 | 6 | 10 | 14 |
第二行 | 3 | 7 | 11 | 15 |
第二行 | 4 | 8 | 12 | 16 |
矩阵的尺寸可以是任意的,不过在WebGL中,我们一般只使用2x2,3x3,4x4这三种尺寸的矩阵。 | ||||
矩阵有自己的一套计算法则,有些类似于基本类型的计算法则,有些则有很大的差异 |
1. 加法
加法和基本类型的计算方法类似,将各个元素分别相加组成新矩阵,要注意的是只有相同尺寸的矩阵才可以相加,得出的矩阵尺寸和它们相同,比如2个2x2的矩阵
1 | 5 |
2 | 6 |
加上
3 | 15 |
0 | 2 |
等于
1 + 3 | 5 + 15 |
2 + 0 | 6 + 2 |
2. 减法
减法和加法类似,只是将元素相加改成元素相减
1 | 5 |
2 | 6 |
减去
3 | 15 |
0 | 2 |
等于
1 - 3 | 5 - 15 |
2 - 0 | 6 - 2 |
3. 乘法
这里介绍两种乘法,一种是数字乘以矩阵,另一种是矩阵乘以矩阵 数字乘以矩阵比较简单,就是数字分别和矩阵所有的元素相乘,得到相同尺寸的新矩阵,比如
6
乘以
3 | 15 |
0 | 2 |
等于
6 * 3 | 6 * 15 |
6 * 0 | 6 * 2 |
矩阵乘以矩阵要求比较严格,假设矩阵A和B相乘,那么矩阵A的的列数必须等于矩阵B的行数。 比如矩阵A有3行2列,那么矩阵B需要有2行,列数可以随意。定义下面两个矩阵
矩阵A
第一列 | 第二列 | |
---|---|---|
第一行 | a11 | a21 |
第二行 | a12 | a22 |
第三行 | a13 | a23 |
矩阵B
第一列 | 第二列 | 第三列 | |
---|---|---|---|
第一行 | b11 | b21 | b31 |
第二行 | b12 | b22 | b32 |
矩阵AxB结果的尺寸是A的行数,B的列数,假设结果中的每个元素为m(i,j),i是元素所在列,j是元素所在行,那么元素的值等于A中第j行和B中第i列元素的乘积之和。以上面定义的矩阵为例,AxB的结果是
第一列 | 第二列 | 第三列 | |
---|---|---|---|
第一行 | a11 * b11 + a21 * b12 | a11 * b21 + a21 * b22 | a11 * b31 + a21 * b32 |
第二行 | a12 * b11 + a22 * b12 | a12 * b21 + a22 * b22 | a12 * b31 + a22 * b32 |
第三行 | a13 * b11 + a23 * b12 | a13 * b21 + a23 * b22 | a13 * b31 + a23 * b32 |
矩阵的乘法满足结合律 (AB)C = A(BC)
,分配律 (A + B)C = AC + BC
,但是要注意的是AB和BA是不同的,在做矩阵乘法的时候要注意矩阵的相乘顺序。
4. 逆矩阵
逆矩阵和数字的倒数概念类似,一个矩阵乘以它的逆矩阵将会得到单位矩阵,不是所有的矩阵都有逆矩阵。以下面2x2的矩阵为例
第一列 | 第二列 | |
---|---|---|
第一行 | 1 | 2 |
第二行 | 3 | 1 |
它的逆矩阵是
第一列 | 第二列 | |
---|---|---|
第一行 | -1/5 | 2/5 |
第二行 | 3/5 | -1/5 |
通过上面矩阵的乘法公式将两者相乘可以得到
第一列 | 第二列 | |
---|---|---|
第一行 | 1 * -1/5 + 2 * 3/5 | 1 * 2/5 + 2 * -1/5 |
第二行 | 3 * -1/5 + 1 * 3/5 | 3 * 2/5 + 1 * -1/5 |
也就是
第一列 | 第二列 | |
---|---|---|
第一行 | 1 | 0 |
第二行 | 0 | 1 |
单位矩阵的特点是列和行索引相同的元素皆为1,其他元素皆为0,任何矩阵乘以或者被乘以单位矩阵,得出的结果还是它本身。 假设A'是A的逆矩阵,那么可以得出B x A x A' = B,这个公式常用来将矩阵中的某个成分给剔除,如果想要将B x A中的B剔除,需要注意我们不能使用B x A x B'的方式,应该使用B' x B x A,因为矩阵的乘法是不满足交换律的,AxB 不等于 BxA。
5. 矩阵转置
由于矩阵是一个二维数组,我们可以对其旋转,将行变成列,列变成行,以下面3x2的矩阵为例
第一列 | 第二列 | |
---|---|---|
第一行 | a11 | a21 |
第二行 | a12 | a22 |
第三行 | a13 | a23 |
将其转置可以得到一个2x3的矩阵
第一列 | 第二列 | 第三列 | |
---|---|---|---|
第一行 | a11 | a12 | a13 |
第二行 | a21 | a22 | a23 |
变换矩阵以及图形学意义
在WebGL中,我们通常使用矩阵来进行顶点的变换,比如平移,缩放,旋转,透视等,GPU可以更好的处理密集型的计算,使用矩阵和内置函数进行计算,比我们自己使用普通的数据计算方式效率更高,下面就来介绍平移,缩放,旋转三种变换矩阵。
1. 平移
平移一个顶点需要三个参数 dx,dy,dz
,表示三个方向上的偏移量,新的顶点坐标可以表示为x + dx,y + dy,z + dz
,可以使用下面的变换矩阵进行平移的操作
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | 1 | 0 | 0 | 0 |
第二行 | 0 | 1 | 0 | 0 |
第三行 | 0 | 0 | 1 | 0 |
第四行 | dx | dy | dz | 1 |
将顶点坐标构造一个1 x 4的矩阵和上面的矩阵相乘,为了两者能够相乘,必须保证它们的列数和行数相同,所以增加一列作为补充
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | x | y | z | 1 |
将后者和前者相乘,可以得到
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | x * 1 + y * 0 + z * 0 + 1 * dx | x * 0 + y * 1 + z * 0 + 1 * dy | x * 0 + y * 0 + z * 1 + 1 * dz | x * 0 + y * 0 + z * 0 + 1 * 1 |
也就是 |
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | x + dx | y + dy | z + dz | 1 |
如果希望通过前者乘以后者的方式计算平移,可以将变换矩阵和顶点坐标构造的矩阵进行转置
转置后的变换矩阵
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | 1 | 0 | 0 | dx |
第二行 | 0 | 1 | 0 | dy |
第二行 | 0 | 0 | 1 | dz |
第二行 | 0 | 0 | 0 | 1 |
转置后的顶点坐标矩阵
第一列 | |
---|---|
第一行 | x |
第二行 | y |
第三行 | z |
第四行 | 1 |
通过矩阵乘法公式将前者乘以后者得到
第一列 | |
---|---|
第一行 | 1 * x + 0 * y + 0 * z + dx * 1 |
第二行 | 0 * x + 1 * y + 0 * z + dy * 1 |
第三行 | 0 * x + 0 * y + 1 * z + dz * 1 |
第四行 | 0 * x + 0 * y + 0 * z + 1 * 1 |
也就是
第一列 | |
---|---|
第一行 | x + dx |
第二行 | y + dy |
第三行 | z + dz |
第四行 | 1 |
2. 缩放
缩放一个顶点需要三个参数 sx,sy,sz
,表示三个方向上的缩放量,新的顶点坐标可以表示为x * sx,y * sy,z * sz
,可以使用下面的变换矩阵进行缩放的操作
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | sx | 0 | 0 | 0 |
第二行 | 0 | sy | 0 | 0 |
第三行 | 0 | 0 | sz | 0 |
第四行 | 0 | 0 | 0 | 1 |
将顶点坐标构造一个1 x 4的矩阵和上面的矩阵相乘
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | x | y | z | 1 |
将后者和前者相乘,可以得到
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | x * sx + y * 0 + z * 0 + 1 * 0 | x * 0 + y * sy + z * 0 + 1 * 0 | x * 0 + y * 0 + z * sz + 1 * 0 | x * 0 + y * 0 + z * 0 + 1 * 1 |
也就是
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | x * sx | y * sy | z * sz | 1 |
类似平移矩阵,通过将上述2个矩阵转置,就可以通过前者相乘后者的方式计算了。
3. 旋转
旋转相对于平移和缩放要复杂一些,因为先绕x轴转在绕y轴转和先绕y轴转在绕x轴转是不同的,所以当我们试图使用绕x轴旋转角度rx
,绕y轴旋转角度ry
,绕z轴旋转角度rz
,来描述顶点的旋转时,可以假设一个前提,顶点先按照x轴旋转,再绕y轴旋转,再绕z轴旋转。下面先分别列出3个轴旋转的旋转变换矩阵
绕x轴旋转
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | 1 | 0 | 0 | 0 |
第二行 | 0 | cos(rx) | sin(rx) | 0 |
第三行 | 0 | -sin(rx) | cos(rx) | 0 |
第四行 | 0 | 0 | 0 | 1 |
将顶点坐标构造一个1 x 4的矩阵和上面的矩阵相乘
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | x | y | z | 1 |
将后者和前者相乘,可以得到
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | x * 1 + y * 0 + z * 0 + 1 * 0 | x * 0 + y * cos(rx) - z * sin(rx) + 1 * 0 | x * 0 + y * sin(rx) + z * cos(rx) + 1 * 0 | x * 0 + y * 0 + z * 0 + 1 * 1 |
也就是
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | x | y * cos(rx) - z * sin(rx) | y * sin(rx) + z * cos(rx) | 1 |
这其实就是二维平面的点旋转公式,绕x轴旋转相当于在yz这个二维平面内旋转点。绕y,z轴的矩阵如下
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | cos(ry) | 0 | -sin(ry) | 0 |
第二行 | 0 | 1 | 0 | 0 |
第三行 | sin(ry) | 0 | cos(ry) | 0 |
第四行 | 0 | 0 | 0 | 1 |
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | cos(rz) | sin(rz) | 0 | 0 |
第二行 | -sin(rz) | cos(rz) | 0 | 0 |
第三行 | 0 | 0 | 1 | 0 |
第四行 | 0 | 0 | 0 | 1 |
假设绕x,y,z轴的旋转矩阵为Rx,Ry,Rz,顶点构造的1 x 4矩阵为V,那么绕x,y,z轴旋转的公式可以表示为V * Rx * Ry * Rz,如果将所有矩阵转置,那么可以表示为Rz * Ry * Rx * V。除了旋转矩阵还可以使用由四个数字组成的四元数(Quaternion)来表示三维的旋转,它和旋转矩阵可以使用公式一一对应。
使用glMatrix库操作矩阵
接下来我们使用glMatrix库来创建矩阵,实践变换矩阵在WebGL中的应用。
这里使用三角形绘制的例子为初始项目,增加glMatrix引用
yarn add @types/gl-matrix
为了能够使用gl-matrix
的方法,需要在使用的文件中导入它
import 'gl-matrix';
修改Vertex Shader
,增加一个mat4
类型的uniform
,后续将glMatrix创建的矩阵传递给这个uniform
,从而控制顶点的位置
let vertexShaderCode =
"attribute vec4 position;"+
"uniform mat4 transform;"+
"void main() {" +
"gl_Position = transform * position;" +
"}";
基于glMatrix的平移矩阵
例子完整项目代码,在
chapter2
,section2-0
目录下
glMatrix支持vec2
,vec3
,vec4
,mat2
,mat3
,mat4
等类型,每种类型的使用方式是一致的,必须先通过create
方法创建对象,在通过其他方法改变对象的值,可以直接将对象当作Float32Array
类型传递给WebGL API。
为了创建平移矩阵,首先创建一个矩阵mat4
类型的对象,再通过identity
方法将其设置为单位矩阵
var translateMatrix = mat4.create();
mat4.identity(translateMatrix);
接下来将矩阵设置成为平移矩阵,在此之前,需要准备一个vec3
类型的变量表示平移的偏移量
var translateVec = vec3.create();
vec3.set(translateVec, Math.sin(elapsedTime / 1000.0), Math.cos(elapsedTime / 1000.0), 0);
使用程序运行总时间给偏移量一些变化,这里通过了vec3
的set
方法来设置它的值。
再使用这个vec3
来创建平移矩阵
mat4.translate(translateMatrix, translateMatrix, translateVec);
translate
这个方法是将第二个参数的矩阵通过第三个参数平移后得到的新矩阵赋值给第一个参数,在这里就是将单位矩阵平移translateVec
的距离得到新矩阵。
最后通过uniform
API设置这个矩阵到Vertex Shader
let uniformLoc = gl.getUniformLocation(program, "transform");
gl.uniformMatrix4fv(uniformLoc, false, translateMatrix);
我们可以打印一个mat4
的数据看看是否和上面的理论描述一致
var matrixStr = "";
var counter = 0;
for (let idx in translateMatrix) {
matrixStr += translateMatrix[idx] + ", "
counter++;
if (counter >= 4) {
counter = 0;
matrixStr += '\n';
}
}
console.log(matrixStr);
得到的结果是
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
1, 2, 3, 1
根据glMatrix的文档,它的mat4
是列优先的数据结构,也就是说0~3是第一列,4~7是第二列,8~11是第三列,12~15是第四列,按照上面的表格形式,它应该是这样的
第一列 | 第二列 | 第三列 | 第四列 | |
---|---|---|---|---|
第一行 | 1 | 0 | 0 | 1 |
第二行 | 0 | 1 | 0 | 2 |
第三行 | 0 | 0 | 1 | 3 |
第四行 | 0 | 0 | 0 | 1 |
基于glMatrix的缩放矩阵
例子完整项目代码,在
chapter2
,section2-1
目录下
为了创建缩放矩阵,首先创建一个矩阵mat4
类型的对象,再通过identity
方法将其设置为单位矩阵
var scaleMatrix = mat4.create();
mat4.identity(scaleMatrix);
接下来将矩阵设置成为缩放矩阵,在此之前,需要准备一个vec3
类型的变量表示缩放量
var scaleVec = vec3.create();
vec3.set(scaleVec, (Math.sin(elapsedTime / 1000.0) + 1.0) * 0.5, (Math.cos(elapsedTime / 1000.0) + 1.0) * 0.5, 0);
再使用这个vec3
来创建缩放矩阵
mat4.scale(scaleMatrix, scaleMatrix, scaleVec);
scale
这个方法是将第二个参数的矩阵通过第三个参数缩放后得到的新矩阵赋值给第一个参数
最后通过uniform
API设置这个矩阵到Vertex Shader
let uniformLoc = gl.getUniformLocation(program, "transform");
gl.uniformMatrix4fv(uniformLoc, false, scaleMatrix);
基于glMatrix的旋转矩阵
例子完整项目代码,在
chapter2
,section2-2
目录下
为了创建旋转矩阵,首先创建一个矩阵mat4
类型的对象,再通过identity
方法将其设置为单位矩阵
var rotMatrix = mat4.create();
mat4.identity(rotMatrix);
接下来将矩阵设置成为旋转矩阵,在此之前,需要准备一个vec3
类型的变量表示旋转轴
var rotAxis = vec3.create();
vec3.set(rotAxis, 0, 0, 1);
这里的0,0,1
表示绕z轴旋转,WebGL的z轴是从里到外为正方向的,所以绕z轴旋转就是常见的2D旋转,再使用这个vec3
来创建旋转矩阵
mat4.rotate(rotMatrix, rotMatrix, elapsedTime / 1000.0, rotAxis);
elapsedTime / 1000.0
表示绕rotAxis
这个轴旋转的弧度
最后通过uniform
API设置这个矩阵到Vertex Shader
let uniformLoc = gl.getUniformLocation(program, "transform");
gl.uniformMatrix4fv(uniformLoc, false, rotMatrix);
基于glMatrix的矩阵混合
例子完整项目代码,在
chapter2
,section2-3
目录下
上面分开讲解了三种变换矩阵,如果想要复杂的运动效果,应该怎么做呢?比如一边旋转,一边缩放,一边移动,这就要用到矩阵的乘法运算了。 先准备三个矩阵,分别是旋转,缩放和平移矩阵
var rotAxis = vec3.create();
vec3.set(rotAxis, 0, 0, 1);
var rotMatrix = mat4.create();
mat4.identity(rotMatrix);
mat4.rotate(rotMatrix, rotMatrix, elapsedTime / 1000.0, rotAxis);
var scaleVec3 = vec3.create();
vec3.set(scaleVec3, (Math.sin(elapsedTime / 1000.0) + 1.0) * 0.5, (Math.cos(elapsedTime / 1000.0) + 1.0) * 0.5, 1);
var scaleMatrix = mat4.create();
mat4.identity(scaleMatrix);
mat4.scale(scaleMatrix, scaleMatrix, scaleVec3);
var translateVec = vec3.create();
vec3.set(translateVec, Math.sin(elapsedTime / 1000.0), 0, 0);
var translateMatrix = mat4.create();
mat4.identity(translateMatrix);
mat4.translate(translateMatrix, translateMatrix, translateVec);
由于glMatrix的矩阵是列优先结构,如果想要先旋转,再缩放,再移动,我们需要保持 translateMatrix * scaleMatrix * rotMatrix
的相乘顺序,用glMatrix的方法表示就是
var transform = mat4.create();
mat4.multiply(transform, translateMatrix, scaleMatrix);
mat4.multiply(transform, transform, rotMatrix);
如果你希望先平移,再旋转,再缩放,则需要调整顺序为rotMatrix * scaleMatrix * translateMatrix
,用glMatrix的方法表示就是
var transform = mat4.create();
mat4.multiply(transform, rotMatrix, scaleMatrix);
mat4.multiply(transform, transform, translateMatrix);
读者可以自己尝试并理解这种写法的效果。
总结
本节主要介绍了矩阵的基本概念,什么是变换矩阵,如果使用glMatrix库创建变换矩阵并赋值给Vertex Shader
,掌握好这些概念,对于后面的学习以及制作复杂的3D效果都有着很大的帮助。
练习
- 尝试自己使用Float32Array构造平移,缩放和旋转矩阵,并传递给
Vertex Shader
- 思考如何让三角形绕自己的中心点旋转的同时以0.6的半径绕0,0,0点公转,为了让效果更明显,可以先使用缩放矩阵对三角形进行缩放。