关于前端:2D矩阵这都是什么妖魔鬼怪啊

7次阅读

共计 5014 个字符,预计需要花费 13 分钟才能阅读完成。

头“秃”来自“the book of shader”

明天我的文章可能会有点形象。我尽量“有图有假相”,不让大家的大脑内存透露!

本文会讲到:

  • transform 2D 变换背地的数学原理
  • 如何直观了解一个矩阵
  • 齐次变换是什么?
  • 可能会波及一些:逆矩阵(),正交(),向量的常识,用到的时候伪装本人晓得就能够了!
  • 但根本不会波及 3D,四元数,各类引擎中 MVP 矩阵变换。话不多说咱们开始吧

scale+skew+rotate 我全都要

作为一个传统前端,一个资深 csser,肯定晓得 transform 属性上,是能够配置 rotate 用于旋转,scale 用于缩放,skew 用于斜切。

更进一步,transform 还提供 matrix,matrix3d 这样的操作让咱们能够更自在地变换图形:

所以大略可能想到 2D 的 rotate,scale,skew 实质其实就是某种非凡的矩阵。

那么接下来就是如何把这三个操作写成矩阵模式。

不过,在此之前咱们先想想,矩阵操作的根本单元是什么?

是“点”

点组成了线,线组成了面!

如果你有写过 shader,你会晓得 gl 也是操作一堆的点来生成画面的。

所以“女神放大了都是马赛克

当然!不放大也可能有马赛克(误)

既然晓得矩阵操作的单元是“点”,那么任意的点 p(x,y)通过一个矩阵变动后的后果是什么?
2D:

3D:

上图公式示意矩阵的乘法:

矩阵 * 矩阵,则能够把右矩阵看成两个点 / 向量拼接而成的。乘法规定不变
当初让咱们来看看 scale/skew/rotate 所对应的矩阵:

对于最简略的 scale 矩阵,(x,y)通过缩放矩阵变换咱们冀望它变成(scaleXx, scaleYy), 带入下面的公式 1 验证:

而对于旋转矩阵,这个 示意逆时针旋转后向量,与原向量之间的夹角。

让咱们验证一下,假如对于 p0(1,1)那么它通过 =45 度旋转后,将变为 p1(0,√2):


斜切矩阵也是相似的套路,角度从一个变成了两个。这个后文咱们还会提到。读者也能够自行验证下。

目前咱们尽管把 scale,skew,rotate 三种变换写成了矩阵,但他们三者仍旧是独立的。人类总是贪心的,能不能只用一种形式去了解它们?换言之,咱们更想要晓得,对于任意的变换(这里说任意其实不太谨严,这个咱们前面会提到),其对应的矩阵是什么?

那就要用引入一个新的概念:

“基”向量(必须用个紫色)

咱们晓得矩阵其实是在扭转“点”,在空间中咱们如何示意一个点?

在 2D 笛卡尔坐标体系下,两个互相垂直的方向形成 x 轴和 y 轴。任意点 P 则用一对数 (a,b) 示意。比方 (2,3) 就示意 2D 空间中的一个确定的点, 这里的 2,3 的意义又是什么?

这里 2 的意思就是 p 在 x 方向占据 2 个单位长度,3 就示意在 y 方向占据 3 个单位长度。这两个单位长度用向量示意为[1,0]^T 与[0,1]^T。

这种观点下,点能够写成:2 [1,0]^T + 3 [0,1]^T:

T 就是转置,能够了解把原先横着写的行向量,竖着写成列向量。看!就是下图那种。

推广到任意坐标(a,b):

|1 0||a|    
|0 1||b| 

而其中[1,0]^T 与[0,1]^T 就被称为一组基向量。他们组成的这个矩阵其实有个名字叫做单位矩阵。

单位矩阵在更简单的交互中其实有很多重要的性质,然而和明天的话题没啥关系,就当听个冷常识吧!

high-level 地了解矩阵

咱们当初把“基向量”和用“基向量”示意的点都带入线性变换中,看看通过一次变换之后基向量会如何扭转?

这里咱们发现了一个乏味的景象:

