Appearance
矩阵的概念与表示
向量能够表示点、方向和位移,但有一类重要的操作是向量无法完成的:变换(Transform)。
思考几个问题:
- 如何将一个物体旋转 45°?
- 如何将一个物体放大 2 倍?
- 如何同时旋转、缩放、平移一个物体?
答案是:使用矩阵。
什么是矩阵
矩阵是一个数字的矩形阵列,由行和列组成。
一个 3×3 矩阵(3 行 3 列):
$$ \mathbf{M} = \begin{bmatrix} m_{11} & m_{12} & m_{13} \ m_{21} & m_{22} & m_{23} \ m_{31} & m_{32} & m_{33} \end{bmatrix} $$
例如:
$$ \begin{bmatrix} 1 & 2 & 3 \ 4 & 5 & 6 \ 7 & 8 & 9 \end{bmatrix} $$
在 3D 图形学中,我们主要使用 4×4 矩阵:
$$ \mathbf{M} = \begin{bmatrix} m_{11} & m_{12} & m_{13} & m_{14} \ m_{21} & m_{22} & m_{23} & m_{24} \ m_{31} & m_{32} & m_{33} & m_{34} \ m_{41} & m_{42} & m_{43} & m_{44} \end{bmatrix} $$
为什么是 4×4 而不是 3×3?因为 4×4 矩阵可以同时表示旋转、缩放、平移,而 3×3 矩阵无法表示平移(后面会详细讲解)。
矩阵的几何意义:变换
矩阵的核心作用是对向量进行变换。
一个矩阵可以看作是一个"变换器":
输入向量 → [矩阵] → 输出向量例如,一个旋转矩阵可以将向量 (1, 0, 0) 旋转90°变为 (0, 1, 0)。
不同的矩阵代表不同的变换:
- 单位矩阵:不做任何改变
- 缩放矩阵:放大或缩小
- 旋转矩阵:旋转
- 平移矩阵:移动位置
矩阵的组成:列向量
一个矩阵可以看作是几个列向量组成的。
对于 4×4 矩阵:
$$ \mathbf{M} = \begin{bmatrix} | & | & | & | \ \mathbf{c_1} & \mathbf{c_2} & \mathbf{c_3} & \mathbf{c_4} \ | & | & | & | \end{bmatrix} $$
其中:
- $\mathbf{c_1} = (m_{11}, m_{21}, m_{31}, m_{41})$ 是第一列
- $\mathbf{c_2} = (m_{12}, m_{22}, m_{32}, m_{42})$ 是第二列
- 以此类推
在变换矩阵中,前三列通常代表变换后的坐标轴方向,第四列代表平移。
矩阵的存储:行主序 vs 列主序
在内存中存储矩阵有两种方式:
行主序(Row-Major):按行存储
javascript
[m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44]列主序(Column-Major):按列存储
javascript
[m11, m21, m31, m41, m12, m22, m32, m42, m13, m23, m33, m43, m14, m24, m34, m44]不同的图形库采用不同的约定:
- OpenGL / WebGL:列主序
- DirectX:行主序
- Three.js:列主序(遵循 WebGL)
本书采用列主序,与 WebGL 和 Three.js 保持一致。
代码实现:Matrix4 类
让我们实现一个基础的 Matrix4 类:
javascript
class Matrix4 {
constructor() {
// 使用一维数组存储16个元素(列主序)
this.elements = [
1, 0, 0, 0, // 第一列
0, 1, 0, 0, // 第二列
0, 0, 1, 0, // 第三列
0, 0, 0, 1 // 第四列
];
}
// 设置矩阵元素(按行列索引)
set(row, col, value) {
// 列主序:index = col * 4 + row
this.elements[col * 4 + row] = value;
}
// 获取矩阵元素
get(row, col) {
return this.elements[col * 4 + row];
}
// 用于调试的字符串表示
toString() {
const e = this.elements;
return `Matrix4(
${e[0].toFixed(2)}, ${e[4].toFixed(2)}, ${e[8].toFixed(2)}, ${e[12].toFixed(2)}
${e[1].toFixed(2)}, ${e[5].toFixed(2)}, ${e[9].toFixed(2)}, ${e[13].toFixed(2)}
${e[2].toFixed(2)}, ${e[6].toFixed(2)}, ${e[10].toFixed(2)}, ${e[14].toFixed(2)}
${e[3].toFixed(2)}, ${e[7].toFixed(2)}, ${e[11].toFixed(2)}, ${e[15].toFixed(2)}
)`;
}
}默认的矩阵是单位矩阵(主对角线为 1,其他为 0):
$$ \mathbf{I} = \begin{bmatrix} 1 & 0 & 0 & 0 \ 0 & 1 & 0 & 0 \ 0 & 0 & 1 & 0 \ 0 & 0 & 0 & 1 \end{bmatrix} $$
单位矩阵的作用是"什么都不做",类似数字中的 1(任何数乘以 1 等于它自己)。
使用示例:
javascript
const matrix = new Matrix4();
console.log(matrix.toString());
// 修改某个元素
matrix.set(0, 3, 5); // 第0行第3列设为5
console.log(matrix.get(0, 3)); // 5为什么需要 4×4 矩阵?
你可能会问:3D 空间只有 x、y、z 三个维度,为什么需要 4×4 矩阵而不是 3×3?
答案是:为了支持平移变换。
3×3 矩阵只能表示线性变换(旋转、缩放、镜像),无法表示平移(将物体从一个位置移动到另一个位置)。
举例:如果想将点 (1, 2, 3) 平移到 (4, 5, 6),用 3×3 矩阵无法实现。
解决方案是使用齐次坐标系统:在 3D 坐标 (x, y, z) 后面加上第四个分量 w,变为 (x, y, z, w)。
通常 w = 1 表示点,w = 0 表示方向向量。
这样,4×4 矩阵的第四列就可以用来表示平移:
$$ \begin{bmatrix} 1 & 0 & 0 & t_x \ 0 & 1 & 0 & t_y \ 0 & 0 & 1 & t_z \ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \ y \ z \ 1 \end{bmatrix} = \begin{bmatrix} x + t_x \ y + t_y \ z + t_z \ 1 \end{bmatrix} $$
这将在后续章节中详细讲解。
矩阵的索引约定
在后续代码中,我们约定:
- 行索引:0, 1, 2, 3(从上到下)
- 列索引:0, 1, 2, 3(从左到右)
- 数组索引:
col * 4 + row(列主序)
例如,元素 $m_{23}$(第2行第3列)在数组中的索引是 3 * 4 + 2 = 14。
这个约定与 Three.js 和 WebGL 一致。
小结
在本章中,我们学习了:
- 矩阵的定义:数字的矩形阵列
- 矩阵的作用:对向量进行变换(旋转、缩放、平移)
- 4×4 矩阵:3D 图形学的标准矩阵,支持平移
- 存储方式:列主序(遵循 WebGL/Three.js)
- Matrix4 类:用一维数组存储 16 个元素
现在我们有了矩阵的基础结构,下一章将学习矩阵的加减和数乘运算。
练习:
- 创建一个
Matrix4对象并打印 - 修改第 1 行第 2 列的元素为 7,验证修改成功
- 计算元素 $m_{32}$ 在列主序数组中的索引(提示:col * 4 + row)
- 思考:为什么单位矩阵对任何向量的变换都是"不变"?
尝试在浏览器控制台中完成这些练习。