矩阵分解:Pixijs 中的 Matrix 和 Transform

开发 前端
矩阵的优点是计算方便,比如父节点和子节点都有 matrix,那子节点最终在画布的 matrix 就是它们的矩阵直接相乘。缺点也明显,就是它的值是几个多种矩阵变换得到的数字,语义糟糕,看不出图形做了什么形变。

大家好,我是前端西瓜哥。

在二维中,对于图形(模型),它会有一个模型矩阵 matrix 来表达图形的形变。

比如图形先做了缩放,然后再位移,则模型矩阵为缩放矩阵左乘位移矩阵得到的复合矩阵。

矩阵的优点是计算方便,比如父节点和子节点都有 matrix,那子节点最终在画布的 matrix 就是它们的矩阵直接相乘。

缺点也明显,就是它的值是几个多种矩阵变换得到的数字,语义糟糕,看不出图形做了什么形变。

这不利于我们对图形的表达。

那么,有没有办法对矩阵做分解,得到多个形变的表达呢?

我们不妨看看 pixijs 怎么做的。

pixijs 里面有两个类:Matrix 和 Transform。

Matrix

Matrix 是平面矩阵类,提供矩阵相关的各种方法。

Matrix 使用 6 个数字表达,代表一个 3x3 的矩阵,用于平面矩阵变换。值有 a、b、c、d、tx、ty。

| a | c | tx|
| b | d | ty|
| 0 | 0 | 1 |

支持缩放、旋转、位移、左乘、右乘、逆矩阵、计算点应用矩阵后的结果等方法。支持链式写法。

缩放、旋转、位移:

import { Matrix } from 'pixi.js';

const matrix = new Matrix();
// 返回一个默认额单位矩阵
// [pixi.js:Matrix a=1 b=0 c=0 d=1 tx=0 ty=0]
// 1, 0, 0,
// 0, 1, 0,
// 0, 0, 1,

matrix.scale(3, 3);
// 放大为原来的 3 倍
// [pixi.js:Matrix a=3 b=0 c=0 d=3 tx=0 ty=0]
// 3, 0, 0,
// 0, 3, 0,
// 0, 0, 1,

// 支持链式写法(等价连续多个变换矩阵左乘)
matrix.rotate(Math.PI / 2).translate(10, 10);
// [pixi.js:Matrix a=1.8369701987210297e-16 b=3 c=-3 d=1.8369701987210297e-16 tx=10 ty=10]
// 上面这个 a 应该为 0,但因为浮点数误差导致一个非常小的小数。

左乘、右乘、逆矩阵:

const leftMatrix = new Matrix();
const rightMatrix = new Matrix();

// 右乘
const newMatrix = leftMatrix.append(rightMatrix);

// 左乘
const newMatrix2 = rightMatrix.prepend(leftMatrix);

// 逆矩阵
const inverseMatrix = leftMatrix.invert();

计算点应用矩阵后的结果、应用逆矩阵的结果:

const matrix = new Matrix();

// 点应用矩阵后的结果
const point = matrix.apply({ x: 100, y: 100 });

// 应用逆矩阵的结果
const inversePoint = matrix.applyInverse({ x: 100, y: 100 });

Transform

Transform 是 Matrix 的等价表达,但是对用户友好。

The Transform class facilitates the manipulation of a 2D transformation matrix through user-friendly properties: position, scale, rotation, skew, and pivot.

Transform 是一些属性的组合,可以表达一个图形的任意形变效果。

属性有:

  1. postion:位置,类型为 point { x: number, y: number }。
  2. scale:缩放,类型为 point。
  3. pivot:基准位置,类型为 point。它作为旋转、缩放的中心点,默认为原点。
  4. skew:斜切,类型为 point。弧度值,表示基向量方向和另一方向形成的角。
  5. rotation:旋转角,弧度单位。

用 typescript 类型表达为:

interface TransformableObject {
  position: PointData;
  scale: PointData;
  pivot: PointData;
  skew: PointData;
  rotation: number;
}

interface PointData {
  x: number;
  y: number;
}

pixijs 的图形使用了 Transform 的这一套表达,让用户能够很简单直观地表达一些简单的形变。

Transform 下有一个 _matrix 属性,维护等价的 matrix 对象,当 transform 的属性更新时,matrix 会标记为 dirty,之后读取的时候会重新生成。

transform 这个名字其实有点迷惑,因为有时候我们也会把用在形变的矩阵 matrix,也叫做 transform。只是 pixijs 这里的命名比较特别,里面也有点乱。

下面看看 Matrix 和 Transform 之间的转换算法。

Transform 转 Matrix

pixijs 中 Transform 转 Matrix 的实现如下。

class Transform {
    /**
     * This matrix is computed by combining this Transforms position, scale, rotation, skew, and pivot
     * properties into a single matrix.
     * @readonly
     */
    get matrix(): Matrix
    {
        const lt = this._matrix;

        if (!this.dirty) return lt;

        lt.a = this._cx * this.scale.x;
        lt.b = this._sx * this.scale.x;
        lt.c = this._cy * this.scale.y;
        lt.d = this._sy * this.scale.y;

        lt.tx = this.position.x - ((this.pivot.x * lt.a) + (this.pivot.y * lt.c));
        lt.ty = this.position.y - ((this.pivot.x * lt.b) + (this.pivot.y * lt.d));

        this.dirty = false;

        return lt;
    }
  
