13 · Vector3:点积、叉积、投影与反射

返回系列目录 · 上一章:长度与约束 · 下一章:插值与采样

把运动拆成沿墙和朝墙的两部分,可以直接解释贴墙滑行;把目标方向分解到角色前方,可以解释视野检测。几何接口的价值正在于这种分解。所有输入先统一坐标空间,不能把局部法线直接用于世界速度。

dot(v)

返回数值 x*v.x + y*v.y + z*v.z,不修改任意向量。几何式为

a⋅b=∣a∣∣b∣cos⁡θ. a\cdot b=|a||b|\cos\theta.

(3,4,0)·(1,0,0)=3,表示沿单位 X 方向的有符号分量。若第二个向量改为 (2,0,0),结果变为 6,因此点积本身不是距离,也不总是余弦。两边都是单位向量时才直接得到 cosθ。

前后判断只关心符号,可以不归一化;夹角阈值判断通常需要单位方向,或者显式乘长度。拿拉行李举例,沿水平前方的拉力分量是 force.dot(forwardUnit);与位移点积还可表示功,其单位随输入变为焦耳。

cross(v)

把 this × v 写回 this 并返回 this。(1,0,0).cross((0,1,0)) 得 (0,0,1)。顺序由右手定则决定,交换顺序会反向;长度为 |a||b|sinθ,表示两向量张成的平行四边形面积。

const normal = edgeA.clone().cross(edgeB).normalize();

clone 是为了不破坏 edgeA。平行或零输入得到零向量,normalize 不会凭空生成法线,必须先做退化检查。

crossVectors(a, b)

将 a×b 写入接收者并返回 this,适合复用输出变量。分量公式是:

a×b=(aybz−azby, azbx−axbz, axby−aybx). a\times b=(a_yb_z-a_zb_y,\ a_zb_x-a_xb_z,\ a_xb_y-a_yb_x).

a=(2,0,0)、b=(0,3,0) 得 (0,0,6),三角形面积为结果长度的一半,即 3。r185 先缓存输入分量,所以接收者可以与输入重合,但长期保存的边向量最好仍与输出分开。

angleTo(v)

返回 [0,π] 的无符号夹角,单位弧度,不修改输入。(1,0,0) 与 (0,1,0) 返回 π/2。内部先按长度归一化点积,再把余弦限制在 [-1,1] 以避免浮点误差让 acos 产生 NaN。

它无法区分左转和右转;需要另选参考法线,用 atan2(referenceNormal·(a×b), a·b) 构造有符号角,且 a、b 应位于该参考平面内。r185 对零长度输入返回 π/2,这是实现兜底,不意味着零向量在数学上与所有方向垂直。

projectOnVector(v)

把当前向量在 v 方向上的投影写回自身,返回 this:

proj⁡v(a)=a⋅vv⋅vv. \operatorname{proj}_v(a)=\frac{a\cdot v}{v\cdot v}v.

(3,4,0) 投影到 (2,0,0) 得 (3,0,0)。v 不必归一化,因为分母补偿了长度;r185 对零 v 返回零向量。非常短的参考方向可能放大数值误差,业务仍应先验证。

投影可以反向:(-3,4,0) 在 +X 轴上的投影是 (-3,0,0)。这表示有符号方向分量,不能用于直接求“向前射线上的最近点”,后者还需把投影参数限制为非负。

projectOnPlane(planeNormal)

移除沿法线方向的分量,返回修改后的 this:

a∥=a−proj⁡n(a). a_{\parallel}=a-\operatorname{proj}_n(a).

(3,-2,1) 对 (0,1,0) 投影得到 (3,0,1)。这里法线不要求单位化,因为底层调用 projectOnVector。零法线会让输入保持不变,但这不是有效平面的几何结果。

该接口投影的是向量,或相对原点的点;它没有接收平面位置参数。将世界点投影到经过 pointOnPlane 的平面,应先减 pointOnPlane,再调用本接口,最后加回该点。若已经使用 THREE.Plane,优先用 plane.projectPoint(point, out) 表达需求。

reflect(normal)

计算反射向量并返回 this:

r=a−2(a⋅n)n,∣n∣=1. r=a-2(a\cdot n)n,\qquad |n|=1.

(3,-2,0) 对单位法线 (0,1,0) 反射得到 (3,2,0)。入射向量应表示物体真实运动方向。单位法线条件不可省略:接口不会自动归一化 normal,非单位法线会错误改变方向和长度。法线取反不改变反射结果,因为公式中它出现两次。

reflect 保留理想反射速度大小,不会检测是否撞到表面,也不包含摩擦与恢复系数。若只希望损失法向能量,应使用 a-(1+e)(a·n)n,不能简单把整个反射向量乘 e,否则切向速度也被减小。

实践:只移除朝墙内的速度

import * as THREE from 'three';

const wallNormal = new THREE.Vector3(1, 0, 0); // 墙面朝可活动空间
const velocity = new THREE.Vector3(-3, 0, 4);

function removeInwardVelocity(velocity, outwardUnitNormal) {
  const normalSpeed = velocity.dot(outwardUnitNormal);
  if (normalSpeed < 0) {
    velocity.addScaledVector(outwardUnitNormal, -normalSpeed);
  }
  return velocity;
}

removeInwardVelocity(velocity, wallNormal);
console.log(velocity.toArray()); // [0, 0, 4]

与无条件 projectOnPlane 相比,这里允许已经远离墙面的速度保留。调用前必须由碰撞系统确认接触,否则角色在离墙很远时也会被约束。墙角同时接触多面时,逐面投影可能互相影响,需要更完整的约束求解。

练习(暂不附答案)

  1. 对 (2,3,0) 分别投影到 (1,0,0)、(10,0,0),预测结果并说明分母作用。
  2. 用叉积求三角形 (0,0,0)、(2,0,0)、(0,0,3) 的法线与面积;交换后两个顶点再比较。
  3. 将世界点投影到 y=3 平面,解释为什么直接 projectOnPlane 得到的位置不对。
  4. 写一个法向恢复系数为 0.5 的反弹函数,确保切向速度不变,并明确何时不应反弹。