四元数 (Quaternion) 深度解析

四元数 (Quaternion) 深度解析

_

🎯 核心概念

四元数是一种用 4 个数字表示 3D 旋转的数学工具。

// 四元数的结构
quaternion = {
  x: 0.0,   // 虚部 i
  y: 0.707, // 虚部 j  
  z: 0.0,   // 虚部 k
  w: 0.707  // 实部
}

// 表示: 绕 Y 轴旋转 90°

📐 为什么需要 4 个数字?

数学表示

四元数 q = w + xi + yj + zk

其中:
- w, x, y, z 是实数
- i, j, k 是虚数单位

满足:
i² = j² = k² = ijk = -1
ij = k,  jk = i,  ki = j
ji = -k, kj = -i, ik = -j

旋转的几何意义

// 绕任意轴 axis(ax, ay, az) 旋转角度 θ

const halfAngle = θ / 2;
const s = Math.sin(halfAngle);

quaternion = {
  x: ax * s,
  y: ay * s,
  z: az * s,
  w: Math.cos(halfAngle)
}

关键点: 使用 θ/2 而不是 θ,这是四元数的数学特性!


🎨 可视化理解

1️⃣ 欧拉角 vs 四元数

欧拉角 (直观但有问题):
┌─────────────────┐
│ Pitch (俯仰)    │ = 30°
│ Yaw   (偏航)    │ = 45°
│ Roll  (翻滚)    │ = 60°
└─────────────────┘
问题: 旋转顺序影响结果

四元数 (抽象但稳定):
┌─────────────────┐
│ x = 0.189       │
│ y = 0.462       │
│ z = 0.191       │
│ w = 0.844       │
└─────────────────┘
优势: 唯一表示,无顺序问题

2️⃣ 单位四元数与旋转

// 单位四元数 (用于表示旋转)
x² + y² + z² + w² = 1

// 示例: 绕 Y 轴旋转 90°
const angle = Math.PI / 2; // 90°
const halfAngle = angle / 2; // 45°

quaternion = {
  x: 0 * Math.sin(halfAngle),  // = 0
  y: 1 * Math.sin(halfAngle),  // = 0.707
  z: 0 * Math.sin(halfAngle),  // = 0
  w: Math.cos(halfAngle)       // = 0.707
}

// 验证: 0² + 0.707² + 0² + 0.707² = 1 ✅

🔧 实际代码示例

从零开始理解

import { Quaternion, Euler } from 'three';

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 示例 1: 创建四元数 (绕 Y 轴旋转 90°)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// 方法 A: 从欧拉角转换 (推荐)
const euler = new Euler(0, Math.PI / 2, 0, 'XYZ');
const quat1 = new Quaternion().setFromEuler(euler);
console.log(quat1);
// { x: 0, y: 0.707, z: 0, w: 0.707 }

// 方法 B: 从轴角转换
const axis = { x: 0, y: 1, z: 0 }; // Y 轴
const angle = Math.PI / 2;          // 90°
const quat2 = new Quaternion().setFromAxisAngle(axis, angle);
console.log(quat2);
// { x: 0, y: 0.707, z: 0, w: 0.707 }

// 方法 C: 直接设置 (不推荐,难以理解)
const quat3 = new Quaternion(0, 0.707, 0, 0.707);

在代码中的应用

