WebGL学习05-初识矩阵

3,003 阅读14分钟

前言

Vertex Shader可以通过uniform变量接受来自CPU的数据,数据可以是floatintvec2vec3mat4等类型,本文将会详细介绍mat前缀的数据类型,这类数据我们称之为矩阵。

矩阵的定义

矩阵的概念来自与线性代数,可以理解为一个二维数组,比如一个mat4类型的变量,它就代表这个矩阵是一个4x4的二维数组

第一列第二列第三列第四列
第一行15913
第二行261014
第二行371115
第二行481216
矩阵的尺寸可以是任意的,不过在WebGL中,我们一般只使用2x2,3x3,4x4这三种尺寸的矩阵。
矩阵有自己的一套计算法则,有些类似于基本类型的计算法则,有些则有很大的差异

1. 加法

加法和基本类型的计算方法类似,将各个元素分别相加组成新矩阵,要注意的是只有相同尺寸的矩阵才可以相加,得出的矩阵尺寸和它们相同,比如2个2x2的矩阵

15
26

加上

315
02

等于

1 + 35 + 15
2 + 06 + 2

2. 减法

减法和加法类似,只是将元素相加改成元素相减

15
26

减去

315
02

等于

1 - 35 - 15
2 - 06 - 2

3. 乘法

这里介绍两种乘法,一种是数字乘以矩阵,另一种是矩阵乘以矩阵 数字乘以矩阵比较简单,就是数字分别和矩阵所有的元素相乘,得到相同尺寸的新矩阵,比如

6

乘以

315
02

等于

6 * 36 * 15
6 * 06 * 2

矩阵乘以矩阵要求比较严格,假设矩阵A和B相乘,那么矩阵A的的列数必须等于矩阵B的行数。 比如矩阵A有3行2列,那么矩阵B需要有2行,列数可以随意。定义下面两个矩阵

矩阵A

第一列第二列
第一行a11a21
第二行a12a22
第三行a13a23

矩阵B

第一列第二列第三列
第一行b11b21b31
第二行b12b22b32

矩阵AxB结果的尺寸是A的行数,B的列数,假设结果中的每个元素为m(i,j),i是元素所在列,j是元素所在行,那么元素的值等于A中第j行和B中第i列元素的乘积之和。以上面定义的矩阵为例,AxB的结果是

第一列第二列第三列
第一行a11 * b11 + a21 * b12a11 * b21 + a21 * b22a11 * b31 + a21 * b32
第二行a12 * b11 + a22 * b12a12 * b21 + a22 * b22a12 * b31 + a22 * b32
第三行a13 * b11 + a23 * b12a13 * b21 + a23 * b22a13 * b31 + a23 * b32

矩阵的乘法满足结合律 (AB)C = A(BC),分配律 (A + B)C = AC + BC,但是要注意的是AB和BA是不同的,在做矩阵乘法的时候要注意矩阵的相乘顺序。

4. 逆矩阵

逆矩阵和数字的倒数概念类似,一个矩阵乘以它的逆矩阵将会得到单位矩阵,不是所有的矩阵都有逆矩阵。以下面2x2的矩阵为例

第一列第二列
第一行12
第二行31

它的逆矩阵是

第一列第二列
第一行-1/52/5
第二行3/5-1/5

通过上面矩阵的乘法公式将两者相乘可以得到

第一列第二列
第一行1 * -1/5 + 2 * 3/51 * 2/5 + 2 * -1/5
第二行3 * -1/5 + 1 * 3/53 * 2/5 + 1 * -1/5

也就是

第一列第二列
第一行10
第二行01

单位矩阵的特点是列和行索引相同的元素皆为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的矩阵为例

第一列第二列
第一行a11a21
第二行a12a22
第三行a13a23

将其转置可以得到一个2x3的矩阵

第一列第二列第三列
第一行a11a12a13
第二行a21a22a23

变换矩阵以及图形学意义

在WebGL中,我们通常使用矩阵来进行顶点的变换,比如平移,缩放,旋转,透视等,GPU可以更好的处理密集型的计算,使用矩阵和内置函数进行计算,比我们自己使用普通的数据计算方式效率更高,下面就来介绍平移,缩放,旋转三种变换矩阵。

1. 平移

平移一个顶点需要三个参数 dx,dy,dz,表示三个方向上的偏移量,新的顶点坐标可以表示为x + dx,y + dy,z + dz,可以使用下面的变换矩阵进行平移的操作

