Euler (欧拉角) 详解

Euler (欧拉角) 详解

_

🎯 什么是 Euler

欧拉角 (Euler Angles) 是一种用三个角度来描述 3D 空间中物体旋转的方法。


📐 基本概念

三个旋转轴

       Y (上)
       │
       │
       └────── X (右)
      ╱
     ╱
    Z (前)

Euler 三个分量:
- X 轴旋转 (Pitch): 前后俯仰 (点头/抬头)
- Y 轴旋转 (Yaw):   左右转向 (摇头)
- Z 轴旋转 (Roll):  左右倾斜 (歪头)

你的代码示例

const rotationEuler = euler().set(0, rotationAngle, 0);
//                                 ↑      ↑           ↑
//                                 X      Y           Z
//                              不俯仰  左右转    不倾斜

🔧 在 Three.js 中的实现

1️⃣ Three.js 的 Euler

// Three.js 源码 (简化版)
class Euler {
  constructor(x = 0, y = 0, z = 0, order = 'XYZ') {
    this._x = x;  // 绕 X 轴的旋转角度 (弧度)
    this._y = y;  // 绕 Y 轴的旋转角度 (弧度)
    this._z = z;  // 绕 Z 轴的旋转角度 (弧度)
    this._order = order;  // 旋转顺序 (重要!)
  }
  
  set(x, y, z, order) {
    this._x = x;
    this._y = y;
    this._z = z;
    if (order !== undefined) this._order = order;
    return this;
  }
  
  // 从四元数转换
  setFromQuaternion(q, order) {
    // 数学转换逻辑
  }
  
  // 转换为旋转矩阵
  toMatrix4(matrix) {
    // 矩阵计算
  }
}

2️⃣ @react-three/rapiereuler() 工具函数

// @react-three/rapier 源码 (简化版)
import { Euler as ThreeEuler } from 'three';

export function euler(x = 0, y = 0, z = 0, order = 'XYZ') {
  return new ThreeEuler(x, y, z, order);
}

// 使用示例
const rot1 = euler();                    // (0, 0, 0, 'XYZ')
const rot2 = euler().set(0, Math.PI, 0); // (0, 180°, 0, 'XYZ')

🎨 旋转顺序 (Order) 的影响

为什么旋转顺序很重要?

// 不同顺序会产生不同结果!
const euler1 = new THREE.Euler(0.5, 0.5, 0, 'XYZ');
const euler2 = new THREE.Euler(0.5, 0.5, 0, 'ZYX');

// euler1 ≠ euler2 (即使角度相同)

Six种旋转顺序

'XYZ'  // 先绕 X,再绕 Y,最后绕 Z (默认)
'XZY'  // 先绕 X,再绕 Z,最后绕 Y
'YXZ'  // 先绕 Y,再绕 X,最后绕 Z
'YZX'  // 先绕 Y,再绕 Z,最后绕 X
'ZXY'  // 先绕 Z,再绕 X,最后绕 Y
'ZYX'  // 先绕 Z,再绕 Y,最后绕 X

可视化示例

顺序 'XYZ':
1. 先绕 X 轴旋转 45° (俯仰)
2. 再绕 Y 轴旋转 45° (转身)
3. 最后绕 Z 轴旋转 45° (倾斜)

顺序 'ZYX':
1. 先绕 Z 轴旋转 45° (倾斜)
2. 再绕 Y 轴旋转 45° (转身)
3. 最后绕 X 轴旋转 45° (俯仰)

结果: 完全不同的姿态!

🔬 底层数学实现

欧拉角转旋转矩阵

// Three.js 内部实现 (简化版)
class Euler {
  // 将欧拉角转换为 4x4 旋转矩阵
  toMatrix4(matrix) {
    const { _x: x, _y: y, _z: z, _order: order } = this;
    
    // 计算三角函数值
    const cos_x = Math.cos(x);
    const sin_x = Math.sin(x);
    const cos_y = Math.cos(y);
    const sin_y = Math.sin(y);
    const cos_z = Math.cos(z);
    const sin_z = Math.sin(z);
    
    // 根据旋转顺序构建矩阵
    if (order === 'XYZ') {
      // X 轴旋转矩阵
      const Rx = [
        [1,     0,       0,      0],
        [0, cos_x, -sin_x,      0],
        [0, sin_x,  cos_x,      0],
        [0,     0,       0,      1]
      ];
      
      // Y 轴旋转矩阵
      const Ry = [
        [ cos_y, 0, sin_y, 0],
        [     0, 1,     0, 0],
        [-sin_y, 0, cos_y, 0],
        [     0, 0,     0, 1]
      ];
      
      // Z 轴旋转矩阵
      const Rz = [
        [cos_z, -sin_z, 0, 0],
        [sin_z,  cos_z, 0, 0],
        [    0,      0, 1, 0],
        [    0,      0, 0, 1]
      ];
      
      // 最终矩阵 = Rz × Ry × Rx
      matrix = Rz * Ry * Rx;
    }
    
    return matrix;
  }
}

