MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

Matrix math for the web

这篇翻译不完整。请帮忙从英语翻译这篇文章

矩阵可以用于表示空间中的对象的变换,并且是在Web上的可视化中使用的重要工具。本文探索如何创建并配合CSS3变换和matrix3d变换类型使用矩阵。

虽然本文为了便于解释而使用了CSS3, 矩阵却是许多技术中的核心概念, 包括WebGL和着色器。本文也是MDN content kit的一部分。示例使用了一组全局对象MDN下的工具函数

什么是变换矩阵?

虽然存在许多类型的矩阵,但我们感兴趣的是三维变换矩阵。这种矩阵是一个4x4方阵,共16个值。在JavaScript中,可以很方便的用数组表示矩阵。比如典型的单位矩阵。单位阵乘上一个点或者矩阵, 其结果保持不变。

var identityMatrix = [
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1
];

说到乘法,这种运算用于矩阵是什么样的呢?最简单的例子是矩阵乘一个点。你可能注意到,三维空间中的点和一个4x4矩阵并不匹配,为此我们加上了额外的第四维W。一般来说,把W设为1就可以了。W维度还有一些额外的用途超出本文的讨论范围。查看WebGL model view projection看它有哪些用途。

注意矩阵和点的对齐方式:

[1, 0, 0, 0,
 0, 1, 0, 0,
 0, 0, 1, 0,
 0, 0, 0, 1]

[4, 3, 2, 1]

定义相乘函数

我们在示例代码中定义了一个乘法函数 — multiplyMatrixAndPoint():

function multiplyMatrixAndPoint(matrix, point) {
  
  // 给矩阵的每一部分一个简单的变量名, 列数(c)与行数(r)
  var c0r0 = matrix[ 0], c1r0 = matrix[ 1], c2r0 = matrix[ 2], c3r0 = matrix[ 3];
  var c0r1 = matrix[ 4], c1r1 = matrix[ 5], c2r1 = matrix[ 6], c3r1 = matrix[ 7];
  var c0r2 = matrix[ 8], c1r2 = matrix[ 9], c2r2 = matrix[10], c3r2 = matrix[11];
  var c0r3 = matrix[12], c1r3 = matrix[13], c2r3 = matrix[14], c3r3 = matrix[15];
  
  // 定义点坐标
  var x = point[0];
  var y = point[1];
  var z = point[2];
  var w = point[3];
  
  // 点坐标和第一列对应相乘, 再求和
  var resultX = (x * c0r0) + (y * c0r1) + (z * c0r2) + (w * c0r3);
  
  // 点坐标和第二列对应相乘, 再求和
  var resultY = (x * c1r0) + (y * c1r1) + (z * c1r2) + (w * c1r3);
  
  // 点坐标和第三列对应相乘, 再求和
  var resultZ = (x * c2r0) + (y * c2r1) + (z * c2r2) + (w * c2r3);
  
  // 点坐标和第四列对应相乘, 再求和
  var resultW = (x * c3r0) + (y * c3r1) + (z * c3r2) + (w * c3r3);
  
  return [resultX, resultY, resultZ, resultW]
}

现在我们可以使用上面的函数将一个点乘上一个矩阵。乘以单位阵将会返回原值:

// identityResult等于[4,3,2,1]
var identityResult = multiplyMatrixAndPoint(identityMatrix, [4,3,2,1]);

返回同一个点并没有什么用处, 但还有一些其它的矩阵可以作用于点,返回有用的结果。接下来我们将演示其中一些矩阵。

两个矩阵相乘

除了把矩阵和点相乘,你也可以把两个矩阵相乘。之前的函数可以帮助我们简化这个过程:

function multiplyMatrices(matrixA, matrixB) {
  
  // 将第二个矩阵按列切片
  var column0 = [matrixB[0], matrixB[4], matrixB[8], matrixB[12]];
  var column1 = [matrixB[1], matrixB[5], matrixB[9], matrixB[13]];
  var column2 = [matrixB[2], matrixB[6], matrixB[10], matrixB[14]];
  var column3 = [matrixB[3], matrixB[7], matrixB[11], matrixB[15]];
  
  // 将每列分别和矩阵相乘
  var result0 = multiplyMatrixAndPoint( matrixA, column0 );
  var result1 = multiplyMatrixAndPoint( matrixA, column1 );
  var result2 = multiplyMatrixAndPoint( matrixA, column2 );
  var result3 = multiplyMatrixAndPoint( matrixA, column3 );
  
  // 把结果重新组合成矩阵
  return [
    result0[0], result1[0], result2[0], result3[0],
    result0[1], result1[1], result2[1], result3[1],
    result0[2], result1[2], result2[2], result3[2],
    result0[3], result1[3], result2[3], result3[3]
  ]
}

