13 · Vector3:点积、叉积、投影与反射
返回系列目录 · 上一章:长度与约束 · 下一章:插值与采样
把运动拆成沿墙和朝墙的两部分,可以直接解释贴墙滑行;把目标方向分解到角色前方,可以解释视野检测。几何接口的价值正在于这种分解。所有输入先统一坐标空间,不能把局部法线直接用于世界速度。
dot(v)
返回数值 x*v.x + y*v.y + z*v.z,不修改任意向量。几何式为
(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=(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:
(3,4,0) 投影到 (2,0,0) 得 (3,0,0)。v 不必归一化,因为分母补偿了长度;r185 对零 v 返回零向量。非常短的参考方向可能放大数值误差,业务仍应先验证。
投影可以反向:(-3,4,0) 在 +X 轴上的投影是 (-3,0,0)。这表示有符号方向分量,不能用于直接求“向前射线上的最近点”,后者还需把投影参数限制为非负。
projectOnPlane(planeNormal)
移除沿法线方向的分量,返回修改后的 this:
(3,-2,1) 对 (0,1,0) 投影得到 (3,0,1)。这里法线不要求单位化,因为底层调用 projectOnVector。零法线会让输入保持不变,但这不是有效平面的几何结果。
该接口投影的是向量,或相对原点的点;它没有接收平面位置参数。将世界点投影到经过 pointOnPlane 的平面,应先减 pointOnPlane,再调用本接口,最后加回该点。若已经使用 THREE.Plane,优先用 plane.projectPoint(point, out) 表达需求。
reflect(normal)
计算反射向量并返回 this:
(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 相比,这里允许已经远离墙面的速度保留。调用前必须由碰撞系统确认接触,否则角色在离墙很远时也会被约束。墙角同时接触多面时,逐面投影可能互相影响,需要更完整的约束求解。
练习(暂不附答案)
- 对
(2,3,0)分别投影到(1,0,0)、(10,0,0),预测结果并说明分母作用。 - 用叉积求三角形
(0,0,0)、(2,0,0)、(0,0,3)的法线与面积;交换后两个顶点再比较。 - 将世界点投影到
y=3平面,解释为什么直接 projectOnPlane 得到的位置不对。 - 写一个法向恢复系数为 0.5 的反弹函数,确保切向速度不变,并明确何时不应反弹。