03 · Local Space 与 World Space
1. 一个位置必须带着参照系
车上的杯架在车辆坐标中始终是固定位置,但车开走以后,它的世界位置改变。局部坐标适合描述“相对于所属物体”,世界坐标适合让互不相关的对象交流。
Object3D 的 position 表示物体原点在父节点坐标系中的位置。几何体顶点的 position attribute 则在该网格自身的局部坐标中。两者名字相同,层级并不相同。
对于列向量:
右侧矩阵先作用。父节点世界矩阵已经包含祖先节点的影响。
2. 车辆与杯架:可直接运行的数值实验
import * as THREE from 'three';
const car = new THREE.Group();
car.position.set(10, 0, 0);
car.rotation.y = Math.PI / 2;
const cup = new THREE.Object3D();
cup.position.set(0, 0, 2);
car.add(cup);
car.updateWorldMatrix(true, true);
const worldPosition = cup.getWorldPosition(new THREE.Vector3());
console.log(worldPosition.toArray()); // 约 [12, 0, 0]
const inCar = car.worldToLocal(worldPosition.clone());
console.log(inCar.toArray()); // 约 [0, 0, 2]
旋转让车辆局部 +Z 指向世界 +X,再加世界平移得到杯架位置。这里不能写 cup.position.distanceTo(other.position) 来比较不同车辆上的杯子,应先获取双方世界位置。
localToWorld 和 worldToLocal 会修改传入的 Vector3。如果仍需要原值,要传副本。逆变换要求矩阵可逆,包含零缩放的层级无法唯一恢复局部位置。
3. 基底如何进入矩阵
在线性变换的 3×3 部分中,三列分别描述局部 X、Y、Z 单位基向量变换后的方向和长度。旋转改变方向,缩放改变长度,剪切使基向量不再正交。
局部向量 (2,1,0) 的世界结果,就是“第一列乘 2,加第二列乘 1”。这让矩阵乘法成为基向量的线性组合,而不是需要死记的九个乘法。
4. 点、位移和单位朝向要区别对待
const worldPoint = new THREE.Vector3(0, 0, 2).applyMatrix4(car.matrixWorld);
const linear = new THREE.Matrix3().setFromMatrix4(car.matrixWorld);
const worldDisplacement = new THREE.Vector3(0, 0, 2).applyMatrix3(linear);
const worldHeading = new THREE.Vector3(0, 0, 1).transformDirection(car.matrixWorld);
第一个包含平移;第二个保留线性部分造成的长度变化;第三个忽略平移并归一化,适合只需要朝向的情况,不适合保留速度大小。法线有不同规则,见法线矩阵。
当你知道世界目标点,想设置某个子物体的 position,应使用该物体父节点的 worldToLocal,不是物体自身的逆矩阵。
5. 更新时机就是数据有效期
渲染通常会更新场景矩阵,但在第一次渲染前、修改后立即查询、或自主管理矩阵时,不能假定缓存始终最新。updateWorldMatrix(true, false) 可更新祖先和当前节点;需要后代一起更新时第二个参数设 true。关闭 matrixAutoUpdate 后,还需自行管理局部矩阵更新。
练习(不附答案)
- 给车辆增加缩放
(2,1,1),手算杯架世界位置后用代码检查。 - 在两个不同父节点下各放置一个对象,编写不修改对象状态的世界距离查询函数。
- 将一个世界目标点转换成杯架可以直接赋给
position的坐标。 - 比较 applyMatrix3 和 transformDirection 对非均匀缩放后向量长度的影响。