23 · Quaternion:轴角、朝向与旋转插值
代码默认 import * as THREE from 'three';。Quaternion 用四个数保存一个三维旋转。它与 Vector4 的数据长度相同,但运算含义完全不同;不能通过逐分量相乘组合旋转,也不能把前三个分量直接当欧拉角。
从轴角理解四个分量
任何绕原点的三维纯旋转都可以表示为绕某条单位轴 a 旋转 θ。对应单位四元数:
半角来自四元数表示旋转的代数结构。几何向量可以写成纯四元数 (v,0),旋转表达为 q(v,0)q⁻¹。日常代码用 Vector3.applyQuaternion(q) 执行,不必手写乘法。
q 与 -q 表示同一旋转,所以旋转表示与四个数字不是一一对应。单位四元数满足 x²+y²+z²+w²=1,这是可靠使用旋转 API 的重要条件。
创建、单位旋转与数值状态
new Quaternion(x=0,y=0,z=0,w=1) 默认单位旋转;identity() 恢复它并返回自身。set(x,y,z,w) 原地覆盖并返回自身,但不会自动归一化。copy(q) 复制到已有对象,clone() 新建副本。isQuaternion 是类型标记。
lengthSq()、length() 返回分量长度,normalize() 原地恢复单位长度。r185 对全零四元数归一化会恢复单位旋转,但这只是一种兜底,不代表 (0,0,0,0) 是合法旋转。频繁累积乘法后可适度归一化,避免舍入误差增长。
setFromAxisAngle(axis, angle)
接受单位 Vector3 轴和弧度,写入自身并返回自身,不修改轴。它不会替你归一化轴:
const axis = new THREE.Vector3(0, 1, 0);
const rotation = new THREE.Quaternion().setFromAxisAngle(axis, Math.PI / 2);
const front = new THREE.Vector3(0, 0, 1).applyQuaternion(rotation);
console.log(rotation.toArray()); // ≈ [0, 0.7071, 0, 0.7071]
console.log(front.toArray()); // ≈ [1, 0, 0]
若轴来自两个向量的叉积,先检查叉积长度再归一化;平行方向没有唯一叉积轴,不能盲目生成轴角。
setFromEuler() 与 setFromRotationMatrix()
setFromEuler(euler,update=true) 根据角度与 order 计算单位旋转,写入自身。通常保留通知机制。setFromRotationMatrix(matrix4) 要求其左上角为纯旋转;带缩放的世界矩阵应先可靠分解,带剪切的矩阵未必能分解为精确 TRS。
这些转换保存姿态,不保存整圈数;反向转换的欧拉角也未必与输入数字相同。
setFromUnitVectors(from, to)
接受两个单位方向,把 from 转到 to,写入并返回自身。它适合把箭头的默认轴对准测量线:
const modelForward = new THREE.Vector3(0, 0, 1);
const targetDirection = new THREE.Vector3(1, 0, 1).normalize();
const orientation = new THREE.Quaternion().setFromUnitVectors(modelForward, targetDirection);
同向时得到单位旋转。反向时需要旋转 180°,但可选轴有无穷多条,实现会选一条可用的垂直轴。方向附近跨过反向点时可能出现滚转变化;需要稳定上方向的相机或车辆应增加 up 约束并构造完整基底。零方向必须由调用者过滤。
组合与求逆
multiply(q) 执行 this=this*q,premultiply(q) 执行 this=q*this,multiplyQuaternions(a,b) 写入 a*b;都返回接收者。与列向量旋转矩阵一样,a*b 先施加 b,再施加 a。
conjugate() 把 x/y/z 取反而保留 w;invert() 在 Three.js 中直接取共轭,因此要求单位四元数。两者原地修改并返回自身。一般非单位四元数的数学逆还需要除以长度平方,不能把本 API 当成那个通用实现。
比较朝向
dot(q) 返回四维点积;angleTo(q) 返回两种旋转的最短角距离,单位弧度、范围 [0,π],要求单位输入且不修改对象:
绝对值处理了 q 与 -q 的等价性。equals(q) 则只比较四个分量严格相等;它不适合带误差的姿态比较。工程上可用 angleTo(target)<angularTolerance 判断是否到位。
slerp()、slerpQuaternions() 与 rotateTowards()
current.slerp(target,t) 原地做球面线性插值,t 通常在 [0,1];result.slerpQuaternions(start,end,t) 把结果写入单独对象。输入应为单位四元数。固定端点时,t 等速增长对应沿最短旋转路径等角速度;每帧把“当前值”重新作为起点则会减速靠近。
const target = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
const current = new THREE.Quaternion();
function updateOrientation(deltaSeconds) {
const maxRadiansPerSecond = Math.PI / 2;
current.rotateTowards(target, maxRadiansPerSecond * deltaSeconds);
}
rotateTowards(target,step) 每次最多转 step 弧度,不会越过目标,修改并返回自身;step 应非负。若想要平滑减速,使用 current.slerp(target,1-Math.exp(-response*dt)),其中 response 单位为每秒。两种策略的手感和到位行为不同。
其他常用数据接口
fromArray(array,offset=0)、fromBufferAttribute(attribute,index) 读入四分量并返回自身,不自动归一化。toArray(array=[],offset=0) 返回目标数组;toJSON() 返回四分量数组;迭代顺序为 x/y/z/w。与某些格式使用 w/x/y/z 的约定不一样,交换数据前必须确认。
random() 原地生成均匀分布的单位旋转。分别随机 Euler 三个角并不能得到相同的均匀朝向分布。slerpFlat、multiplyQuaternionsFlat 是静态的扁平数组批量处理工具;一般对象级逻辑优先使用实例方法,只有已测得对象开销成为问题时再使用它们。
实践选择
让炮台转向目标时,目标相同方向上的不同距离不应影响朝向,先用“目标位置减炮台位置”得到方向。若底座有旋转父节点,世界方向不能直接写成局部 quaternion;必须先转换空间,下一章给出公式。还要决定是否允许炮台滚转,仅对齐一个方向不足以表达这条业务约束。
练习
- 手算绕 X 正转 180° 的四元数,检验它对 +Y 和 +Z 的作用。
- 比较 q 和 -q 的
equals()、dot()、angleTo()结果。 - 分别用固定起终点 slerp、逐帧 slerp、rotateTowards 做同一转向,记录 0.5 秒和 1 秒的角距离。
- 让箭头对齐两个完全相反的方向,观察滚转,描述还需要哪个输入才能唯一确定姿态。