网页矩阵数学
矩阵可用于表示空间中物体的变换,并用于在 Web 上构建图像和可视化数据时执行许多关键类型的计算。本文探讨了如何创建矩阵以及如何将它们与 CSS 变换 和 matrix3d
变换类型一起使用。
虽然本文使用 CSS 来简化解释,但矩阵是许多不同技术使用的核心概念,包括 WebGL、WebXR(VR 和 AR)API 以及 GLSL 着色器。本文也可作为 MDN 内容工具包。实时示例使用 实用函数 集合,这些函数在名为 MDN
的全局对象下可用。
变换矩阵
矩阵有很多类型,但我们感兴趣的是 3D 变换矩阵。这些矩阵由一组 16 个值组成,这些值按 4×4 网格排列。在 JavaScript 中,很容易将矩阵表示为数组。
让我们从考虑单位矩阵开始。这是一个特殊的变换矩阵,其功能类似于标量乘法中的数字 1;就像 n * 1 = n 一样,将任何矩阵乘以单位矩阵都会得到一个结果矩阵,其值与原始矩阵匹配。
单位矩阵在 JavaScript 中看起来像这样
let identityMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
乘以单位矩阵是什么样子?最简单的例子是将一个点乘以单位矩阵。由于 3D 点只需要三个值(x
、y
和 z
),而变换矩阵是一个 4×4 值矩阵,我们需要在点中添加第四个维度。按照惯例,这个维度称为透视,用字母 w
表示。对于典型位置,将 w
设置为 1 将使数学运算正常进行。
在将 w
分量添加到点之后,请注意矩阵和点是如何整齐地对齐的
[1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1]
[4, 3, 2, 1] // Point at [x, y, z, w]
w
分量还有一些其他用途,本文不再详细介绍。查看 WebGL 模型视图投影 文章,了解它如何派上用场。
将矩阵和点相乘
在我们的示例代码中,我们定义了一个函数来将矩阵和点相乘——multiplyMatrixAndPoint()
// point • matrix
function multiplyMatrixAndPoint(matrix, point) {
// Give a simple variable name to each part of the matrix, a column and row number
let c0r0 = matrix[0],
c1r0 = matrix[1],
c2r0 = matrix[2],
c3r0 = matrix[3];
let c0r1 = matrix[4],
c1r1 = matrix[5],
c2r1 = matrix[6],
c3r1 = matrix[7];
let c0r2 = matrix[8],
c1r2 = matrix[9],
c2r2 = matrix[10],
c3r2 = matrix[11];
let c0r3 = matrix[12],
c1r3 = matrix[13],
c2r3 = matrix[14],
c3r3 = matrix[15];
// Now set some simple names for the point
let x = point[0];
let y = point[1];
let z = point[2];
let w = point[3];
// Multiply the point against each part of the 1st column, then add together
let resultX = x * c0r0 + y * c0r1 + z * c0r2 + w * c0r3;
// Multiply the point against each part of the 2nd column, then add together
let resultY = x * c1r0 + y * c1r1 + z * c1r2 + w * c1r3;
// Multiply the point against each part of the 3rd column, then add together
let resultZ = x * c2r0 + y * c2r1 + z * c2r2 + w * c2r3;
// Multiply the point against each part of the 4th column, then add together
let resultW = x * c3r0 + y * c3r1 + z * c3r2 + w * c3r3;
return [resultX, resultY, resultZ, resultW];
}
注意:本页上的示例使用行向量来表示点,并使用右乘法来应用变换矩阵。也就是说,上述操作执行 point * matrix
,其中 point
是一个 4x1 行向量。如果你想使用列向量和左乘法,你需要相应地调整乘法函数,并对下面介绍的每个矩阵进行转置。
例如,下面介绍的 translationMatrix
原来看起来像这样
[1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1]
转置后,它看起来像这样
[1, 0, 0, x,
0, 1, 0, y,
0, 0, 1, z,
0, 0, 0, 1]
现在,使用上面的函数,我们可以将点乘以矩阵。使用单位矩阵,它应该返回一个与原始点相同的点,因为点(或任何其他矩阵)乘以单位矩阵始终等于它本身
// sets identityResult to [4,3,2,1]
let identityResult = multiplyMatrixAndPoint(identityMatrix, [4, 3, 2, 1]);
返回相同的点并没有什么用,但还有其他类型的矩阵可以对点执行有用的操作。接下来的部分将演示其中一些矩阵。
将两个矩阵相乘
除了将矩阵和点相乘之外,你还可以将两个矩阵相乘。上面的函数可以重复使用来帮助完成此过程
//matrixB • matrixA
function multiplyMatrices(matrixA, matrixB) {
// Slice the second matrix up into rows
let row0 = [matrixB[0], matrixB[1], matrixB[2], matrixB[3]];
let row1 = [matrixB[4], matrixB[5], matrixB[6], matrixB[7]];
let row2 = [matrixB[8], matrixB[9], matrixB[10], matrixB[11]];
let row3 = [matrixB[12], matrixB[13], matrixB[14], matrixB[15]];
// Multiply each row by matrixA
let result0 = multiplyMatrixAndPoint(matrixA, row0);
let result1 = multiplyMatrixAndPoint(matrixA, row1);
let result2 = multiplyMatrixAndPoint(matrixA, row2);
let result3 = multiplyMatrixAndPoint(matrixA, row3);
// Turn the result rows back into a single matrix
return [
result0[0],
result0[1],
result0[2],
result0[3],
result1[0],
result1[1],
result1[2],
result1[3],
result2[0],
result2[1],
result2[2],
result2[3],
result3[0],
result3[1],
result3[2],
result3[3],
];
}
让我们看看这个函数在实际中的应用
let someMatrix = [4, 0, 0, 0, 0, 3, 0, 0, 0, 0, 5, 0, 4, 8, 4, 1];
let identityMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
// Returns a new array equivalent to someMatrix
let someMatrixResult = multiplyMatrices(identityMatrix, someMatrix);
警告:这些矩阵函数是为了解释清楚而编写的,而不是为了速度或内存管理。这些函数创建了大量的新的数组,由于垃圾回收,这对于实时操作来说可能非常昂贵。在实际的生产代码中,最好使用优化的函数。 glMatrix 是一个专注于速度和性能的库示例。glMatrix 库的重点是在更新循环之前分配目标数组。
平移矩阵
平移矩阵基于单位矩阵,用于 3D 图形中沿一个或多个三个方向(x
、y
和/或 z
)移动点或物体。最简单的理解平移的方法就像拿起一个咖啡杯。咖啡杯必须保持直立并以相同的方式定向,这样就不会洒出咖啡。它可以从桌子上向上移动到空中,并在空中移动。
你实际上无法只用平移矩阵喝咖啡,因为要喝咖啡,你必须能够倾斜或旋转杯子,将咖啡倒入你的嘴里。我们将在后面看看用于执行此操作的矩阵类型(巧妙地称为旋转矩阵)。
let x = 50;
let y = 100;
let z = 0;
let translationMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1];
将三个轴上的距离分别放置在平移矩阵中的相应位置,然后将它乘以需要在 3D 空间中移动的点或矩阵。
使用矩阵操作 DOM
使用矩阵的一个非常简单的办法是使用 CSS matrix3d()
transform
。首先,我们将设置一个简单的 <div>
,其中包含一些内容。样式没有显示,但它被设置为固定宽度和高度,并且在页面上居中。<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>
最后,对于每个示例,我们将生成一个 4×4 矩阵,然后更新 <div>
的样式,使其应用变换,并将其设置为 matrix3d
。请记住,即使矩阵由 4 行 4 列组成,它也会折叠成一行 16 个值。在 JavaScript 中,矩阵始终存储在一维列表中。
// Create the matrix3d style property from a matrix array
function matrixArrayToCssMatrix(array) {
return `matrix3d(${array.join(",")})`;
}
// Grab the DOM element
let moveMe = document.getElementById("move-me");
// Returns a result like: "matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 50, 100, 0, 1);"
let matrix3dRule = matrixArrayToCssMatrix(translationMatrix);
// Set the transform
moveMe.style.transform = matrix3dRule;
缩放矩阵
缩放矩阵使物体在一个或多个三个维度(宽度、高度和深度)上变大或变小。在典型的(笛卡尔)坐标系中,这会导致物体在相应的方向上拉伸或收缩。
要应用于宽度、高度和深度的变化量分别放置在对角线上,从左上角开始,向下延伸到右下角。
let w = 1.5; // width (x)
let h = 0.7; // height (y)
let d = 1; // depth (z)
let scaleMatrix = [w, 0, 0, 0, 0, h, 0, 0, 0, 0, d, 0, 0, 0, 0, 1];
旋转矩阵
旋转矩阵用于旋转点或物体。旋转矩阵看起来比缩放和变换矩阵复杂一些。它们使用三角函数来执行旋转。虽然本节不会将步骤分解成详尽的细节(有关详细信息,请查看 Wolfram MathWorld 上的这篇文章),但请参考以下示例进行说明。
首先,以下代码在不使用矩阵的情况下将点绕原点旋转。
// Manually rotating a point about the origin without matrices
let point = [10, 2];
// Calculate the distance from the origin
let distance = Math.sqrt(point[0] * point[0] + point[1] * point[1]);
// The equivalent of 60 degrees, in radians
let rotationInRadians = Math.PI / 3;
let transformedPoint = [
Math.cos(rotationInRadians) * distance,
Math.sin(rotationInRadians) * distance,
];
可以将这些类型的步骤编码到矩阵中,并针对 x
、y
和 z
的每个维度执行。下面是在左手坐标系中绕 Z 轴逆时针旋转的表示
let sin = Math.sin;
let 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
let a = Math.PI * 0.3; //Rotation amount in radians
// Rotate around Z axis
let rotateZMatrix = [
cos(a),
-sin(a),
0,
0,
sin(a),
cos(a),
0,
0,
0,
0,
1,
0,
0,
0,
0,
1,
];
以下是一组函数,它们返回用于绕三个轴旋转的旋转矩阵。一个重要的注意事项是,没有应用透视,因此它可能感觉不太 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];
}
矩阵合成
矩阵的真正强大之处在于矩阵组合。当特定类别的矩阵相乘时,它们会保留变换的历史记录并可逆。这意味着如果将平移、旋转和缩放矩阵组合在一起,当矩阵的顺序反转并重新应用时,原始点将被返回。
矩阵相乘的顺序很重要。当乘以数字时,a * b = c 和 b * a = c 都是真的。例如,3 * 4 = 12,而 4 * 3 = 12。在数学中,这些数字被称为可交换的。如果矩阵的顺序颠倒,矩阵不保证相同,因此矩阵是不可交换的。
另一个令人费解的事情是,WebGL 和 CSS 中的矩阵乘法需要以与操作直观发生顺序相反的顺序进行。例如,要将某个物体缩小 80%,向下移动 200 像素,然后围绕原点旋转 90 度,在伪代码中看起来像这样。
transformation = rotate * translate * scale
组合多个变换
我们将用来组合矩阵的函数是multiplyArrayOfMatrices()
,它是本文开头介绍的实用函数集的一部分。它接受一个矩阵数组并将它们相乘,返回结果。在 WebGL 着色器代码中,这是内置在语言中的,可以使用*
运算符。此外,此示例使用scale()
和translate()
函数,它们返回如上定义的矩阵。
let transformMatrix = MDN.multiplyArrayOfMatrices([
rotateAroundZAxis(Math.PI * 0.5), // Step 3: rotate around 90 degrees
translate(0, 200, 0), // Step 2: move down 200 pixels
scale(0.8, 0.8, 0.8), // Step 1: scale down
]);
最后,一个有趣的一步是展示矩阵如何工作,那就是反转步骤,将矩阵恢复到原始的单位矩阵。
let transformMatrix = MDN.multiplyArrayOfMatrices([
scale(1.25, 1.25, 1.25), // Step 6: scale back up
translate(0, -200, 0), // Step 5: move back up
rotateAroundZAxis(-Math.PI * 0.5), // Step 4: rotate back
rotateAroundZAxis(Math.PI * 0.5), // Step 3: rotate around 90 degrees
translate(0, 200, 0), // Step 2: move down 200 pixels
scale(0.8, 0.8, 0.8), // Step 1: scale down
]);
为什么矩阵很重要
矩阵很重要,因为它们包含一小部分数字,可以描述空间中各种各样的变换。它们可以在程序中轻松地共享。不同的坐标空间可以用矩阵来描述,一些矩阵乘法会将一组数据从一个坐标空间移动到另一个坐标空间。矩阵有效地记住生成它们的之前所有变换的每个部分。
对于 WebGL 中的使用,显卡在将大量空间点乘以矩阵方面特别出色。定位点、计算光照和摆放动画角色等不同的操作都依赖于这个基本工具。