Skip to content

矩阵的概念与表示

向量能够表示点、方向和位移,但有一类重要的操作是向量无法完成的:变换(Transform)。

思考几个问题:

  1. 如何将一个物体旋转 45°?
  2. 如何将一个物体放大 2 倍?
  3. 如何同时旋转、缩放、平移一个物体?

答案是:使用矩阵

什么是矩阵

矩阵是一个数字的矩形阵列,由行和列组成。

一个 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 个元素

现在我们有了矩阵的基础结构,下一章将学习矩阵的加减和数乘运算。


练习

  1. 创建一个 Matrix4 对象并打印
  2. 修改第 1 行第 2 列的元素为 7,验证修改成功
  3. 计算元素 $m_{32}$ 在列主序数组中的索引(提示:col * 4 + row)
  4. 思考:为什么单位矩阵对任何向量的变换都是"不变"?

尝试在浏览器控制台中完成这些练习。

矩阵的概念与表示 has loaded