17 · Matrix3:几何直觉、创建与存储

返回系列目录 · 下一章:组合变换

本章至第 21 章按项目安装的 Three.js r185 源码逐项讲解 Matrix3。代码使用 import * as THREE from 'three';,可放进项目示例的 JavaScript 入口运行。Matrix3 保存九个数字;它既可以表达三维线性变换,也可以表达二维齐次变换,含义由输入数据约定决定。

一列就是一个基向量的去向

列向量约定下,若矩阵三列为 axisX、axisY、axisZ,则:

M[xyz]=x axisX+y axisY+z axisZ. M\begin{bmatrix}x\\y\\z\end{bmatrix} =x\,axisX+y\,axisY+z\,axisZ.

例如 diag(2, 3, 1) 把单位 X 方向变为 (2, 0, 0),把单位 Y 方向变为 (0, 3, 0);点 (1, 2, 3) 变为 (2, 6, 3)。这不是记忆乘法口诀,而是在重建变换后的坐标基底。

三维线性变换必然把原点映射到原点,因此一般三维平移不能只用 Matrix3。二维点可以编码为 (x, y, 1),把第三维作为齐次分量,此时第三列可以承载二维平移;不要再把其中的 1 理解为三维高度。

constructor(n11, …, n33)

无参数构造得到单位矩阵,返回一个新 Matrix3。传入参数时必须完整提供九个数,顺序按照纸面上的行:

const stretch = new THREE.Matrix3(
  2, 0, 0,
  0, 3, 0,
  0, 0, 1
);
const point = new THREE.Vector3(1, 2, 3).applyMatrix3(stretch);
console.log(point.toArray()); // [2, 6, 3]

构造函数不会验证缺失参数;只传第一个数并不代表统一缩放,其他分量可能成为 undefined,随后传播为 NaN。单位矩阵在数学上是“不改变输入”的变换,并非全零矩阵。

elements

这是可写的、长度为九的普通数组,按列主序存储。它不是返回副本的 getter:直接修改数组会改变矩阵。

const matrix = new THREE.Matrix3().set(
  1, 2, 3,
  4, 5, 6,
  7, 8, 9
);
console.log(matrix.elements); // [1, 4, 7, 2, 5, 8, 3, 6, 9]

从零计数的 (row, column) 存在 elements[column * 3 + row]。例如第三列第一行在索引 6。列主序只是存储规则,不意味着向量必须写成行向量,也不意味着上传 uniform 前应手动转置。日常代码优先通过 set() 和语义方法修改,减少索引错误。

isMatrix3

只读类型标记,r185 定义在原型上,读取结果为 true。value && value.isMatrix3 === true 可做轻量类型判断,不修改对象。它不是数值有效性验证:包含 NaN 的矩阵同样有这个标记;外部对象也能伪造标记。

set(n11, …, n33)

九个参数按行给出,覆盖接收者全部元素,返回 this,允许链式调用。不修改任何输入对象,因为参数是数字。

const shear = new THREE.Matrix3().set(
  1, 0.5, 0,
  0, 1,   0,
  0, 0,   1
);
console.log(new THREE.Vector3(0, 2, 0).applyMatrix3(shear).toArray());
// [1, 2, 0]:Y 越高,沿 X 的偏移越大,这是剪切。

set() 不会保留旧值,也不接受一个九元素数组作为替代参数;数组输入应使用 fromArray()。

identity()

无参数,覆盖接收者为对角线为 1、其余为 0 的单位矩阵,返回 this。任何向量经过它保持不变。适合在重算变换前清除上一帧状态,避免无意累积变换;不要在需要继续累积的地方重置。

copy(matrix)

把参数 Matrix3 的九个数复制到接收者,返回 this,源矩阵不变。cached.copy(source) 更新既有对象,适合渲染循环复用。

const source = new THREE.Matrix3().makeScale(2, 3);
const cached = new THREE.Matrix3().copy(source);
source.identity();
console.log(cached.elements[0]); // 2,数组没有共享。

cached = source 则是共享同一个对象,不是复制。复制只保存当前数字,不会跟踪源矩阵后续变化。

clone()

无参数,返回包含相同数字的新 Matrix3,不改变原矩阵。适合保存初始状态或在一次性推导中保留原值,例如 const inverse = source.clone().invert()。与 copy() 的区别在于分配新对象;每帧大量 clone() 会制造垃圾回收压力。

equals(matrix)

逐元素使用严格相等比较,返回布尔值,两边都不修改。单位矩阵与其副本相等;旋转再逆旋转的结果可能因为舍入误差不与单位矩阵严格相等。

数值检验应结合绝对与相对误差:

function nearlyEqualMatrix(a, b, absTol = 1e-10, relTol = 1e-10) {
  return a.elements.every((value, index) => {
    const other = b.elements[index];
    return Number.isFinite(value) && Number.isFinite(other) &&
      Math.abs(value - other) <= absTol + relTol * Math.max(Math.abs(value), Math.abs(other));
  });
}

容差要结合业务尺度设置;模型单位为毫米与天文单位时不能机械使用同一阈值。

fromArray(array, offset = 0)

从数组或兼容的数值数组读取连续九个元素,按列主序覆盖接收者,返回 this;不修改数组。offset 是元素索引,不是字节偏移。

const matrix = new THREE.Matrix3().fromArray([
  99, 2, 0, 0, 0, 3, 0, 0, 0, 1
], 1);
// matrix 是 diag(2, 3, 1),99 被跳过。

长度不足不会得到友好异常,而会读入 undefined。外部数据应先检查长度、布局和有限性。不要把逐行导出的 CSV 数字直接当成列主序数组。

toArray(array = [], offset = 0)

按列主序把九个数字写入目标数组,返回目标数组,不修改矩阵;省略数组会新建数组。用于序列化或填充批量数据缓冲区。

const packed = new Float32Array(18);
new THREE.Matrix3().makeScale(2, 3).toArray(packed, 9);
console.log(packed[9], packed[13], packed[17]); // 2, 3, 1

目标 TypedArray 必须有足够容量;Float32Array 还会把 JavaScript 的双精度数舍入为单精度。该方法不是转置,返回数组也不是矩阵的实时视图。

练习

  1. 写出把 (x,y,z) 映射为 (z,x,y) 的矩阵,分别预测其 set() 参数和 elements 内容。
  2. 用三个单位基向量验证上面的剪切矩阵,说明它为什么不能只用缩放表达。
  3. 将两个矩阵写进同一个 Float32Array,再用不同 offset 读回,检验复制是否独立。
  4. 构造两个数学上等价、严格 equals() 却可能失败的计算结果,并设计符合场景的误差检查。