第一列第二列第三列第四列
第一行1000
第二行0100
第三行0010
第四行dxdydz1

将顶点坐标构造一个1 x 4的矩阵和上面的矩阵相乘,为了两者能够相乘,必须保证它们的列数和行数相同,所以增加一列作为补充

第一列第二列第三列第四列
第一行xyz1

将后者和前者相乘,可以得到

第一列第二列第三列第四列
第一行x * 1 + y * 0 + z * 0 + 1 * dxx * 0 + y * 1 + z * 0 + 1 * dyx * 0 + y * 0 + z * 1 + 1 * dzx * 0 + y * 0 + z * 0 + 1 * 1
也就是
第一列第二列第三列第四列
第一行x + dxy + dyz + dz1

如果希望通过前者乘以后者的方式计算平移,可以将变换矩阵和顶点坐标构造的矩阵进行转置

转置后的变换矩阵

第一列第二列第三列第四列
第一行100dx
第二行010dy
第二行001dz
第二行0001

转置后的顶点坐标矩阵

第一列
第一行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,可以使用下面的变换矩阵进行缩放的操作

第一列第二列第三列第四列
第一行sx000
第二行0sy00
第三行00sz0
第四行0001

将顶点坐标构造一个1 x 4的矩阵和上面的矩阵相乘

第一列第二列第三列第四列
第一行xyz1

将后者和前者相乘,可以得到

第一列第二列第三列第四列
第一行x * sx + y * 0 + z * 0 + 1 * 0x * 0 + y * sy + z * 0 + 1 * 0x * 0 + y * 0 + z * sz + 1 * 0x * 0 + y * 0 + z * 0 + 1 * 1

也就是

第一列第二列第三列第四列
第一行x * sxy * syz * sz1

类似平移矩阵,通过将上述2个矩阵转置,就可以通过前者相乘后者的方式计算了。

3. 旋转

旋转相对于平移和缩放要复杂一些,因为先绕x轴转在绕y轴转和先绕y轴转在绕x轴转是不同的,所以当我们试图使用绕x轴旋转角度rx,绕y轴旋转角度ry,绕z轴旋转角度rz,来描述顶点的旋转时,可以假设一个前提,顶点先按照x轴旋转,再绕y轴旋转,再绕z轴旋转。下面先分别列出3个轴旋转的旋转变换矩阵

绕x轴旋转

第一列第二列第三列第四列
第一行1000
第二行0cos(rx)sin(rx)0
第三行0-sin(rx)cos(rx)0
第四行0001

将顶点坐标构造一个1 x 4的矩阵和上面的矩阵相乘

第一列第二列第三列第四列
第一行xyz1

将后者和前者相乘,可以得到

第一列第二列第三列第四列
第一行x * 1 + y * 0 + z * 0 + 1 * 0x * 0 + y * cos(rx) - z * sin(rx) + 1 * 0x * 0 + y * sin(rx) + z * cos(rx) + 1 * 0x * 0 + y * 0 + z * 0 + 1 * 1

也就是

第一列第二列第三列第四列
第一行xy * cos(rx) - z * sin(rx)y * sin(rx) + z * cos(rx)1

这其实就是二维平面的点旋转公式,绕x轴旋转相当于在yz这个二维平面内旋转点。绕y,z轴的矩阵如下

第一列第二列第三列第四列
第一行cos(ry)0-sin(ry)0
第二行0100
第三行sin(ry)0cos(ry)0
第四行0001
第一列第二列第三列第四列
第一行cos(rz)sin(rz)00
第二行-sin(rz)cos(rz)00
第三行0010
第四行0001

假设绕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的平移矩阵

例子完整项目代码,在chapter2section2-0目录下

glMatrix支持vec2vec3vec4mat2mat3mat4等类型,每种类型的使用方式是一致的,必须先通过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);

使用程序运行总时间给偏移量一些变化,这里通过了vec3set方法来设置它的值。 再使用这个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是第四列,按照上面的表格形式,它应该是这样的

第一列第二列第三列第四列
第一行1001
第二行0102
第三行0013
第四行0001

基于glMatrix的缩放矩阵

例子完整项目代码,在chapter2section2-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的旋转矩阵

例子完整项目代码,在chapter2section2-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的矩阵混合

例子完整项目代码,在chapter2section2-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点公转,为了让效果更明显,可以先使用缩放矩阵对三角形进行缩放。