    /** Called when the skew or the rotation changes. */
    protected updateSkew(): void
    {
        this._cx = Math.cos(this._rotation + this.skew.y);
        this._sx = Math.sin(this._rotation + this.skew.y);
        this._cy = -Math.sin(this._rotation - this.skew.x); // cos, added PI/2
        this._sy = Math.cos(this._rotation - this.skew.x); // sin, added PI/2

        this.dirty = true;
    }
}

_cx、_sx、_cy、sy 会在更新 skew 或 rotataion 时进行更新,是缓存数据。

我们抽出算法。

上面为了提高计算效率,没有用矩阵类的方法,这里给矩阵相乘表达。

import { Matrix } from 'pixi.js';

const transformToMatrix = (tf: TransformableObject) => {
  const cosX = Math.cos(tf.rotation + tf.skew.y);
  const sinX = Math.sin(tf.rotation + tf.skew.y);
  const cosY = -Math.sin(tf.rotation - tf.skew.x);
  const sinY = Math.cos(tf.rotation - tf.skew.x);

  const skewMatrix = new Matrix(cosX, sinX, cosY, sinY, 0, 0);

  return new Matrix()
    .translate(-tf.pivot.x, -tf.pivot.y)
    .prepend(skewMatrix)
    .scale(tf.scale.x, tf.scale.y)
    .translate(tf.position.x, tf.position.y);
};

斜切和旋转二者需要合并为一个斜切矩阵。因为旋转本质是一种斜切,只是刚好两个斜切角的和为 360 度的倍数。

所以这里要把 skew 和 rotation 加起来,计算一个斜切矩阵。

结果矩阵为下面几个矩阵连续左乘:

  1. pivot 负方向的位移矩阵。表示图形上的某个点,移动到坐标原点。pivot 可以理解为前置版位移。
  2. skew 和 rotation 得到的斜切矩阵。
  3. scale 对应的缩放矩阵。
  4. position 对应的位移矩阵。

Matrix 转 Transform

pixi.js 的实现为:

class Matrix {
      /**
     * Decomposes the matrix (x, y, scaleX, scaleY, and rotation) and sets the properties on to a transform.
     * @param transform - The transform to apply the properties to.
     * @returns The transform with the newly applied properties
     */
    public decompose(transform: TransformableObject): TransformableObject
    {
        // 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;
    }
}

上面这个是 matrix 对象的方法,接收一个 transform 对象,修改它的值,并返回它自身。

pivot 这个就直接取传入的 transform 的 pivot。

计算斜切值 skew。即求图形两条相邻边各自的余弦值对应的角。

如果刚好两个斜切角之和为 0 或 360 度,说明是特殊的斜切——旋转,那就给 rotation 设置为 skewY。skew 设置为 0。

如果不是,rotation 设置为 0,skew 设置为斜切角。

scale 分别为 a 和 b、c 和 d 的平方和开方。

最后是 position,理论上直接取 tx 和 ty 即可,不过有个 pivot。pivot 是图形斜切缩放前的前置位移,所以给它应用去掉 tx 和 ty 的矩阵做一个运算,然后再加上 tx 和 ty 即可。

结尾

矩阵 matrix 体现了数学的简洁之美,只用几个数字,就能表达图形的各种变换的组合。

但问题是可读性差,无法直接看出图形的特性,比如旋转了多少,缩放了多少。

为了提高易用性,pixijs 引入了一套和 matrix 等价的 transform,让开发者使用图形时,能够快速上手,很好地解决了 Matrix 的弊端。

责任编辑:姜华 来源: 前端西瓜哥
相关推荐

2023-02-15 09:00:00

算法推荐系统矩阵分解算法

2017-02-08 09:25:16

Spark分解推荐

2014-07-04 10:05:57

机器学习

2023-01-08 23:06:14

css3d变换

2014-07-15 09:36:55

机器学习

2017-07-06 08:36:10

特征向量矩阵PCA

2022-10-24 15:56:55

PythonPyTorchGPU 计算

2021-06-24 08:30:00

人工智能数据计算

2023-02-08 17:04:14

Python计算库数学函数

2023-10-09 07:49:33

PixiJSWebGL

2021-07-14 06:40:02

矩阵路径字符串

2023-10-13 07:29:23

PixiJSRunner

2023-02-28 07:28:50

Spritepixijs

2023-06-08 08:16:33

TickerPixiJS

2023-12-11 07:52:19

图像处理矩阵计算计算机视觉

2023-06-07 08:13:46

PixiJSCanvas 库

2023-02-22 09:27:31

CanvasWebGL

2022-11-07 19:08:28

transform属性浏览器

2010-09-02 14:53:19

DHCP Relay

2021-05-22 09:44:21

PythonNumpy数组Python矩阵
点赞
收藏

51CTO技术栈公众号