原先的基向量 x0,y0,通过变换后变为 1,1(就是上图中红色的局部)。原先的点 P0 通过变换后变为 P1,此时点 p1 是能够用 1,1 形容。而 1,1 后面的系数还是原来 p0(a,b)的系数。(尽管形态产生了变动,但图中红色的虚线网格与基向量的比例不变)

整顿成数学语言就是:

举个“栗”子:

对于一个缩放操作,x 轴放大 1.5 倍,y 轴放大 2 倍。

[x] = [1.5,0]^T

[y] = [0,2]^T

那么该变换对应的矩阵就是:

|x y| = |1.5  0 |   =  |scaleX   0    |
         | 0    2 |     |   0   scaleY |

同理,旋转和斜切变换也能够应用基向量的思路,易得对应矩阵:


这样包含但不限于 rotate/scale/skew 的矩阵变换,咱们都能够从基向量的角度去了解了。

说到这,对于 2D 矩阵的知识点,大抵就完结了!吗?


是不是有哪里怪怪的?

老子动不了了!

对!咱们齐全没有解释 translate。到目前为止咱们说的 2 * 2 矩阵只能进行如下操作

只能把点 [x,y] 变换成 [ax+cy,bx+dy]^T 的模式。
这种变换被叫做线性变换,线性变换有 2 个比拟显著的特点:

  • 线性:对于矩阵变换去察看空间中任意的点,它们所受到的影响是统一的。或者不谨严地说,网格不会在变换后变成曲线(如下图)也不会不平均: 部分放大,另一部分放大。
  • 对于原点对称:

    图中不管基向量如何变动,原点都没有产生扭转。

这个性质其实也很好了解,[0,0]这个点不论用什么矩阵解决后都是[0,0],这意味着实际上矩阵变换没有能力进行平移操作。

因为线性变换的代数实质是:

然鹅!平移操作的代数实质是:

这时候仅仅应用一个 2D 矩阵,想要平移。就真的是“臣妾做不到了”,所以这时咱们引入一个新的变换“齐次变换”!

齐次变换


说好是 2D 怎么又给我整成 3D?尽管咱们把原来的 2 维点 [x,y] 拓展为三维的点[x,y,1]。

但咱们不用操心第三个行,咱们仅看矩阵的前两行即可:

实际上,它就是 css 中的 matrix(a,b,c,d,tx,tx)。而这个矩阵变换的后果就真的实现了 2D 线性变换与平移:

至此,2D 矩阵的变换的原理局部就根本说完了。上面咱们联合下 PIXI.js 的源代码进行一些简略的源码剖析:

秃头环节

PIXI.js 采纳 transform+matrix 的组合实现图形操作。
让咱们先来看看 Matrix.ts.。

export class Matrix
{
// ......
  constructor(a = 1, b = 0, c = 0, d = 1, tx = 0, ty = 0)
    {
        /**
         * @member {number}
         * @default 1
         */
        this.a = a;

        /**
         * @member {number}
         * @default 0
         */
        this.b = b;

        /**
         * @member {number}
         * @default 0
         */
        this.c = c;

        /**
         * @member {number}
         * @default 1
         */
        this.d = d;

        /**
         * @member {number}
         * @default 0
         */
        this.tx = tx;

        /**
         * @member {number}
         * @default 0
         */
        this.ty = ty;
    }
// .....
}

能够看到这个构造函数的参数,和咱们上文所说的齐次变换矩阵的前两行是一毛一样的,也与 css 中的 matrix 参数统一。

然而它的正文还是容易引起一些误会的:

/**
     * @param {number} [a=1] - x scale
     * @param {number} [b=0] - y skew
     * @param {number}  - x skew
     * @param {number} [d=1] - y scale
     * @param {number} [tx=0] - x translation
     * @param {number} [ty=0] - y translation
*/

具体每个参数的用处,大可不必拘泥,咱们当初能够从基向量的角度去精确了解了。

matrix 的大部分办法都比拟容易了解,这里咱们筛选几个来说说吧!