function rotation() {
  // 🎯 目标: 让角色转向移动方向
  
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  // 步骤 1: 计算目标角度 (用欧拉角思维)
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  
  // 假设 velocity = (1, 0, 0) [向右移动]
  const rotationAngle = Math.atan2(velocity.x, velocity.z);
  // rotationAngle = Math.atan2(1, 0) = Math.PI / 2 (90°)
  
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  // 步骤 2: 转为欧拉角对象
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  
  const rotationEuler = euler().set(0, rotationAngle, 0);
  // rotationEuler = Euler { x: 0, y: 1.57, z: 0 }
  
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  // 步骤 3: 转为四元数 (物理引擎需要)
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  
  /**
   * 内部计算过程:
   * halfAngle = 1.57 / 2 = 0.785 (45°)
   * 
   * x = 0 * sin(0.785) = 0
   * y = 1 * sin(0.785) = 0.707
   * z = 0 * sin(0.785) = 0
   * w = cos(0.785)     = 0.707
   */
  const rotationQuaternion = quat().setFromEuler(rotationEuler);
  // rotationQuaternion = { x: 0, y: 0.707, z: 0, w: 0.707 }
  
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  // 步骤 4: 获取当前旋转
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  
  // 假设当前角色面朝前方 (0°)
  const startQuaternion = quat().copy(player.current.rotation());
  // startQuaternion = { x: 0, y: 0, z: 0, w: 1 }
  
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  // 步骤 5: 球面线性插值 (关键!)
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  
  /**
   * Slerp 的数学原理:
   * 
   * result = (sin((1-t)θ) / sin(θ)) * start + (sin(tθ) / sin(θ)) * end
   * 
   * 其中:
   * - θ 是两个四元数之间的夹角
   * - t 是插值系数 (0.2)
   * 
   * 几何意义:
   * - 在 4D 空间的单位球面上进行插值
   * - 沿着球面的最短弧线移动
   * - 保证旋转速度恒定
   */
  startQuaternion.slerp(rotationQuaternion, 0.2);
  
  /**
   * 假设第一帧执行后:
   * startQuaternion ≈ { x: 0, y: 0.141, z: 0, w: 0.990 }
   * 
   * 对应角度 ≈ 16.2° (已旋转了 20% 的 90°)
   * 
   * 第二帧:
   * startQuaternion ≈ { x: 0, y: 0.270, z: 0, w: 0.963 }
   * 对应角度 ≈ 31.3°
   * 
   * ...逐渐接近 90°
   */
  
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  // 步骤 6: 应用到刚体
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  
  player.current.setRotation(startQuaternion, true);
}

🧮 四元数运算详解

1️⃣ Slerp (球面线性插值)

// 源码简化版
class Quaternion {
  slerp(target, t) {
    // 1. 计算夹角余弦值
    let cosHalfTheta = this.w * target.w 
                     + this.x * target.x 
                     + this.y * target.y 
                     + this.z * target.z;
    
    // 2. 如果两个四元数几乎相同,直接复制
    if (Math.abs(cosHalfTheta) >= 1.0) {
      return this;
    }
    
    // 3. 计算夹角
    const halfTheta = Math.acos(cosHalfTheta);
    const sinHalfTheta = Math.sqrt(1.0 - cosHalfTheta * cosHalfTheta);
    
    // 4. 如果夹角太小,用线性插值
    if (Math.abs(sinHalfTheta) < 0.001) {
      this.w = this.w * 0.5 + target.w * 0.5;
      this.x = this.x * 0.5 + target.x * 0.5;
      this.y = this.y * 0.5 + target.y * 0.5;
      this.z = this.z * 0.5 + target.z * 0.5;
      return this;
    }
    
    // 5. 球面插值
    const ratioA = Math.sin((1 - t) * halfTheta) / sinHalfTheta;
    const ratioB = Math.sin(t * halfTheta) / sinHalfTheta;
    
    this.w = this.w * ratioA + target.w * ratioB;
    this.x = this.x * ratioA + target.x * ratioB;
    this.y = this.y * ratioA + target.y * ratioB;
    this.z = this.z * ratioA + target.z * ratioB;
    
    return this;
  }
}

2️⃣ 可视化 Slerp

在 4D 单位球面上:

起点 A: (0, 0, 0, 1)    → 0°
终点 B: (0, 0.707, 0, 0.707) → 90°

Slerp 路径 (t 从 0 到 1):
    B (90°)
   ╱
  ╱  ← 球面上的弧线
 ╱
A (0°)

线性插值 Lerp (错误):
    B
   ╱
  ╱  ← 穿过球内部的直线 (会改变速度)
 ╱
A

Slerp 优势:
✅ 始终在球面上
✅ 旋转速度恒定
✅ 走最短路径

🎓 常见场景示例

场景 1: 角色转向目标

// 当前方向
const currentQuat = player.rotation;

// 目标位置
const target = new THREE.Vector3(10, 0, 5);
const direction = target.sub(player.position).normalize();

// 计算目标旋转
const targetAngle = Math.atan2(direction.x, direction.z);
const targetQuat = new THREE.Quaternion().setFromEuler(
  new THREE.Euler(0, targetAngle, 0)
);

// 平滑插值
currentQuat.slerp(targetQuat, 0.1);
player.setRotation(currentQuat);

