01.向量和矩阵

74 阅读5分钟

往期

01.向量和矩阵

02.相机矩阵和透视矩阵的推导过程

向量

向量在图形学中主要的作用是求夹角和平面法线,对应了向量的两种计算方式,点乘和叉乘。

向量的点乘

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。

00_mul.png

位移矩阵

00_translate.png

假设坐标点[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同理,所以位移矩阵就可以这样表示。

缩放矩阵

00_scale.png

假设坐标点[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轴的正方向的夹角,于是有下面公式。

00_rotate.png

于是有了绕z轴逆时针旋转30°的矩阵如下

00_rotate_z.png

那么绕y轴和x轴逆时针旋转30°的矩阵呢,我们看下图中

00_rotate_y.png

把xz和yz变成横竖时,每个轴的朝向,y轴是朝内的,而x轴是和z轴一样,朝外的,所以绕y轴逆时针旋转30°,如果在z和x轴上的旋转来说,是顺时针旋转的30°,那么上述公式中的b相当于变成了-b。

00_rotate_y_y.png

代码实现

我们讲解过程用的是行优先的矩阵,因为我们上学时候的矩阵都是行优先的。但是在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都是对一个轴的旋转。

使用矩阵类完成之前第四章中的示例

效果

01_result.png

代码

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);

总结

本章我们简单介绍了向量和矩阵,以及一些形变矩阵。
然后实现了一个基础的矩阵,仅包含乘法运算及形变的三个矩阵。之后章节开始结合这些矩阵或者衍生新的矩阵方法,实现一些实际的应用效果。

相关代码gitee