🎯 在你的代码中的使用

function rotation() {
  // 1️⃣ 计算目标角度
  const rotationAngle = Math.atan2(velocity.x, velocity.z);
  // 例: velocity = (1, 0, 0) → rotationAngle = Math.PI / 2 (90°)
  
  // 2️⃣ 创建欧拉角对象
  const rotationEuler = euler().set(0, rotationAngle, 0);
  // rotationEuler = Euler {
  //   _x: 0,
  //   _y: 1.5707963267948966 (90° in radians),
  //   _z: 0,
  //   _order: 'XYZ'
  // }
  
  // 3️⃣ 转换为四元数
  const rotationQuaternion = quat().setFromEuler(rotationEuler);
  // 四元数更适合插值和物理引擎
}

⚠️ 欧拉角的问题 (为什么要转四元数)

1️⃣ 万向锁 (Gimbal Lock)

// 当某个轴旋转 90° 时,会失去一个自由度
const euler1 = new THREE.Euler(0, Math.PI / 2, 0);
// 此时绕 X 和 Z 轴旋转会产生相同效果!

可视化:

正常状态:
  Y
  │
  └─── X
 ╱
Z

X 轴旋转 90° 后:
  X (原来的 Y)
  │
  └─── Y (原来的 -Z)
 ╱
Z

现在 Y 和 Z 轴重叠,失去了一个旋转维度!

2️⃣ 插值不平滑

// ❌ 欧拉角线性插值
function lerpEuler(start, end, t) {
  return {
    x: start.x + (end.x - start.x) * t,
    y: start.y + (end.y - start.y) * t,
    z: start.z + (end.z - start.z) * t,
  };
}

// 问题: 可能会走"弯路"
// 从 (0, 0, 0) 到 (0, 180°, 0)
// 中间会经过 (0, 90°, 0) - 正确
// 但如果是 (0, 0, 0) 到 (0, -10°, 0)
// 线性插值会经过 (0, -5°, 0) 而不是直接转 10°

3️⃣ 旋转顺序依赖

// 同样的角度,不同顺序,结果不同
const rot1 = new THREE.Euler(30, 45, 60, 'XYZ');
const rot2 = new THREE.Euler(30, 45, 60, 'ZYX');

// rot1 和 rot2 的最终姿态完全不同!

✅ 四元数的优势

// ✅ 四元数球面线性插值 (Slerp)
const startQuat = new THREE.Quaternion().setFromEuler(eulerStart);
const endQuat = new THREE.Quaternion().setFromEuler(eulerEnd);

startQuat.slerp(endQuat, 0.5); // 平滑插值

// 优势:
// 1. 没有万向锁
// 2. 插值平滑 (走最短路径)
// 3. 无旋转顺序问题
// 4. 适合物理引擎

📊 对比表格

特性

欧拉角 (Euler)

四元数 (Quaternion)

表示方式

3 个角度 (x, y, z)

4 个数字 (x, y, z, w)

直观性

✅ 易于理解

❌ 难以理解

万向锁

❌ 存在

✅ 不存在

插值

❌ 不平滑

✅ 平滑 (Slerp)

旋转顺序

⚠️ 依赖

✅ 无依赖

存储空间

✅ 3 个数

⚠️ 4 个数

适用场景

用户输入、配置

动画、物理引擎


💡 最佳实践

// ✅ 推荐流程
// 1. 用欧拉角计算 (直观)
const targetEuler = euler().set(0, angle, 0);

// 2. 转为四元数 (稳定)
const targetQuat = quat().setFromEuler(targetEuler);

// 3. 插值 (平滑)
currentQuat.slerp(targetQuat, 0.2);

// 4. 应用到对象
player.setRotation(currentQuat, true);

🎓 总结

Euler 欧拉角:

  • ✅ 人类友好的旋转表示方式

  • ✅ 适合初始设置和配置

  • ❌ 有万向锁问题

  • ❌ 插值不平滑

在 Three.js 中的实现:

  • 通过 THREE.Euler 类封装

  • 内部转换为旋转矩阵

  • 通常配合四元数使用

你的代码中的使用:

euler().set(0, angle, 0)  // 计算目标旋转
  ↓
quat().setFromEuler()     // 转为四元数
  ↓
slerp(target, 0.2)        // 平滑插值
  ↓
setRotation()             // 应用到刚体

这就是为什么要先用 Euler 计算,再转四元数应用的原因! 🎯

/**
   * 🎯 人物旋转函数
   * 功能: 让角色模型平滑地朝向移动方向旋转
   * 应用场景: 玩家移动时,模型面朝移动方向(如前进时面朝前,后退时转身等)
   */
