第一部分:为什么需要四元数?旋转问题的困境
1. 旋转的三种表示方法比较
| 表示方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 欧拉角 | 直观易懂,人类可读 | 万向节死锁,插值困难 | 简单旋转设置,编辑器操作 |
| 旋转矩阵 | 无万向节死锁,可组合变换 | 9个参数冗余,插值困难 | 底层图形API,坐标变换 |
| 四元数 | 无死锁,高效插值,内存小 | 不直观,数学复杂 | 游戏中的动态旋转,平滑过渡 |
2. 万向节死锁:欧拉角的致命缺陷
什么是万向节死锁?
当三个旋转轴中的两个轴对齐时,会失去一个旋转自由度。
csharp
// 演示万向节死锁
transform.eulerAngles = new Vector3(90f, 0f, 0f); // X轴旋转90度
// 此时Y轴和Z轴在同一平面,再绕Y或Z旋转效果相同
transform.eulerAngles = new Vector3(90f, 30f, 0f);
transform.eulerAngles = new Vector3(90f, 0f, 30f); // 效果几乎相同!
四元数如何解决?
四元数通过四个数值表示旋转,避免了三轴顺序依赖的问题。
第二部分:四元数基础 - 从概念到理解
1. 四元数的数学本质
四元数是一个四维复数:q = w + xi + yj + zk
其中:
-
w 是实部(标量部分)
-
x, y, z 是虚部(向量部分)
-
i, j, k 是虚数单位,满足:i² = j² = k² = ijk = -1
几何意义:四元数表示绕某个轴旋转一定角度
-
旋转轴:v = (x, y, z) / sin(θ/2)
-
旋转角度:θ = 2 * acos(w)
csharp
// Unity中的四元数结构
public struct Quaternion {
public float x, y, z, w;
// w是实部,x,y,z是虚部
}
2. 创建四元数的四种方法
csharp
// 方法1:直接构造(不推荐,除非你知道自己在做什么)
Quaternion q1 = new Quaternion(0, 0, 0.707f, 0.707f); // 绕Z轴旋转90度
// 方法2:从欧拉角转换(最常用)
Quaternion q2 = Quaternion.Euler(0, 90, 0); // 绕Y轴旋转90度
// 方法3:轴-角表示法(明确指定旋转轴和角度)
Vector3 axis = Vector3.up; // Y轴
float angle = 45f; // 45度
Quaternion q3 = Quaternion.AngleAxis(angle, axis);
// 方法4:看向某个方向
Vector3 lookDirection = target.position - transform.position;
Quaternion q4 = Quaternion.LookRotation(lookDirection);
// 单位四元数(不旋转)
Quaternion identity = Quaternion.identity; // (0, 0, 0, 1)
3. 四元数的基本性质
csharp
// 1. 模长(长度)
Quaternion q = Quaternion.Euler(0, 45, 0);
float magnitude = q.magnitude; // 单位四元数长度为1
// 2. 归一化(确保是单位四元数)
q.Normalize(); // 或 q = q.normalized;
// 3. 逆四元数(反向旋转)
Quaternion inverse = Quaternion.Inverse(q);
Quaternion shouldBeIdentity = q * inverse; // 应等于Quaternion.identity
// 4. 共轭四元数
// 对于单位四元数,逆等于共轭:q⁻¹ = q* = (-x, -y, -z, w)
第三部分:四元数运算 - 核心操作详解
1. 四元数乘法:组合旋转
关键概念:四元数乘法表示旋转的组合,顺序很重要(从右向左执行)
csharp
// 先绕Y轴旋转90度,再绕X轴旋转90度
Quaternion rotY = Quaternion.Euler(0, 90, 0);
Quaternion rotX = Quaternion.Euler(90, 0, 0);
// 组合旋转:注意顺序!
Quaternion combined = rotX * rotY; // 先执行rotY,再执行rotX
// 验证:应用旋转
transform.rotation = combined;
// 重要:四元数乘法不满足交换律
Quaternion result1 = rotX * rotY;
Quaternion result2 = rotY * rotX;
bool areEqual = result1 == result2; // false!
2. 旋转向量(点)
csharp
// 使用四元数旋转一个向量(点)
Vector3 point = new Vector3(1, 0, 0);
Quaternion rotation = Quaternion.Euler(0, 90, 0); // 绕Y轴旋转90度
// 方法1:乘法操作符(最常用)
Vector3 rotatedPoint = rotation * point; // (0, 0, -1)
// 方法2:使用Rotate方法(原地旋转)
rotatedPoint = point; // 复制
rotation.Rotate(ref rotatedPoint); // 原地旋转
// 实际应用:计算物体前方
Vector3 forward = transform.rotation * Vector3.forward;
// 等同于:transform.forward(但理解原理很重要)
3. 四元数插值 - 平滑旋转的关键
3.1 线性插值(Lerp)
csharp
Quaternion startRotation = transform.rotation;
Quaternion targetRotation = Quaternion.LookRotation(target.position - transform.position);
// 线性插值
float t = Time.deltaTime * rotationSpeed;
transform.rotation = Quaternion.Lerp(startRotation, targetRotation, t);
// Lerp特点:
// - 插值路径是四维空间的直线
// - 在球面上不是最短路径
// - 旋转速度不均匀(中间快,两端慢)
3.2 球面线性插值(Slerp)
csharp
// 球面线性插值(推荐用于旋转)
transform.rotation = Quaternion.Slerp(startRotation, targetRotation, t);
// Slerp特点:
// - 沿着球面大圆弧插值
// - 是最短路径
// - 角速度恒定(旋转平滑)
3.3 两者对比与选择
csharp
public class RotationExample : MonoBehaviour {
public Transform target;
public float rotationSpeed = 2f;
void Update() {
Quaternion targetRotation = Quaternion.LookRotation(target.position - transform.position);
// 情况1:小角度旋转(小于90度) - 两者差异不大
// 情况2:大角度旋转 - 使用Slerp避免奇怪路径
// 情况3:需要恒定角速度 - 使用Slerp
// 推荐:大多数情况下使用Slerp
float t = rotationSpeed * Time.deltaTime;
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, t);
// 或者使用RotateTowards(恒定角速度)
float maxDegreesDelta = 90f * Time.deltaTime;
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, maxDegreesDelta);
}
}
第四部分:实用四元数函数详解
1. Quaternion.LookRotation - 朝向目标
csharp
// 基本用法:看向目标点
Vector3 direction = target.position - transform.position;
transform.rotation = Quaternion.LookRotation(direction);
// 指定向上方向(用于在斜坡上保持垂直)
Vector3 upDirection = Vector3.up; // 默认向上
transform.rotation = Quaternion.LookRotation(direction, upDirection);
// 实际应用:坦克炮塔(只能水平旋转)
Vector3 toTarget = target.position - transform.position;
toTarget.y = 0; // 忽略垂直分量,只水平旋转
if (toTarget != Vector3.zero) {
transform.rotation = Quaternion.LookRotation(toTarget);
}
// 相机看向目标,但保持自身水平
public class CameraLookAt : MonoBehaviour {
public Transform target;
void Update() {
Vector3 direction = target.position - transform.position;
Quaternion lookRotation = Quaternion.LookRotation(direction);
// 只保留Y轴旋转,X和Z保持0
float yRotation = lookRotation.eulerAngles.y;
transform.rotation = Quaternion.Euler(0, yRotation, 0);
}
}
2. Quaternion.FromToRotation - 方向对齐
csharp
// 将fromDirection旋转到toDirection
Vector3 fromDirection = transform.up;
Vector3 toDirection = Vector3.up;
Quaternion rotation = Quaternion.FromToRotation(fromDirection, toDirection);
transform.rotation = rotation * transform.rotation;
// 实际应用:让物体始终垂直于地面(斜坡行走)
public class AlignToGround : MonoBehaviour {
public float rayLength = 1.5f;
void Update() {
RaycastHit hit;
if (Physics.Raycast(transform.position, Vector3.down, out hit, rayLength)) {
// 计算从当前向上方向到地面法线的旋转
Quaternion groundRotation = Quaternion.FromToRotation(transform.up, hit.normal);
// 应用旋转,但保持原有朝向
transform.rotation = groundRotation * transform.rotation;
}
}
}
3. Quaternion.RotateTowards - 逐步旋转
csharp
// 以最大角度步长旋转到目标
float maxDegreesDelta = 90f * Time.deltaTime; // 每秒90度
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, maxDegreesDelta);
// 实际应用:限制旋转速度的炮塔
public class TurretRotation : MonoBehaviour {
public Transform target;
public float maxRotationSpeed = 60f; // 度/秒
void Update() {
if (target != null) {
Vector3 direction = target.position - transform.position;
Quaternion targetRotation = Quaternion.LookRotation(direction);
float step = maxRotationSpeed * Time.deltaTime;
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, step);
}
}
}
4. 其他实用函数
csharp
// 计算两个旋转之间的角度
float angle = Quaternion.Angle(currentRotation, targetRotation);
// 判断旋转是否近似相等
bool isSimilar = Quaternion.Angle(rot1, rot2) < 1f; // 1度误差内
// 创建特定轴的旋转
Quaternion xRotation = Quaternion.AngleAxis(angle, Vector3.right);
Quaternion yRotation = Quaternion.AngleAxis(angle, Vector3.up);
Quaternion zRotation = Quaternion.AngleAxis(angle, Vector3.forward);
第五部分:四元数与欧拉角的转换
1. 相互转换的方法
csharp
// 欧拉角 -> 四元数
Vector3 euler = new Vector3(30, 45, 60);
Quaternion fromEuler = Quaternion.Euler(euler);
// 四元数 -> 欧拉角
Quaternion rotation = transform.rotation;
Vector3 toEuler = rotation.eulerAngles;
// 注意:欧拉角范围是[0, 360),可能有多个表示方式
2. 正确处理欧拉角
csharp
public class SafeEulerManipulation : MonoBehaviour {
// 错误方式:直接修改欧拉角可能导致万向节死锁
void WrongWay() {
Vector3 euler = transform.eulerAngles;
euler.x += 10 * Time.deltaTime;
transform.eulerAngles = euler; // 危险!
}
// 正确方式:使用四元数进行旋转
void CorrectWay() {
// 方法1:使用Rotate方法
transform.Rotate(10 * Time.deltaTime, 0, 0);
// 方法2:使用四元数乘法
Quaternion xRotation = Quaternion.AngleAxis(10 * Time.deltaTime, Vector3.right);
transform.rotation = xRotation * transform.rotation;
// 方法3:累加欧拉角,但转换为四元数应用
currentEulerX += 10 * Time.deltaTime;
transform.rotation = Quaternion.Euler(currentEulerX, currentEulerY, currentEulerZ);
}
private float currentEulerX, currentEulerY, currentEulerZ;
}
第六部分:实战应用案例
案例1:平滑的第三人称相机
csharp
public class ThirdPersonCamera : MonoBehaviour {
public Transform target;
public float distance = 5f;
public float height = 2f;
public float rotationSpeed = 3f;
public float heightDamping = 2f;
private float currentRotationAngle;
private float currentHeight;
void LateUpdate() {
if (!target) return;
// 计算目标角度和高度
float targetRotationAngle = target.eulerAngles.y;
float targetHeight = target.position.y + height;
// 平滑过渡
currentRotationAngle = Mathf.LerpAngle(currentRotationAngle, targetRotationAngle,
rotationSpeed * Time.deltaTime);
currentHeight = Mathf.Lerp(currentHeight, targetHeight,
heightDamping * Time.deltaTime);
// 转换为四元数旋转
Quaternion currentRotation = Quaternion.Euler(0, currentRotationAngle, 0);
// 计算相机位置
transform.position = target.position;
transform.position -= currentRotation * Vector3.forward * distance;
transform.position = new Vector3(transform.position.x, currentHeight, transform.position.z);
// 让相机看向目标
transform.LookAt(target);
}
}
案例2:角色移动与斜坡处理
csharp
public class CharacterMovement : MonoBehaviour {
public float moveSpeed = 5f;
public float rotationSpeed = 10f;
public float groundCheckDistance = 0.2f;
private CharacterController controller;
private Vector3 moveDirection;
void Start() {
controller = GetComponent<CharacterController>();
}
void Update() {
// 获取输入
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
// 计算移动方向(相对于摄像机)
Vector3 cameraForward = Camera.main.transform.forward;
cameraForward.y = 0;
cameraForward.Normalize();
Vector3 cameraRight = Camera.main.transform.right;
cameraRight.y = 0;
cameraRight.Normalize();
moveDirection = (cameraForward * vertical + cameraRight * horizontal).normalized;
if (moveDirection.magnitude > 0.1f) {
// 朝向移动方向
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation,
rotationSpeed * Time.deltaTime);
// 斜坡检测与调整
RaycastHit hit;
if (Physics.Raycast(transform.position, Vector3.down, out hit, groundCheckDistance + 0.1f)) {
// 将移动方向投影到斜坡平面
moveDirection = Vector3.ProjectOnPlane(moveDirection, hit.normal).normalized;
}
// 应用移动
Vector3 movement = moveDirection * moveSpeed * Time.deltaTime;
controller.Move(movement);
}
}
}
案例3:武器悬挂系统(弹簧效果)
csharp
public class WeaponSway : MonoBehaviour {
public float swayAmount = 0.02f;
public float maxSwayAmount = 0.06f;
public float smoothFactor = 6f;
public float rotationSwayFactor = 4f;
private Vector3 initialPosition;
private Quaternion initialRotation;
private Vector2 mouseInput;
void Start() {
initialPosition = transform.localPosition;
initialRotation = transform.localRotation;
}
void Update() {
// 获取鼠标输入
float mouseX = Input.GetAxis("Mouse X") * swayAmount;
float mouseY = Input.GetAxis("Mouse Y") * swayAmount;
// 限制摆动幅度
mouseX = Mathf.Clamp(mouseX, -maxSwayAmount, maxSwayAmount);
mouseY = Mathf.Clamp(mouseY, -maxSwayAmount, maxSwayAmount);
// 计算目标位置和旋转
Vector3 targetPosition = new Vector3(mouseX, mouseY, 0) + initialPosition;
Quaternion targetRotation = initialRotation *
Quaternion.Euler(-mouseY * rotationSwayFactor,
mouseX * rotationSwayFactor, 0);
// 平滑插值
transform.localPosition = Vector3.Lerp(transform.localPosition, targetPosition,
smoothFactor * Time.deltaTime);
transform.localRotation = Quaternion.Slerp(transform.localRotation, targetRotation,
smoothFactor * Time.deltaTime);
}
}
第七部分:高级技巧与优化
1. 四元数缓存与重用
csharp
public class OptimizedRotation : MonoBehaviour {
private Quaternion targetRotation;
private bool needsRotationUpdate = false;
void Update() {
// 只在需要时计算目标旋转
if (Input.GetMouseButtonDown(0)) {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
Vector3 direction = hit.point - transform.position;
direction.y = 0;
if (direction != Vector3.zero) {
targetRotation = Quaternion.LookRotation(direction);
needsRotationUpdate = true;
}
}
}
// 平滑旋转到目标
if (needsRotationUpdate) {
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation,
5f * Time.deltaTime);
// 检查是否到达目标
if (Quaternion.Angle(transform.rotation, targetRotation) < 0.5f) {
needsRotationUpdate = false;
transform.rotation = targetRotation;
}
}
}
}
2. 四元数运算顺序优化
csharp
// 错误:频繁的连续旋转
void Update() {
transform.rotation *= Quaternion.Euler(0, 1, 0);
transform.rotation *= Quaternion.Euler(1, 0, 0);
}
// 正确:组合旋转后一次应用
Quaternion accumulatedRotation = Quaternion.identity;
void Update() {
Quaternion yRot = Quaternion.Euler(0, 1, 0);
Quaternion xRot = Quaternion.Euler(1, 0, 0);
// 组合旋转
accumulatedRotation = xRot * yRot * accumulatedRotation;
// 一帧应用一次
transform.rotation = accumulatedRotation;
}
3. 避免四元数翻转问题
csharp
Quaternion SafeSlerp(Quaternion from, Quaternion to, float t) {
float dot = Quaternion.Dot(from, to);
// 如果点积为负,四元数表示相反方向的旋转
// 取反其中一个,确保插值走最短路径
if (dot < 0) {
to = new Quaternion(-to.x, -to.y, -to.z, -to.w);
dot = -dot;
}
// 如果两个旋转非常接近,使用Lerp避免除零
if (dot > 0.9995f) {
return Quaternion.Lerp(from, to, t);
}
// 否则使用Slerp
return Quaternion.Slerp(from, to, t);
}
第八部分:常见问题与解决方案
问题1:旋转突然翻转
原因:四元数q和-q表示相同的旋转,但插值路径不同。
解决方案:
csharp
// 在Slerp前检查点积
float dot = Quaternion.Dot(startRot, endRot);
if (dot < 0) {
// 取反一个四元数,确保走最短路径
endRot = new Quaternion(-endRot.x, -endRot.y, -endRot.z, -endRot.w);
}
transform.rotation = Quaternion.Slerp(startRot, endRot, t);
问题2:旋转累积误差
原因:浮点数精度问题导致四元数不再是单位四元数。
解决方案:
csharp
// 定期归一化
void Update() {
// 进行多次旋转操作后...
transform.rotation = transform.rotation.normalized;
}
问题3:朝向旋转时上下颠倒
原因:LookRotation的默认向上方向是Vector3.up,在某些情况下可能导致翻转。
解决方案:
csharp
// 指定自定义向上方向
Vector3 direction = target.position - transform.position;
Vector3 customUp = CalculateCustomUpDirection(); // 根据场景计算
transform.rotation = Quaternion.LookRotation(direction, customUp);
总结:四元数思维模式
-
停止思考角度,开始思考旋转:四元数表示的是一个完整的旋转状态,而不是三个独立的角度。
-
理解乘法顺序 :记住四元数乘法是从右向左执行:
q3 = q2 * q1表示先执行q1,再执行q2。 -
优先使用四元数函数 :尽量使用
Quaternion.LookRotation、Quaternion.Slerp等函数,而不是直接操作四元数分量。 -
拥抱球面插值:对于旋转动画,Slerp是你的好朋友。
-
可视化调试 :使用
Debug.DrawRay绘制旋转轴和方向,帮助理解。
最终练习 :
尝试理解并实现一个完整的角色控制器,包括:
-
基于输入的移动和旋转
-
斜坡检测与适应
-
相机跟随与碰撞避免
-
动画状态与旋转同步
记住:四元数是工具,不是魔法。开始时可能会觉得抽象,但通过实践,你会逐渐建立直觉。从简单的小球旋转开始,逐步构建复杂的旋转系统,很快你就能驾驭这位"旋转大师"了!