11 · Vector3:算术与运动更新

返回系列目录 · 上一章:数据操作 · 下一章:长度与约束

本章所有方法都会修改接收者并返回 this;参数向量在与接收者不是同一个对象时保持不变。示例默认 import * as THREE from 'three';。先给每个量标明意义:位置的单位可能是米,速度是米/秒,帧间隔是秒。只有 速度 × 时间 才能作为位移加到位置上。

add(v)

逐分量执行 this += v。(1,2,3)+(4,0,-1)=(5,2,2)。位置加位移得到新位置,速度加速度增量得到新速度。两者应使用相同轴方向和单位;两个位置直接相加通常需要进一步解释,例如计算中点时再除以 2。

position.add(displacement); // 消耗的是位移,不是每秒速度

addScalar(s)

三个分量都加同一个数。(1,2,3).addScalar(2) 得 (3,4,5),等价于加 (2,2,2)。可把随机区间整体平移,例如 random().addScalar(-0.5);不能理解为“沿原方向前进 2 米”,其位移长度是 2√3。

addVectors(a, b)

把 a+b 写入接收者,不累加接收者旧值。result.addVectors((1,0,0),(0,2,0)) 得 (1,2,0)。适合把基础偏移和动画偏移组合到复用的输出对象,避免修改配置数据。

addScaledVector(v, s)

计算 this += s*v,v 保持不变。(1,0,0).addScaledVector((2,0,0),0.5) 得 (2,0,0)。这是运动更新最常用的接口:position.addScaledVector(velocity, dt),无需先创建 velocity.clone().multiplyScalar(dt)。r185 也接受 Vector4,但仅使用前三个分量,w 不参与。

sub(v)

计算 this -= v。target.clone().sub(origin) 得到从 origin 指向 target 的位移。例如目标 (5,0,0)、角色 (2,0,0),结果是 (3,0,0)。写成 origin.sub(target) 不仅方向反了,还会破坏原位置。

subScalar(s)

三个分量分别减去 s。(3,4,5).subScalar(2) 得 (1,2,3)。用于统一偏置消除或把 [0,1) 随机值映射到 [-0.5,0.5),不是减小向量长度 s;部分分量越过零时还会改变方向。

subVectors(a, b)

把 a-b 写入接收者。offset.subVectors(target, current) 是高频写法,清楚地保留两个位置并复用 offset。运算顺序决定朝向:终点减起点。父子层级不同的 object.position 不能直接这样相减,应先获取世界位置。

multiply(v)

逐分量乘法,也称 Hadamard 乘积:(2,3,4)*(5,2,0)=(10,6,0)。适合轴向缩放或用 (1,0,1) 清除 Y 分量。它既不是点积(返回标量),也不是叉积(生成垂直向量),更不能替代一般旋转矩阵。

multiplyScalar(scalar)

每个分量乘同一标量。(3,4,0)*2=(6,8,0),长度翻倍且方向不变;乘负数会反向;乘零变为零向量。适合单位转换,例如毫米转米乘 0.001。缩放位置是围绕该坐标系原点进行,围绕任意中心需先减中心、缩放、再加回。

multiplyVectors(a, b)

把逐分量乘积 a⊙b 写入接收者。result.multiplyVectors(baseSize, scale) 可计算无旋转盒子的各轴尺寸,但旋转后的世界轴对齐包围盒尺寸不能这样求,应使用 Box3。

divide(v)

逐分量相除。(8,9,10).divide((2,3,5))=(4,3,2)。可以逆转单纯的轴向缩放,但不能逆转包含旋转和剪切的一般变换。分母分量为零不会抛异常,而会得到 Infinity 或 NaN;应在运算前检查。

divideScalar(scalar)

三个分量除以 scalar,内部实现是乘以 1/scalar。(6,8,0)/2=(3,4,0)。例如求两点中点:midpoint.addVectors(a,b).divideScalar(2)。scalar 为零不会自动兜底,normalize() 对零长度的防护不意味着这里也有防护。

negate()

无参数,把每个分量取负,即乘以 -1。(1,-2,3) 变成 (-1,2,-3)。用于反转运动或法线方向,长度不变。但法线朝向翻转不等同于修正三角形顶点绕序,也不会自动改变材质剔除设置。

实践:带重力的速度更新

import * as THREE from 'three';

const position = new THREE.Vector3(0, 2, 0);
const velocity = new THREE.Vector3(3, 5, 0);
const acceleration = new THREE.Vector3(0, -9.81, 0);

function step(dt) {
  // 世界空间;米、秒。先更新速度再更新位置,是半隐式 Euler 积分。
  velocity.addScaledVector(acceleration, dt);
  position.addScaledVector(velocity, dt);
  if (position.y < 0) {
    position.y = 0;
    if (velocity.y < 0) velocity.y *= -0.6;
  }
}

for (let i = 0; i < 60; i++) step(1 / 60);
console.log(position.toArray(), velocity.toArray());

重力使每秒 Y 速度减少 9.81。把反弹系数从 0.6 改为 1 会减少碰撞造成的能量损失,但离散积分仍有误差。这段代码适合教学或简单粒子,不是连续碰撞检测:大 dt 会让球穿入地面后才纠正。交互程序可使用固定步长累积时间;单纯截断 dt 会在卡顿时丢失模拟时间,需要根据产品目标选择。

练习(暂不附答案)

  1. 用 addVectors 和 multiplyScalar 求两个世界位置的中点,确保原位置不变。
  2. 把速度单位从厘米/秒换成米/秒,预测不转换时动画速度相差多少。
  3. 写一个围绕指定中心、按三轴不同比例缩放点的位置函数,输出对象由调用者提供。
  4. 比较上面运动例子在 dt 为 1/30 和 1/120 时一秒后的结果,解释为什么使用 dt 仍不保证积分结果完全一致。