03 · Local Space 与 World Space

返回目录 · 上一章 · 下一章

1. 一个位置必须带着参照系

车上的杯架在车辆坐标中始终是固定位置,但车开走以后,它的世界位置改变。局部坐标适合描述“相对于所属物体”,世界坐标适合让互不相关的对象交流。

Object3D 的 position 表示物体原点在父节点坐标系中的位置。几何体顶点的 position attribute 则在该网格自身的局部坐标中。两者名字相同,层级并不相同。

对于列向量:

pworld=Mworldplocal,Mworld=MparentWorldMlocal p_{world}=M_{world}p_{local},\qquad M_{world}=M_{parentWorld}M_{local}

右侧矩阵先作用。父节点世界矩阵已经包含祖先节点的影响。

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 后,还需自行管理局部矩阵更新。

练习(不附答案)

  1. 给车辆增加缩放 (2,1,1),手算杯架世界位置后用代码检查。
  2. 在两个不同父节点下各放置一个对象,编写不修改对象状态的世界距离查询函数。
  3. 将一个世界目标点转换成杯架可以直接赋给 position 的坐标。
  4. 比较 applyMatrix3 和 transformDirection 对非均匀缩放后向量长度的影响。