01 · 点、向量、方向与单位
本章目标:看到一段位置更新代码时,能够说明每个量表示什么、处于哪个空间、使用什么单位。后续公式默认右手坐标系、列向量,角度使用弧度。
1. 相同的三个数字,可以有不同的含义
new THREE.Vector3(3, 0, 4) 只是一个存储三个分量的对象。它可以表示世界位置、位移、速度、法线或 RGB 数据;类名并不能保证这些含义可以混用。
点描述“在哪里”,依赖坐标原点;位移描述“从这里到那里”,是两个点的差。设仓库中机器人的位置为 A,充电站为 B:
沿位移 d 走,机器人能抵达充电站。把整个仓库平移 100 米后,A、B 的坐标改变,但它们的差不变。
import * as THREE from 'three';
const robot = new THREE.Vector3(1, 0, 2);
const station = new THREE.Vector3(4, 0, 6);
const displacement = new THREE.Vector3().subVectors(station, robot);
const destination = robot.clone().add(displacement);
console.log(displacement.toArray()); // [3, 0, 4]
console.log(destination.equals(station)); // true
这里必须保留原始位置,因此使用 clone() 和独立输出变量。station.sub(robot) 会直接修改 station,造成充电站悄悄移动。
常用的语义规则是:点减点得到位移,点加位移得到点,位移加位移仍是位移。两点直接相加虽能计算,却通常需要进一步除以 2 才能解释为中点。
2. 长度和方向分开管理
欧氏长度来自勾股定理:
单位向量只保留朝向。若速度为每秒 2 个世界单位,则速度向量为 direction * 2,而不是直接把 displacement 当成速度。后者会让离目标越远的机器人跑得越快。
const distance = displacement.length();
const direction = displacement.clone();
if (distance > 1e-8) direction.divideScalar(distance);
else direction.set(0, 0, 0); // 到达阈值内停止,不能把未归一化的小位移当朝向。
const velocity = direction.clone().multiplyScalar(2);
const dt = 0.1; // 秒
robot.addScaledVector(velocity, dt); // 位移 = 速度 × 时间
归一化不能从零向量中创造朝向。若机器人已经到站,业务应停止移动或保留上一次朝向,而不是期待数学库猜测一个方向。
3. 用量纲检查代码
Three.js 不强制米或厘米。项目应约定一个世界单位代表什么,并在模型导入边界转换。例如模型使用毫米,业务使用米,就将长度乘以 0.001。
| 量 | 建议单位 | 常见关系 |
|---|---|---|
| 位置、距离 | 世界单位,例如米 | 位置差得到位移 |
| 速度 | 世界单位/秒 | 乘 dt 得到位移 |
| 加速度 | 世界单位/秒² | 乘 dt 得到速度变化 |
| 方向、法线 | 无量纲,通常单位长度 | 乘速度得到速度向量 |
| 角度 | 弧度 | Math.PI 等于 180° |
position.add(velocity) 缺少时间因子,帧率越高就跑得越快。distance < radius * radius 则把长度和长度平方比较,量纲已经不一致。比较平方距离时,应写 distanceToSquared(target) < radius * radius。
4. 分量不是绝对方向
x 表示相对于当前基底第一根轴的分量,不保证永远是屏幕右侧。角色转身后,它的局部右方向会改变,但世界 X 轴没有变。变量命名如 targetWorld、velocityLocal 可以提前暴露跨空间混用。
同一空间也不意味着同一种量:世界位置不能直接与世界法线相加来表达物理运动,除非明确将单位法线乘上一个位移长度,例如“沿法线抬高 0.01 米”。
练习(不附答案)
- 给定 A=(2,1,-1)、B=(-1,1,3),求位移、距离和单位方向,并用代码验证。
- 机器人每秒移动 3 米,分别使用 1/30 和 1/120 秒的步长模拟一秒。说明什么条件下终点应该相同。
- 写一个计算两点中点的函数,要求不修改输入对象。
- 排查
position.add(direction.multiplyScalar(speed))的问题:分别考虑帧率和重复修改 direction。