场景 2: 相机跟随

useFrame(() => {
  // 计算理想相机位置
  const idealOffset = new THREE.Vector3(0, 5, 10);
  idealOffset.applyQuaternion(player.quaternion);
  const idealPosition = player.position.clone().add(idealOffset);
  
  // 计算理想相机朝向
  const idealLookAt = player.position.clone();
  idealLookAt.y += 1.5; // 看向角色头部
  
  const direction = idealLookAt.sub(idealPosition).normalize();
  const targetQuat = new THREE.Quaternion().setFromUnitVectors(
    new THREE.Vector3(0, 0, -1),
    direction
  );
  
  // 平滑插值
  camera.position.lerp(idealPosition, 0.1);
  camera.quaternion.slerp(targetQuat, 0.1);
});

场景 3: 多段旋转动画

const keyframes = [
  { time: 0, quat: new THREE.Quaternion(0, 0, 0, 1) },         // 0°
  { time: 1, quat: new THREE.Quaternion(0, 0.707, 0, 0.707) }, // 90°
  { time: 2, quat: new THREE.Quaternion(0, 1, 0, 0) },         // 180°
];

function animate(currentTime) {
  // 找到当前时间段
  let i = 0;
  while (i < keyframes.length - 1 && currentTime > keyframes[i + 1].time) {
    i++;
  }
  
  const start = keyframes[i];
  const end = keyframes[i + 1];
  
  // 计算插值系数
  const t = (currentTime - start.time) / (end.time - start.time);
  
  // Slerp 插值
  const currentQuat = start.quat.clone().slerp(end.quat, t);
  
  player.setRotation(currentQuat);
}

🐛 常见误区

❌ 误区 1: 直接操作四元数分量

// ❌ 错误: 手动修改分量会破坏单位性
quaternion.y += 0.1;

// ✅ 正确: 使用方法
const euler = new Euler(0, 0.1, 0);
quaternion.setFromEuler(euler);

❌ 误区 2: 用 Lerp 代替 Slerp

// ❌ 错误: 线性插值会改变旋转速度
quaternion.x = lerp(start.x, end.x, t);
quaternion.y = lerp(start.y, end.y, t);
quaternion.z = lerp(start.z, end.z, t);
quaternion.w = lerp(start.w, end.w, t);

// ✅ 正确: 使用 Slerp
quaternion.slerp(targetQuat, t);

❌ 误区 3: 忘记归一化

// ❌ 四元数运算后可能失去单位性
const combined = quat1.multiply(quat2);

// ✅ 需要归一化
combined.normalize();

📊 性能对比

// 测试: 1000 次旋转插值

// 欧拉角 Lerp
console.time('Euler Lerp');
for (let i = 0; i < 1000; i++) {
  eulerStart.x = lerp(eulerStart.x, eulerEnd.x, 0.1);
  eulerStart.y = lerp(eulerStart.y, eulerEnd.y, 0.1);
  eulerStart.z = lerp(eulerStart.z, eulerEnd.z, 0.1);
}
console.timeEnd('Euler Lerp');
// ~0.5ms

// 四元数 Slerp
console.time('Quaternion Slerp');
for (let i = 0; i < 1000; i++) {
  quatStart.slerp(quatEnd, 0.1);
}
console.timeEnd('Quaternion Slerp');
// ~1.2ms

// 结论: 欧拉角更快,但四元数更稳定准确

💡 总结

四元数的本质

  1. 4D 空间的单位向量: 表示 3D 旋转

  2. 数学工具: 避免万向锁,平滑插值

  3. 物理引擎标准: Rapier/Cannon/Ammo 都使用四元数

使用建议

// ✅ 推荐流程
计算目标角度 (欧拉角)
    ↓
转为四元数
    ↓
Slerp 插值
    ↓
应用到对象

关键公式

绕轴 (ax, ay, az) 旋转 θ:

q.x = ax * sin(θ/2)
q.y = ay * sin(θ/2)
q.z = az * sin(θ/2)
q.w = cos(θ/2)

单位性: x² + y² + z² + w² = 1

四元数不需要完全理解数学,只需知道它是更好的旋转表示方式! 🚀

glTF 文件详解 2025-12-30
Euler (欧拉角) 详解 2025-12-30

评论区