22 · Euler:三个角怎样描述旋转

返回系列目录 · 上一章 · 下一章

代码默认 import * as THREE from 'three';。给门设置开合角、给摄像机设置偏航和俯仰时,角度是很自然的输入。Euler 保存的就是绕三个轴的角度,以及解释这些角度的顺序;它不是一个空间方向向量。

单轴旋转与弧度

弧度定义为圆弧长度除以半径,转一圈是 2π。Three.js 的旋转 API 接收弧度,GUI 若显示角度,边界处显式转换:

const rotation = new THREE.Euler(0, THREE.MathUtils.degToRad(90), 0, 'XYZ');
const front = new THREE.Vector3(0, 0, 1).applyEuler(rotation);
console.log(front.toArray()); // ≈ [1, 0, 0]

按右手定则绕 +Y 转正 90°,+Z 变成 +X。变量名 yawDegrees、yawRadians 比一个含糊的 angle 更能防止单位混淆。

构造函数、属性与 set()

new Euler(x=0,y=0,z=0,order='XYZ') 创建对象。x/y/z 是各轴弧度,order 支持 XYZ、YXZ、ZXY、ZYX、YZX、XZY。isEuler 是类型标记;Euler.DEFAULT_ORDER 默认 XYZ,项目中最好显式传递约定,而非修改全局默认值。

set(x,y,z,order=this.order) 覆盖角度并返回自身。copy(euler) 复制角度与顺序到已有对象;clone() 新建副本。object.rotation 是与 object.quaternion 同步的 Euler,正常使用公开 setter 即可,不要操作 _x 等内部字段绕过通知机制。

顺序到底指什么

Three.js 使用内禀旋转,即按顺序围绕逐步变化的局部轴旋转。对于 XYZ,矩阵表达为 R = Rx Ry Rz。把它作用在列向量上时,代数计算是右侧的 Rz 先乘向量;这与“依次绕物体自身 X、Y、Z 轴”的描述并不矛盾,二者使用的轴参照不同。

const anglesXYZ = new THREE.Euler(Math.PI / 2, Math.PI / 2, 0, 'XYZ');
const anglesYXZ = new THREE.Euler(Math.PI / 2, Math.PI / 2, 0, 'YXZ');
const forward = new THREE.Vector3(0, 0, 1);
console.log(forward.clone().applyEuler(anglesXYZ).toArray()); // ≈ [1, 0, 0]
console.log(forward.clone().applyEuler(anglesYXZ).toArray()); // ≈ [0, -1, 0]

所以 rotation.x += delta 是改欧拉参数,并非在任意现有姿态上绕当前局部 X 追加一个独立旋转;后者应使用 rotateX() 或四元数组合。

reorder(newOrder) 与直接修改 order

reorder('YXZ') 先转换为四元数,再用新顺序重新求角,修改自身并返回自身,保持当前姿态。直接赋值 order='YXZ' 则用新顺序重新解释原来三个数字,通常会改变姿态。

reorder() 会丢失整圈数:旋转 4π + 0.2 与 0.2 姿态相同,但已经转过几圈是额外的动画信息。机械计数器或车轮里程应另存累计角度,而不是从转换后的 Euler 读回圈数。

从其他表示得到 Euler

setFromQuaternion(q, order=this.order, update=true) 接受单位四元数,修改并返回自身。update 控制内部变化通知,一般不要关闭,特别是 Object3D 的 rotation。

setFromRotationMatrix(matrix4, order=this.order, update=true) 接受左上角为纯旋转的 Matrix4,不可直接传含非均匀缩放或剪切的矩阵。对可分解 TRS 先 decompose() 得到 Quaternion,再转换。

setFromVector3(vector, order=this.order) 只是把三个分量当角度复制,不会求“朝向该向量”的旋转。让模型朝向目标需要方向对齐或 lookAt()。

万向节锁与表示不唯一

在某些姿态下,三个欧拉参数对空间朝向的影响不再独立。例如 XYZ 的 Y 接近 ±90° 时,第一和第三个旋转自由度发生耦合。物体仍然可以拥有任何三维朝向,但这组参数在附近不适合作为稳定控制坐标;转换函数可能选择一个角为零的等价表达,其他角突然跳变。

不要通过给角度增加很小的偏移“修复”问题。对于限制在合理范围内的门、转台和俯仰机构,Euler 很好用;自由飞行、任意方向插值适合 Quaternion。四元数避免欧拉参数奇异,但它不会自动定义你想要的滚转方向。

数据交换与比较

toArray(array=[],offset=0) 返回写入 [x,y,z,order] 的数组,不修改 Euler;顺序是字符串,所以不能完整保存到 Float32Array。fromArray(array) 读取前三个数和可选第四个顺序,返回自身;它没有 offset 参数。[...euler] 按相同次序迭代。

equals(other) 比较三个角与顺序是否严格相等,不是比较姿态。Euler(0,0,0) 与 Euler(0,2π,0) 不相等但姿态相同。需要比较姿态时转为单位四元数,再比较角距离。

实践:门轴与摄像机云台

门的几何中心通常不是铰链。创建铰链 Group,让门网格在组内偏移,再只控制组的 Y 角:

const hinge = new THREE.Group();
const door = new THREE.Mesh(
  new THREE.BoxGeometry(1, 2, 0.05),
  new THREE.MeshNormalMaterial()
);
door.position.set(0.5, 1, 0);
hinge.add(door);
hinge.rotation.y = THREE.MathUtils.degToRad(60);
// 将 hinge 加入现有 scene,门会绕左下方铰链轴旋转。

监控摄像机云台也可以用两层 Group:父层负责绕世界竖直方向偏航,子层负责局部俯仰。这样角度限位和产品语义都明确,且避免把自由滚转混入输入。俯仰范围若接近垂直,还应明确交互的边界行为。

练习

  1. 将同一组三个非零角按六种 order 作用到三个基向量,比较结果。
  2. 对比修改 order 与调用 reorder() 前后的 Quaternion 角距离。
  3. 给门添加 0°~110° 限位,并使动画不依赖显示刷新率。
  4. 将 Euler 的某个角设置为多圈后执行 Quaternion 往返转换,设计一个保留累计圈数的数据结构。