rotate(angle: number): this
    {const cos = Math.cos(angle);
        const sin = Math.sin(angle);

        const a1 = this.a;
        const c1 = this.c;
        const tx1 = this.tx;

        this.a = (a1 * cos) - (this.b * sin);
        this.b = (a1 * sin) + (this.b * cos);
        this.c = (c1 * cos) - (this.d * sin);
        this.d = (c1 * sin) + (this.d * cos);
        this.tx = (tx1 * cos) - (this.ty * sin);
        this.ty = (tx1 * sin) + (this.ty * cos);

        return this;
    }

rotate 的作用就是旋转,matrix 自身也记录了一套变换。那么在此基础上,再进行操作就须要应用矩阵的乘法。矩阵的乘法是有程序的。例如咱们须要对点进行如图平移,缩放,平移操作,就必须把矩阵顺次序相乘:

而 rotate 操作是在原有矩阵之后执行的,所以新矩阵就是[Result] = Rotate:

是不是恍然大悟?

是的!除非是一些非凡操作,大部分失常人类是不会违心面对矩阵计算的,因而 PIXI 提供了另一个类 Transform。把矩阵变换用一些比拟容易了解的属性(position,scale,rotate…)代替(CSS 重的 transform 也有相似效用)除外,Transform 代码也比拟业务,它下面的数据都是 ObservablePoint,还实现了父子级的关系。而理论运行时,Transform 通过 decompose 办法实现本身数据与 Matrix 数据的转换。

decompose(transform: Transform): Transform
    {
        // sort out rotation / skew..
        const a = this.a;
        const b = this.b;
        const c = this.c;
        const d = this.d;
        const pivot = transform.pivot;

        const skewX = -Math.atan2(-c, d);
        const skewY = Math.atan2(b, a);

        const delta = Math.abs(skewX + skewY);

        if (delta < 0.00001 || Math.abs(PI_2 - delta) < 0.00001)
        {
            transform.rotation = skewY;
            transform.skew.x = transform.skew.y = 0;
        }
        else
        {
            transform.rotation = 0;
            transform.skew.x = skewX;
            transform.skew.y = skewY;
        }

        // next set scale
        transform.scale.x = Math.sqrt((a * a) + (b * b));
        transform.scale.y = Math.sqrt((c * c) + (d * d));

        // next set position
        transform.position.x = this.tx + ((pivot.x * a) + (pivot.y * c));
        transform.position.y = this.ty + ((pivot.x * b) + (pivot.y * d));

        return transform;
    }

其中 Math.atan2(-c, d)和 Math.atan2(b, a)。其实在计算 2 个基向量变换前后的夹角:

当ø与 相等时,咱们就认为这是一个旋转操作。然而因为数值精度的问题,浮点数很难会相等,所以 PIXI 采纳差值法判断, 即

var delta = Math.abs(ø -);if(delta < Number.EPSILON){...}else{....}

这里须要留神 x 基向量确定的是 Y 方向的斜切值,而 y 基向量确定的是 X 方向的斜切值:

此处呈现了我感觉是这段代码中最神来之笔的中央。

因为咱们算进去的 skewX 其实是负方向的。而 delta = Math.abs(ø –)又须要取两值之差。PIXI 就间接在 Math.atan2(-c, d)加上了符号。后续判断间接应用 delta = Math.abs(skewX + skewY);而 skewX 方向也被校对了。这种代码上的整合和巧思其实遍布 PIXI.js 的每个角落,切实拜服作者的逻辑能力。

最初说说 scale 和 position

position 就是失常的齐次变换。

而对于 scale,咱们判断是否有缩放的规范就是基向量是否缩放,所以 scale.x 就是旋转后 x 基向量的模长(aa+bb)^0.5

scale.y 同理。

以上就是集体总结的一些对于 2D 矩阵的知识点,心愿各位大佬不吝赐教。

忽然发现咱们好久没聊 shader 了,下期咱们就来聊聊水雾和毛玻璃的 shader 要如何实现吧。

参考资料:

Fundamentals of Computer Graphics, Fourth Edition

《3blue1Brown 线性代数实质》

thebookofshaders

正文完
 0