往期
向量
向量在图形学中主要的作用是求夹角和平面法线,对应了向量的两种计算方式,点乘和叉乘。
向量的点乘
ab向量点乘的几何意义是b在a上的投影,计算结果为一个数值,XaXb + YaYb + Za*Zb。
有cosA = a·b/|a||b|,其中|a|代表了a向量的长度sqrt(Xa² + Ya²)
向量的叉乘
ab向量叉乘的几何意义是ab组成平面的法线,由右手螺旋法则a向b握拳,大拇指朝向及法线朝向
计算结果为一个新的向量(YaZb - YbZa, ZaXb - XaZb, XaYb - YaXb)
矩阵
webgl中涉及到的矩阵主要为4阶矩阵,及4x4的矩阵。我们主要讲解一下n阶矩阵在webgl中的作用——形变。
将(1, 1)沿x轴向右移动1个单位,变成(1, 2)。将(1, 1)顺时针旋转90°,变成(1, -1)。。。
这些过程从数学上,都是可以通过矩阵计算去得到的。
矩阵相乘
我们先看矩阵相乘的结果是什么,比如说m1xn1的矩阵和m2xn2的矩阵相乘,结果为m1xn2的矩阵,且n1必须等于m2。
位移矩阵
假设坐标点[1, 2, 3, 1]
要沿x轴移动x距离,y轴移动y距离,z轴移动z距离,就是需要变成[1+x, 2+y, 3+z, 1]
,那么位移矩阵就表示如上
1x4矩阵乘4x4矩阵,结果为1x4矩阵,且第一列的值为1乘以第一列的每行之和,那么原本的y和z不能影响结果,所以为0,最终只能在第四行设置x。
y和z同理,所以位移矩阵就可以这样表示。
缩放矩阵
假设坐标点[1, 2, 3, 1]
要放大a倍,就是x要变成a,y变成2a,z变成3a,缩放矩阵就表示如上。
旋转矩阵
旋转矩阵就相对复杂一点,试想一下,如果[1, 2, 3, 1]
要绕z轴逆时针旋转30°,那么对于z值来说,是不变的,变得只有x和y。
那么我们就可以把z轴去掉,将xy变成二维的坐标系,观察xy的变化.
求新的xy,就是新的位置的线段和x轴的正方向的夹角,于是有下面公式。
于是有了绕z轴逆时针旋转30°的矩阵如下
那么绕y轴和x轴逆时针旋转30°的矩阵呢,我们看下图中
把xz和yz变成横竖时,每个轴的朝向,y轴是朝内的,而x轴是和z轴一样,朝外的,所以绕y轴逆时针旋转30°,如果在z和x轴上的旋转来说,是顺时针旋转的30°,那么上述公式中的b相当于变成了-b。
代码实现
我们讲解过程用的是行优先的矩阵,因为我们上学时候的矩阵都是行优先的。但是在webgl中,是以列优先的,所以上面所讲的内容都需要颠倒一下。
下面都是从列优先的角度去实现的
矩阵初始化
class Matrix4 {
elements;
constructor() {
this.elements = new Float32Array([
1,0,0,0,
0,1,0,0,
0,0,1,0,
0,0,0,1
]);
}
setIdentity() {
const e = this.elements;
e[0] = 1; e[4] = 0; e[8] = 0; e[12] = 0;
e[1] = 0; e[5] = 1; e[9] = 0; e[13] = 0;
e[2] = 0; e[6] = 0; e[10] = 1; e[14] = 0;
e[3] = 0; e[7] = 0; e[11] = 0; e[15] = 1;
return this;
}
}
初始化比较简单,就是定义一个单位矩阵,然后在定义了一个setIdentity初始化方法,将矩阵的值再次变成单位矩阵
矩阵相乘
multiply(other) {
let ai0, ai1, ai2, ai3;
const e = this.elements;
const a = this.elements;
const b = other.elements;
for (let i = 0; i < 4; i++) {
ai0=a[i]; ai1=a[i+4]; ai2=a[i+8]; ai3=a[i+12];
e[i] = ai0 * b[0] + ai1 * b[1] + ai2 * b[2] + ai3 * b[3];
e[i+4] = ai0 * b[4] + ai1 * b[5] + ai2 * b[6] + ai3 * b[7];
e[i+8] = ai0 * b[8] + ai1 * b[9] + ai2 * b[10] + ai3 * b[11];
e[i+12] = ai0 * b[12] + ai1 * b[13] + ai2 * b[14] + ai3 * b[15];
}
return this;
}
首先要记得webgl是列优先,e[0] e[4] e[8] e[12]
代表的是第一行
e和a代表了左边的矩阵,b代表了右边的矩阵,ai0-ai3记录了左边矩阵的每行原始值
每次计算同样也是左边的每行乘右边的每列得出每行的新值。
位移矩阵
setTranslate(x, y, z) {
const e = this.elements;
e[0] = 1; e[4] = 0; e[8] = 0; e[12] = x;
e[1] = 0; e[5] = 1; e[9] = 0; e[13] = y;
e[2] = 0; e[6] = 0; e[10] = 1; e[14] = z;
e[3] = 0; e[7] = 0; e[11] = 0; e[15] = 1;
return this;
};
translate(x, y, z) {
const e = this.elements;
e[12] += e[0] * x + e[4] * y + e[8] * z;
e[13] += e[1] * x + e[5] * y + e[9] * z;
e[14] += e[2] * x + e[6] * y + e[10] * z;
e[15] += e[3] * x + e[7] * y + e[11] * z;
return this;
};
setTranslate就和上章讲的一样,第四行设置x,y,z的移动量即可。
但是我们不可能只做一次位移,大多情况会在旋转或缩放后位移,于是有了translate方法,可以实现旋转或缩放后做位移,而不需要用两个矩阵相称,内部实现逻辑就相当于当前矩阵乘上了单位的位移矩阵的结果。
缩放矩阵
setScale(x, y, z) {
const e = this.elements;
e[0] = x; e[4] = 0; e[8] = 0; e[12] = 0;
e[1] = 0; e[5] = y; e[9] = 0; e[13] = 0;
e[2] = 0; e[6] = 0; e[10] = z; e[14] = 0;
e[3] = 0; e[7] = 0; e[11] = 0; e[15] = 1;
return this;
}
scale(x, y, z) {
const e = this.elements;
e[0] *= x; e[4] *= y; e[8] *= z;
e[1] *= x; e[5] *= y; e[9] *= z;
e[2] *= x; e[6] *= y; e[10] *= z;
e[3] *= x; e[7] *= y; e[11] *= z;
return this;
};
同样的,缩放矩阵也定义了两个方法,用于初始化并缩放和基于当前矩阵缩放
旋转矩阵
setRotate(angle, x, y, z) {
angle = Math.PI * angle / 180;
const e = this.elements;
const s = Math.sin(angle);
const c = Math.cos(angle);
if (0 !== x && 0 === y && 0 === z) {
// Rotation around X axis
if (x < 0) {
s = -s;
}
e[0] = 1; e[4] = 0; e[ 8] = 0; e[12] = 0;
e[1] = 0; e[5] = c; e[ 9] =-s; e[13] = 0;
e[2] = 0; e[6] = s; e[10] = c; e[14] = 0;
e[3] = 0; e[7] = 0; e[11] = 0; e[15] = 1;
} else if (0 === x && 0 !== y && 0 === z) {
// Rotation around Y axis
if (y < 0) {
s = -s;
}
e[0] = c; e[4] = 0; e[ 8] = s; e[12] = 0;
e[1] = 0; e[5] = 1; e[ 9] = 0; e[13] = 0;
e[2] =-s; e[6] = 0; e[10] = c; e[14] = 0;
e[3] = 0; e[7] = 0; e[11] = 0; e[15] = 1;
} else if (0 === x && 0 === y && 0 !== z) {
// Rotation around Z axis
if (z < 0) {
s = -s;
}
e[0] = c; e[4] =-s; e[ 8] = 0; e[12] = 0;
e[1] = s; e[5] = c; e[ 9] = 0; e[13] = 0;
e[2] = 0; e[6] = 0; e[10] = 1; e[14] = 0;
e[3] = 0; e[7] = 0; e[11] = 0; e[15] = 1;
}
return this;
};
rotate(angle, x, y, z) {
return this.multiply(new Matrix4().setRotate(angle, x, y, z));
};
我们先看rotate,顺便去理解上面的translate和scale,其实translate和scale和内部实现是和rotate一致的,只不过translate和scale可以缩减计算步骤。
本质就是当前矩阵乘以一个旋转矩阵。
再看setRotate,每个if都是对一个轴的旋转。
使用矩阵类完成之前第四章中的示例
效果
代码
const modelMatrix = new Matrix4();
modelMatrix.rotate(45, 0, 1, 0)
modelMatrix.rotate(45, 1, 0, 0)
gl.uniformMatrix4fv(gl.getUniformLocation(gl.program, 'v_ModelMatrix'), false, modelMatrix.elements);
总结
本章我们简单介绍了向量和矩阵,以及一些形变矩阵。
然后实现了一个基础的矩阵,仅包含乘法运算及形变的三个矩阵。之后章节开始结合这些矩阵或者衍生新的矩阵方法,实现一些实际的应用效果。