unity四元数 - “处理旋转的大师”

第一部分:为什么需要四元数?旋转问题的困境

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);

总结:四元数思维模式

  1. 停止思考角度,开始思考旋转:四元数表示的是一个完整的旋转状态,而不是三个独立的角度。

  2. 理解乘法顺序 :记住四元数乘法是从右向左执行:q3 = q2 * q1 表示先执行q1,再执行q2。

  3. 优先使用四元数函数 :尽量使用 Quaternion.LookRotationQuaternion.Slerp 等函数,而不是直接操作四元数分量。

  4. 拥抱球面插值:对于旋转动画,Slerp是你的好朋友。

  5. 可视化调试 :使用 Debug.DrawRay 绘制旋转轴和方向,帮助理解。

最终练习

尝试理解并实现一个完整的角色控制器,包括:

  • 基于输入的移动和旋转

  • 斜坡检测与适应

  • 相机跟随与碰撞避免

  • 动画状态与旋转同步

记住:四元数是工具,不是魔法。开始时可能会觉得抽象,但通过实践,你会逐渐建立直觉。从简单的小球旋转开始,逐步构建复杂的旋转系统,很快你就能驾驭这位"旋转大师"了!

相关推荐
wuguan_2 小时前
C#索引器
c#·索引器
聪明努力的积极向上2 小时前
【设计】分批查询数据通用方法(基于接口 + 泛型 + 定点复制)
开发语言·设计模式·c#
张人玉3 小时前
C# WPF 折线图制作(可以连接数据库)
数据库·c#·wpf·sugar
kylezhao20193 小时前
C# 中的委托(Delegate)与事件(Event)
c#·c#上位机
野区捕龙为宠3 小时前
unity 实现3D空间音效特性
3d·unity·游戏引擎
lzhdim4 小时前
C#应用程序取得当前目录和退出
开发语言·数据库·microsoft·c#
wuguan_4 小时前
C#之接口
c#·接口
老朱佩琪!4 小时前
Unity外观模式
unity·游戏引擎·外观模式
程序员茶馆4 小时前
【unity】Shader艺术之unity内置变量个性化控制
unity·游戏引擎