WebGL第二十一课:矩阵乘法的非数学理解法(适合不想搞数学的小伙伴)| 8月更文挑战

577 阅读5分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

本文标题:WebGL第二十一课:矩阵乘法的非数学理解法(适合不想搞数学的小伙伴)| 8月更文挑战

引子

    # 每个方程式都会使书的销量减少一半。
                             ---- 霍金

前面几次课,不得不用这些严谨的数学推导来表达矩阵以及矩阵乘法。

不喜欢数学的小伙伴肯定是看不下去的,那么既想学习WebGL,又不想了解这些乏味的公式,那怎么办呢?

将矩阵看成一个函数

一个向量,经过左乘一个矩阵,变成另一个向量。这种形式跟我们的函数非常像,我们就直接这么理解矩阵就好了。

例如:

function 拉伸函数(输入向量) {
    return 拉伸矩阵 * 输入向量;
}

上面的伪代码是不是清晰的代表了这个概念:

无论传入什么向量,你都会得到一个拉伸之后的向量。

我们再定义一个旋转函数:

function 旋转函数(输入向量) {
    return 旋转矩阵 * 输入向量;
}

那么上面的旋转函数就是说:

无论传入什么向量,你都会得到一个旋转之后的向量。

一个向量经过多个矩阵函数

假如我们有一个需求,一个向量,先拉伸一下,然后再旋转一下,怎么办。

如果不熟悉矩阵数学表达法的小伙伴,肯定就懵了。

但是根据矩阵函数理解法,那么就很简单:

var A;// 一个初始的输入向量

A = 拉伸函数(A);

A = 旋转函数(A);

经过上面两次函数调用,我们就会得到一个先被拉伸,再被旋转的新向量。

多个矩阵合并

我们仔细观察一下上面的两个代码,把他们进行合并:

A' = 旋转矩阵 * (拉伸矩阵 * A)

A' = 旋转矩阵 * 拉伸矩阵 * A

A' = (先拉伸再旋转矩阵) * A

也就是说:

先拉伸再旋转矩阵 = 旋转矩阵 * 拉伸矩阵

根据上式, 我们可以定义如下代码:

function 先拉伸再旋转函数(输入向量){
    return 先拉伸再旋转矩阵 * 输入向量;
}

也就是一句话:

如果你有一个向量,你想将这个向量翻过来,倒过去,把他跟一堆矩阵搞来搞去的话,你可以:

先把这一堆矩阵合并(就是乘起来),最后把这个向量扔给这个矩阵,最后得到你想要的向量。

综合例子

有一个向量A(xy)\left(\begin{array}{cc} x\\ y\end{array}\right)

我们想把它先拉伸,x变成 a倍, y变成 b倍;

然后向把他旋转 α弧度。

请写出一个函数,可以进行以上操作。

假设 A = [x,y]; // 就是一个数组
function 先拉伸再旋转函数(A){
    /// 请给出具体实现代码
}
先解决矩阵的合并

我们把拉伸的矩阵拿出来:

[a00b]\begin{bmatrix} a & 0 \\ 0 & b \end{bmatrix}

我们然后把旋转的矩阵拿出来:

[cos(α)sin(α)sin(α)cos(α)]\begin{bmatrix} cos(α) & -sin(α) \\ sin(α) & cos(α) \end{bmatrix}

因为是先拉伸再旋转,所以合并之后的矩阵应该是:

[cos(α)sin(α)sin(α)cos(α)]\begin{bmatrix} cos(α) & -sin(α) \\ sin(α) & cos(α) \end{bmatrix} * [a00b]\begin{bmatrix} a & 0 \\ 0 & b \end{bmatrix}

上面两个矩阵相乘的话,有兴趣的小伙伴可以自己手算一下,精通矩阵乘法的小伙伴,其实可以口算出来:

[cos(α)sin(α)sin(α)cos(α)]\begin{bmatrix} cos(α) & -sin(α) \\ sin(α) & cos(α) \end{bmatrix} * [a00b]\begin{bmatrix} a & 0 \\ 0 & b \end{bmatrix} = [acos(α)bsin(α)asin(α)bcos(α)]\begin{bmatrix} a*cos(α) & -b*sin(α) \\ a*sin(α) & b*cos(α) \end{bmatrix}

那最后的算法就是:

[acos(α)bsin(α)asin(α)bcos(α)]\begin{bmatrix} a*cos(α) & -b*sin(α) \\ a*sin(α) & b*cos(α) \end{bmatrix} * (xy)\left(\begin{array}{cc} x\\ y\end{array}\right)

再解决矩阵乘以向量

矩阵乘以向量,可以化成,向量的线性组合,而向量的线性组合包含了:

  • 向量数乘
  • 向量相加

两个操作

我们先实现向量数乘的函数:

// A = [x, y]
// return [nx, ny]
function 向量数乘(A, n) {
    return [A[0]*n, A[1]*n];
}

再实现向量相加:

// A = [x, y]
// B = [w, z]
// return [x + w, y + z]
function 向量相加(A, B) {
    return [A[0] + B[0], A[1] + B[1]];
}

我们知道矩阵就是多个向量横着排在一起了,我们给出矩阵可以用向量的数组来给出,例如:

// A = [x, y];
// B = [w, z];
var 矩阵 = [A, B];

好了,我们可以给出矩阵乘以向量的函数了:

// 向量1 = [a, b];
// 向量2 = [c, d];
// D = [向量1, 向量2];
// A = [x, y]
function 矩阵乘以向量(D, A){
    var res0 =  向量数乘(D[0], A[0]);
    var res1 =  向量数乘(D[1], A[1]);
    return 向量相加(res0, res1);
}
最终,给出先拉伸,再旋转的函数式:
// A 是一个向量
// 最后的结果是,A先被拉伸,拉伸系数是a b。
// 然后再旋转alpha弧度。
function 先拉伸再旋转(a, b, alpha, A){
    var D = [    [a*Math.cos(alpha),a*Math.sin(alpha)], [-b*Math.sin(alpha), b*Math.cos(alpha)]
    ];
    return 矩阵乘以向量(D, A);
}

以上包含了四个函数,都是直接可以运行在浏览器的console环境的,你如果介意中文函数名,自行修改就行,不过,中文函数名确实是可以直接运行的,以下给出整体代码例子:

function 向量数乘(A, n) {
    return [A[0]*n, A[1]*n];
}
function 向量相加(A, B) {
    return [A[0] + B[0], A[1] + B[1]];
}
function 矩阵乘以向量(D, A){
    var res0 =  向量数乘(D[0], A[0]);
    var res1 =  向量数乘(D[1], A[1]);
    return 向量相加(res0, res1);
}
function 先拉伸再旋转(a, b, alpha, A){
    var D = [    [a*Math.cos(alpha),a*Math.sin(alpha)], [-b*Math.sin(alpha), b*Math.cos(alpha)]
    ];
    return 矩阵乘以向量(D, A);
}
var A = [1,1];
A = 先拉伸再旋转(1,1, 3.14/2, A); // 不拉伸,然后旋转大约90°。
// 得到的结果是 [-0.9992033562211013, 1.0007960096425679]
// 约等于 [-1, 1]A = 先拉伸再旋转(2,3, 0, A); // x坐标变成2倍,y坐标变成3倍,不旋转
// 结果是[-1.9984067124422027, 3.0023880289277036]
// 约等于 [-2, -3]
A = 先拉伸再旋转(0.5,1/3, -3.14/2, A); // 还原到最初的[1,1]
// 结果就是 [1,1]

总结

本次课讲了一个概念:

将矩阵看成函数,并且可以合并。

合并之后的矩阵,跟向量进行运算,可以一次性得到最终的向量。




  正文结束,下面是答疑

小丫丫说:我后面不想记具体数学表达式了,我把函数一抄,直接用函数就好了。

  • 答:这就是这次课的目的!