function rotation() {
	// 🛡️ 安全检查: 确保玩家刚体引用存在
	if (!player.current) return;

	// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
	// 📐 步骤 1: 计算目标旋转角度
	// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

	/**
     * Math.atan2(y, x): 计算点 (x, y) 相对于原点的角度(弧度)
     *
     * 为什么用 atan2(velocity.x, velocity.z)?
     * - Three.js 坐标系: X(左右), Y(上下), Z(前后)
     * - 我们要在 XZ 平面上旋转(水平面)
     * - atan2 参数顺序: atan2(对边, 邻边)
     *
     * 示例:
     * - velocity = (0, 0, -1) [向前] → atan2(0, -1) = 0° (面朝 -Z 轴)
     * - velocity = (1, 0, 0) [向右] → atan2(1, 0) = 90° (面朝 +X 轴)
     * - velocity = (0, 0, 1) [向后] → atan2(0, 1) = 180° (面朝 +Z 轴)
     * - velocity = (-1, 0, 0) [向左] → atan2(-1, 0) = -90° (面朝 -X 轴)
     */
	const rotationAngle = Math.atan2(velocity.x, velocity.z);

	// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
	// 📐 步骤 2: 转换为欧拉角
	// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

	/**
     * euler(): 创建一个欧拉角对象
     * .set(x, y, z): 设置绕 X、Y、Z 轴的旋转角度
     *
     * 参数:
     * - x: 0 (不绕 X 轴旋转,即不前后俯仰)
     * - y: rotationAngle (绕 Y 轴旋转,实现左右转向)
     * - z: 0 (不绕 Z 轴旋转,即不左右倾斜)
     *
     * 为什么只旋转 Y 轴?
     * - 角色站立时,Y 轴垂直于地面
     * - 绕 Y 轴旋转 = 水平面上转身
     * - 就像人站着左右转头/转身
     */
	const rotationEuler = euler().set(0, rotationAngle, 0);

	// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
	// 📐 步骤 3: 转换为四元数
	// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

	/**
     * 为什么需要四元数?
     *
     * 欧拉角的问题:
     * - ❌ 存在万向锁(Gimbal Lock)
     * - ❌ 插值不平滑
     * - ❌ 旋转顺序影响结果
     *
     * 四元数的优势:
     * - ✅ 避免万向锁
     * - ✅ 插值平滑(Slerp 球面线性插值)
     * - ✅ 适合物理引擎
     *
     * quat(): 创建四元数对象
     * .setFromEuler(): 从欧拉角转换为四元数
     */
	const rotationQuaternion = quat().setFromEuler(rotationEuler);

	/**
     * 获取当前角色的旋转(四元数格式)
     * player.current.rotation(): 返回刚体的当前旋转四元数
     * quat().copy(): 复制四元数值到新对象
     */
	const startQuaternion = quat().copy(player.current.rotation());

	// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
	// 📐 步骤 4: 球面线性插值(Slerp)
	// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

	/**
     * Slerp (Spherical Linear Interpolation): 球面线性插值
     *
     * 作用: 在两个旋转之间平滑过渡
     *
     * 参数:
     * - rotationQuaternion: 目标旋转(要转向的角度)
     * - 0.2: 插值系数(lerp factor)
     *
     * 插值系数说明:
     * - 0.2 表示每帧移动 20% 的距离
     * - 假设 60fps,完全转到目标约需: 1/0.2/60 ≈ 0.083 秒
     * - 值越大 → 旋转越快,但可能不够平滑
     * - 值越小 → 旋转越慢,但更平滑
     *
     * 可视化理解:
     * 当前角度: 0°
     * 目标角度: 90°
     *
     * 第 1 帧: 0° + (90° - 0°) × 0.2 = 18°
     * 第 2 帧: 18° + (90° - 18°) × 0.2 = 32.4°
     * 第 3 帧: 32.4° + (90° - 32.4°) × 0.2 = 43.92°
     * ...
     * 最终逐渐接近 90°
     *
     * 为什么不是 0.2 秒?
     * - 0.2 是每帧的插值比例,不是时间
     * - 每帧都会重新计算,形成平滑动画
     */
	startQuaternion.slerp(rotationQuaternion, 0.2);

	// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
	// 📐 步骤 5: 应用到刚体
	// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

    /**
     * setRotation(): 设置刚体的旋转
     *
     * 参数:
     * - startQuaternion: 插值后的四元数
     * - true: 立即唤醒刚体(如果处于休眠状态)
     *
     * 为什么需要 true?
     * - Rapier 物理引擎会让静止的刚体"休眠"以节省性能
     * - 设置旋转时需要唤醒,否则不会生效
     */
    player.current.setRotation(startQuaternion, true);
  }

四元数 (Quaternion) 深度解析 2025-12-30
@react-three/fiber的useFrame 2025-12-30

评论区