用法

让我们看一看实际使用:

var someMatrix = [
  4, 0, 0, 0,
  0, 3, 0, 0,
  0, 0, 5, 0,
  4, 8, 4, 1
]

var identityMatrix = [
  1, 0, 0, 0,
  0, 1, 0, 0,
  0, 0, 1, 0,
  0, 0, 0, 1
];

// 返回someMatrix的数组表示
var someMatrixResult = multiplyMatrices(identityMatrix, someMatrix);

重要: 这些函数是为了解释的清晰而编写,而不是为了速度或者内存管理。这些函数新建了大量数组, 可能在实时运算时导致垃圾回收的巨大开销。在实际产品中最好使用优化过的函数。比如glMatrix就是一个注重速度和性能的库。

平移矩阵

平移矩阵基于单位矩阵。它将一个对象沿x,y,z其中一个方向进行移动。最简单的想象平移的方式是设想拿起一个咖啡杯。咖啡杯必须保持直立和朝向以免咖啡洒出来。它可以离开桌子在空间中移动。

现在我们还喝不到这个杯子里的咖啡,因为在平移矩阵的单独作用下杯子并不能倾斜。在之后的部分,我们会讨论新的矩阵,来解决这个问题。

var x = 50;
var y = 100;
var z = 0;

var translationMatrix = [
    1,    0,    0,   0,
    0,    1,    0,   0,
    0,    0,    1,   0,
    x,    y,    z,   1
];

用矩阵操作DOM

一个非常简单的开始使用矩阵的方法是使用CSS3里的matrix3d变换。首先, 我们新建一个简单的<div>并加上一些内容。这里样式没有展示出来,但我们将其设置成了页面居中且固定宽度与高度。我们同样为变换设定了动画以便清晰的观察发生的变化。

<div id='move-me' class='transformable'>
  <h2>Move me with a matrix</h2>
  <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit...</p>
</div>

最后,我们会为每一个例子生成一个4x4矩阵,然后更新<div>的样式,对其应用一个matrix3d变换。要记住即使矩阵有四行四列,也要将其写成单行16个值。

// 从矩阵数组创建matrix3d样式属性
function matrixArrayToCssMatrix(array) {
  return "matrix3d(" + array.join(',') + ")";
}
	
// 获取DOM元素
var moveMe = document.getElementById('move-me');

// 返回结果如:"matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 50, 100, 0, 1);"
var matrix3dRule = matrixArrayToCssMatrix( translationMatrix );

// 设置变换
moveMe.style.transform = matrix3dRule;

在JSFiddle中查看

An example of matrix translation

缩放矩阵

缩放矩阵使对象的高度、宽度和深度三个维度的其中之一变大或变小。在典型(笛卡尔)坐标系中, 这将使得x,y,z坐标拉伸或收缩。

var w = 1.5; // width  (x)
var h = 0.7; // height (y)
var d = 1;   // depth  (z)

var scaleMatrix = [
    w,    0,    0,   0,
    0,    h,    0,   0,
    0,    0,    d,   0,
    0,    0,    0,   1
];

在JSFiddle中查看

An example of matrix scaling

旋转矩阵

旋转矩阵比平移和缩放矩阵要稍复杂一些。其中用到了三角函数来完成旋转。尽管这部分不会把步骤讲得过于详细,但是下例有简单的说明。

// 不借助矩阵将点绕原点旋转
var point = [10,2];

// 计算到原点的距离
var distance = Math.sqrt(point[0] * point[0] + point[1] * point[1]);

// 60度
var rotationInRadians = Math.PI / 3; 

var transformedPoint = [
  Math.cos( rotationInRadians ) * distance,
  Math.sin( rotationInRadians ) * distance
];

我们可以将上述步骤表示为一个矩阵,并且单独应用到x, y,和z坐标。下面是绕Z轴旋转的表示:

var sin = Math.sin;
var cos = Math.cos;

// NOTE: There is no perspective in these transformations, so a rotation
//       at this point will only appear to only shrink the div

var a = Math.PI * 0.3; // 转角

// 绕Z轴旋转
var rotateZMatrix = [
  cos(a), -sin(a),    0,    0,
  sin(a),  cos(a),    0,    0,
       0,       0,    1,    0,
       0,       0,    0,    1
];

在JSFiddle中查看

这里是一组返回旋转矩阵的函数。要注意的是由于没有加入透视,所以旋转看起来没有那么3D。这就好像摄像机在极近距离拍摄一个物体——透视的感觉消失了。

function rotateAroundXAxis(a) {
  
  return [
       1,       0,        0,     0,
       0,  cos(a),  -sin(a),     0,
       0,  sin(a),   cos(a),     0,
       0,       0,        0,     1
  ];
}

function rotateAroundYAxis(a) {
  
  return [
     cos(a),   0, sin(a),   0,
          0,   1,      0,   0,
    -sin(a),   0, cos(a),   0,
          0,   0,      0,   1
  ];
}

function rotateAroundZAxis(a) {
  
  return [
    cos(a), -sin(a),    0,    0,
    sin(a),  cos(a),    0,    0,
         0,       0,    1,    0,
         0,       0,    0,    1
  ];
}

在JSFiddle中查看

矩阵组合

矩阵的真正厉害之处在于矩阵的组合。当一组特定类型的矩阵连乘起来,它们保留了变换的经过并且是可逆的。这意味着如果平移、旋转和缩放矩阵组合在一起,当我们使用逆变换并颠倒应用的顺序,可以得到原来的点。

矩阵相乘的结果与顺序有关。两个数相乘时,a * b = c, 和 b * a = c 都是正确的。例如,3 * 4 = 12, 和 4 * 3 = 12。在数学上这些数被称为可交换。矩阵不能保证交换顺序后的运算结果,所以矩阵是不可交换的。

另一个需要记住的点是在WebGL和CSS3中的矩阵相乘需要和变换发生的顺序相反。例如,缩放对象到80%,向下移动200像素,然后绕原点旋转90度在伪代码中应该像下面这样。

  transformation = rotate * translate * scale

组合多种变换

我们将使用的函数是工具函数的一部分。其接受矩阵的数组并把它们乘起来。在WebGL着色器代码里,这内建在语言里,并且我们可以使用 * 运算符。除此之外,本例使用了一个缩放函数,一个平移函数,它们返回之前定义的矩阵。

var transformMatrix = MDN.multiplyArrayOfMatrices([
  rotateAroundZAxis(Math.PI * 0.5),    // 第3步:旋转90度
  translate(0, 200, 0),                // 第2步:下移100像素
  scale(0.8, 0.8, 0.8),                // 第1步:缩小
]);

在JSFiddle中查看

An example of matrix composition

最后是展示矩阵用途的有趣的一步,看看怎样颠倒这些步骤并得到最开始的单位矩阵。

var transformMatrix = MDN.multiplyArrayOfMatrices([
  scale(1.25, 1.25, 1.25),             // 第6步:放大
  translate(0, -200, 0),               // 第5步:上移
  rotateAroundZAxis(-Math.PI * 0.5),   // 第4步:倒转
  rotateAroundZAxis(Math.PI * 0.5),    // 第3步:旋转90度
  translate(0, 200, 0),                // 第2步:下移100像素
  scale(0.8, 0.8, 0.8),                // 第1步:缩小
]);

为什么矩阵这么重要?

矩阵之所以重要,是因为它可以用少量的数字描述大量的空间中的变换,并且能轻易地在程序间共享。矩阵可以不同的坐标空间,甚至一些矩阵乘法可以将一组数据从一个坐标空间映射到另一个坐标空间。矩阵能够高效率地保存生成它们的每一步变换。

对于在WebGL中使用,显卡尤其擅长大量的点乘矩阵运算。各种各样的运算,如点定位、光线运算、动态角色,都依赖这个基础工具。

文档标签和贡献者

标签: 
 此页面的贡献者: ZhouQD, xgqfrms-GitHub, Jerry.M.WLong
 最后编辑者: ZhouQD,