矩陣分解:Pixijs 中的 Matrix 和 Transform
大家好,我是前端西瓜哥。
在二維中,對于圖形(模型),它會有一個模型矩陣 matrix 來表達(dá)圖形的形變。
比如圖形先做了縮放,然后再位移,則模型矩陣為縮放矩陣左乘位移矩陣得到的復(fù)合矩陣。
矩陣的優(yōu)點(diǎn)是計算方便,比如父節(jié)點(diǎn)和子節(jié)點(diǎn)都有 matrix,那子節(jié)點(diǎn)最終在畫布的 matrix 就是它們的矩陣直接相乘。
缺點(diǎn)也明顯,就是它的值是幾個多種矩陣變換得到的數(shù)字,語義糟糕,看不出圖形做了什么形變。
這不利于我們對圖形的表達(dá)。
那么,有沒有辦法對矩陣做分解,得到多個形變的表達(dá)呢?
我們不妨看看 pixijs 怎么做的。
pixijs 里面有兩個類:Matrix 和 Transform。
Matrix
Matrix 是平面矩陣類,提供矩陣相關(guān)的各種方法。
Matrix 使用 6 個數(shù)字表達(dá),代表一個 3x3 的矩陣,用于平面矩陣變換。值有 a、b、c、d、tx、ty。
| a | c | tx|
| b | d | ty|
| 0 | 0 | 1 |
支持縮放、旋轉(zhuǎn)、位移、左乘、右乘、逆矩陣、計算點(diǎn)應(yīng)用矩陣后的結(jié)果等方法。支持鏈?zhǔn)綄懛ā?/p>
縮放、旋轉(zhuǎn)、位移:
import { Matrix } from 'pixi.js';
const matrix = new Matrix();
// 返回一個默認(rèn)額單位矩陣
// [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,
// 支持鏈?zhǔn)綄懛ǎǖ葍r連續(xù)多個變換矩陣左乘)
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 應(yīng)該為 0,但因?yàn)楦↑c(diǎn)數(shù)誤差導(dǎo)致一個非常小的小數(shù)。
左乘、右乘、逆矩陣:
const leftMatrix = new Matrix();
const rightMatrix = new Matrix();
// 右乘
const newMatrix = leftMatrix.append(rightMatrix);
// 左乘
const newMatrix2 = rightMatrix.prepend(leftMatrix);
// 逆矩陣
const inverseMatrix = leftMatrix.invert();
計算點(diǎn)應(yīng)用矩陣后的結(jié)果、應(yīng)用逆矩陣的結(jié)果:
const matrix = new Matrix();
// 點(diǎn)應(yīng)用矩陣后的結(jié)果
const point = matrix.apply({ x: 100, y: 100 });
// 應(yīng)用逆矩陣的結(jié)果
const inversePoint = matrix.applyInverse({ x: 100, y: 100 });
Transform
Transform 是 Matrix 的等價表達(dá),但是對用戶友好。
The Transform class facilitates the manipulation of a 2D transformation matrix through user-friendly properties: position, scale, rotation, skew, and pivot.
Transform 是一些屬性的組合,可以表達(dá)一個圖形的任意形變效果。
屬性有:
- postion:位置,類型為 point { x: number, y: number }。
- scale:縮放,類型為 point。
- pivot:基準(zhǔn)位置,類型為 point。它作為旋轉(zhuǎn)、縮放的中心點(diǎn),默認(rèn)為原點(diǎn)。
- skew:斜切,類型為 point。弧度值,表示基向量方向和另一方向形成的角。
- rotation:旋轉(zhuǎn)角,弧度單位。
用 typescript 類型表達(dá)為:
interface TransformableObject {
position: PointData;
scale: PointData;
pivot: PointData;
skew: PointData;
rotation: number;
}
interface PointData {
x: number;
y: number;
}
pixijs 的圖形使用了 Transform 的這一套表達(dá),讓用戶能夠很簡單直觀地表達(dá)一些簡單的形變。
Transform 下有一個 _matrix 屬性,維護(hù)等價的 matrix 對象,當(dāng) transform 的屬性更新時,matrix 會標(biāo)記為 dirty,之后讀取的時候會重新生成。
transform 這個名字其實(shí)有點(diǎn)迷惑,因?yàn)橛袝r候我們也會把用在形變的矩陣 matrix,也叫做 transform。只是 pixijs 這里的命名比較特別,里面也有點(diǎn)亂。
下面看看 Matrix 和 Transform 之間的轉(zhuǎn)換算法。
Transform 轉(zhuǎn) Matrix
pixijs 中 Transform 轉(zhuǎn) Matrix 的實(shí)現(xiàn)如下。
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 時進(jìn)行更新,是緩存數(shù)據(jù)。
我們抽出算法。
上面為了提高計算效率,沒有用矩陣類的方法,這里給矩陣相乘表達(dá)。
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);
};
斜切和旋轉(zhuǎn)二者需要合并為一個斜切矩陣。因?yàn)樾D(zhuǎn)本質(zhì)是一種斜切,只是剛好兩個斜切角的和為 360 度的倍數(shù)。
所以這里要把 skew 和 rotation 加起來,計算一個斜切矩陣。
結(jié)果矩陣為下面幾個矩陣連續(xù)左乘:
- pivot 負(fù)方向的位移矩陣。表示圖形上的某個點(diǎn),移動到坐標(biāo)原點(diǎn)。pivot 可以理解為前置版位移。
- skew 和 rotation 得到的斜切矩陣。
- scale 對應(yīng)的縮放矩陣。
- position 對應(yīng)的位移矩陣。
Matrix 轉(zhuǎn) Transform
pixi.js 的實(shí)現(xiàn)為:
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。即求圖形兩條相鄰邊各自的余弦值對應(yīng)的角。
如果剛好兩個斜切角之和為 0 或 360 度,說明是特殊的斜切——旋轉(zhuǎn),那就給 rotation 設(shè)置為 skewY。skew 設(shè)置為 0。
如果不是,rotation 設(shè)置為 0,skew 設(shè)置為斜切角。
scale 分別為 a 和 b、c 和 d 的平方和開方。
最后是 position,理論上直接取 tx 和 ty 即可,不過有個 pivot。pivot 是圖形斜切縮放前的前置位移,所以給它應(yīng)用去掉 tx 和 ty 的矩陣做一個運(yùn)算,然后再加上 tx 和 ty 即可。
結(jié)尾
矩陣 matrix 體現(xiàn)了數(shù)學(xué)的簡潔之美,只用幾個數(shù)字,就能表達(dá)圖形的各種變換的組合。
但問題是可讀性差,無法直接看出圖形的特性,比如旋轉(zhuǎn)了多少,縮放了多少。
為了提高易用性,pixijs 引入了一套和 matrix 等價的 transform,讓開發(fā)者使用圖形時,能夠快速上手,很好地解決了 Matrix 的弊端。