欧拉角与四元数互转,及四元数slerp球面线性插值算法

23 篇文章 0 订阅
8 篇文章 1 订阅

1. 欧拉角与四元数是什么?

roll:翻滚角,pitch:俯仰角,heading:航向角

roll、pitch、heading,这3个角又称为欧拉角,欧拉角是弧度。弧度与度°可以通过公式转换;

四元数:w,x,y,z,有 xx+yy+zz+ww = 1,四元数在计算机图形学中是姿态和姿态内插中常用的一种表达。
四元数更能表达光滑移动的相机,球面线性插值具有连续性,在旋转之间做内插和形成刚性变换链也都比较容易。

欧拉角与四元数可以互转,四元数插值完在转回欧拉角,对于航向角突变的情况会更准确;

  • Math.toDegrees(eulerAngles.roll); // 弧度转角度
  • Math.toRadians(roll); // 角度转弧度
  • roll 范围 [-180°~180°]
  • pitch 范围 [-180°~180°]
  • heading 范围 [0°~360°]

2. 源码

2.1 欧拉角类

package test;

/****************************
 * Class Name: EulerAngles
 * Description: <欧拉角类>
 * @Author: seminar
 * @create: 2021/05/21
 * @since: 1.0.0
 ***************************/
public class EulerAngles {
    /**
     * Math.toRadians(roll) 角度转弧度
     * Math.toDegrees(roll) 弧度转角度
     * <p>
     * 翻滚角(roll) 弧度
     */
    public double roll;

    /**
     * 俯仰角(pitch) 弧度
     */
    public double pitch;

    /**
     * yaw 即heading(航向角) 弧度
     */
    public double yaw;

    public EulerAngles(float pitch, float yaw, float roll) {
        this.pitch = pitch;
        this.yaw = yaw;
        this.roll = roll;
    }

    public EulerAngles(float w, float x, float y, float z) {
        // roll (x-axis rotation)
        float sinr_cosp = 2 * (w * x + y * z);
        float cosr_cosp = 1 - 2 * (x * x + y * y);
        this.roll = (float) Math.atan2(sinr_cosp, cosr_cosp);

        // pitch (y-axis rotation)
        float sinp = 2 * (w * y - z * x);
        if (Math.abs(sinp) >= 1) {
            this.pitch = Math.copySign(1.57075f, sinp); // use 90 degrees if out of range
        } else {
            this.pitch = (float) Math.asin(sinp);
        }

        // yaw (z-axis rotation)
        float siny_cosp = 2 * (w * z + x * y);
        float cosy_cosp = 1 - 2 * (y * y + z * z);
        this.yaw = (float) Math.atan2(siny_cosp, cosy_cosp);
    }

    public Quaternion toQuaternion() {
        //欧拉角转四元数,角度减半是因为四元数旋转计算时需要旋转两次,具体原理请查看四元数原理
        float cy = (float) Math.cos(yaw * 0.5f);
        float sy = (float) Math.sin(yaw * 0.5f);
        float cp = (float) Math.cos(pitch * 0.5f);
        float sp = (float) Math.sin(pitch * 0.5f);
        float cr = (float) Math.cos(roll * 0.5f);
        float sr = (float) Math.sin(roll * 0.5f);
        Quaternion q = new Quaternion();
        q.w = cy * cp * cr + sy * sp * sr;
        q.x = cy * cp * sr - sy * sp * cr;
        q.y = sy * cp * sr + cy * sp * cr;
        q.z = sy * cp * cr - cy * sp * sr;
        return q;
    }
}

2.2 四元数类

package test;

import lombok.extern.slf4j.Slf4j;

/****************************
 * Class Name: Quaternion
 * Description: <四元数类>
 * @Author: seminar
 * @create: 2021/05/21
 * @since: 1.0.0
 ***************************/
@Slf4j
public class Quaternion {
    public float w;
    public float x;
    public float y;
    public float z;

    public Quaternion() {
    }

    public Quaternion(Quaternion b) {
        this.w = b.w;
        this.x = b.x;
        this.y = b.y;
        this.z = b.z;
    }

    public Quaternion(float w, float x, float y, float z) {
        this.w = w;
        this.x = x;
        this.y = y;
        this.z = z;
    }

    //向量旋转
    static void VectorRotation(float[] vector, Quaternion q) {
        Quaternion qv = new Quaternion(0, vector[0], vector[1], vector[2]);
        //四元数旋转公式q0*qv*(q0逆)s
        qv = Quaternion.Multiplication(Quaternion.Multiplication(q, qv), q.Inverse());
        vector[0] = qv.x;
        vector[1] = qv.y;
        vector[2] = qv.z;
    }


    //返回欧拉角
    public EulerAngles toEulerAngles() {
        // roll (x-axis rotation)
        return new EulerAngles(this.w, this.x, this.y, this.z);
    }

    //四元数相乘
    static Quaternion Multiplication(Quaternion q0, Quaternion q1) {
        Quaternion ret = new Quaternion();
        ret.w = q0.w * q1.w - q0.x * q1.x - q0.y * q1.y - q0.z * q1.z;
        ret.x = q0.w * q1.x + q0.x * q1.w + q0.y * q1.z - q0.z * q1.y;
        ret.y = q0.w * q1.y + q0.y * q1.w + q0.z * q1.x - q0.x * q1.z;
        ret.z = q0.w * q1.z + q0.z * q1.w + q0.x * q1.y - q0.y * q1.x;
        return ret;
    }

    //四元数求逆
    public Quaternion Inverse() {
        Quaternion ret;
        ret = this;
        ret.x *= -1;
        ret.y *= -1;
        ret.z *= -1;
        return ret;
    }
}

2.3 欧拉角与四元数互转及球面线性插值算法

球面线性插值也称四元数内插,更加光滑;

package test;

import test.EulerAngles;
import test.Quaternion;
import lombok.extern.slf4j.Slf4j;

import static java.lang.Math.abs;

/*************************************
 *Class Name: EulerAngle2QuatUtil
 *Description: <四元数与欧拉角互转>
 *@author: seminar
 *@create: 2021/5/24
 *@since 1.0.0
 *************************************/
@Slf4j
public class EulerAngle2QuatUtil {

    /**
     * 归一化
     *
     * @param x
     * @param y
     * @param z
     * @param w
     * @return
     */
    public Quaternion normalizeQuaternion(float w, float x, float y, float z) {
        double lengthD = 1.0f / (w * w + x * x + y * y + z * z);
        w *= lengthD;
        x *= lengthD;
        y *= lengthD;
        z *= lengthD;
        return new Quaternion(w, x, y, z);
    }

    /**
     * Slerp球面线性插值(Spherical Linear Interpolation)
     *
     * @param a 原始数据a
     * @param b 原始数据b
     * @param t 要插值的比例(中间插一个值1/2)
     * @return
     */
    public Quaternion makeInterpolated(Quaternion a, Quaternion b, double t) {
        Quaternion out = new Quaternion();
        double cosHalfTheta = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
        if (cosHalfTheta < 0.0F) {
            b = new Quaternion(b);
            cosHalfTheta = -cosHalfTheta;
            b.x = -b.x;
            b.y = -b.y;
            b.z = -b.z;
            b.w = -b.w;
        }

        double halfTheta = (double) Math.acos((double) cosHalfTheta);
        double sinHalfTheta = (double) Math.sqrt((double) (1.0F - cosHalfTheta * cosHalfTheta));
        double ratioA;
        double ratioB;
        if ((double) abs(sinHalfTheta) > 0.001D) {
            double oneOverSinHalfTheta = 1.0F / sinHalfTheta;
            ratioA = (double) Math.sin((double) ((1.0F - t) * halfTheta)) * oneOverSinHalfTheta;
            ratioB = (double) Math.sin((double) (t * halfTheta)) * oneOverSinHalfTheta;
        } else {
            ratioA = 1.0F - t;
            ratioB = t;
        }

        out.x = (float) (ratioA * a.x + ratioB * b.x);
        out.y = (float) (ratioA * a.y + ratioB * b.y);
        out.z = (float) (ratioA * a.z + ratioB * b.z);
        out.w = (float) (ratioA * a.w + ratioB * b.w);

        out = normalizeQuaternion(out.w, out.x, out.y, out.z);
        return out;
    }

    /**
     * 欧拉角(弧度)转四元数
     *
     * @param pitch
     * @param yaw
     * @param roll
     * @return
     */
    public Quaternion toQuaternion(double pitch, double yaw, double roll) {
        EulerAngles eu = new EulerAngles((float) Math.toRadians(pitch), (float) Math.toRadians(yaw), (float) Math.toRadians(roll)); // 角度转弧度
        return eu.toQuaternion();
    }

    /**
     * 四元数转欧拉角(弧度)
     *
     * @param quaternion
     * @return
     */
    public EulerAngles toEulerAngles(Quaternion quaternion) {
        return quaternion.toEulerAngles();
    }

    /**
     * 姿态角——即欧拉角转四元数,对俩个四元数进行球面插值,四元数转回欧拉角并返回
     *
     * @param pitch    位置一俯仰角 -180~180
     * @param yaw      位置一航向角 0~360
     * @param roll     位置一翻滚角 -180~180
     * @param pitch1   位置二俯仰角 -180~180
     * @param yaw1     位置二俯仰角 0~360°
     * @param roll1    位置二翻滚角 -180~180
     * @param t        位置一时间
     * @param t1       位置二时间
     * @param t_insert 要计算姿态角的位置对应时间
     * @return
     */
    public EulerAngles slerpInsert(float pitch, float yaw, float roll, float pitch1, float yaw1, float roll1, long t, long t1, long t_insert) {
        // 位置1 欧拉角转四元数
        // 位置2 欧拉角转四元数
        Quaternion p = toQuaternion(pitch, yaw, roll);
        Quaternion q = toQuaternion(pitch1, yaw1, roll1);

        // 计算插入的scale
        float scale = (float) ((t_insert - t) / ((t1 - t) * 1.0));

        // Slerp球面线性插值
        Quaternion r = makeInterpolated(q, p, scale);

        // 四元数转欧拉角
        EulerAngles eulerAngles = r.toEulerAngles();
        return eulerAngles;
    }

    public static void main(String[] args) {
//        示例,中间1615609866585L的插值不太对
//                         Roll    Pitch    Heading
// 1615609866544L         -0.9    -0.405   358.809
// 1615609866585L         -0.942   -0.362  314.489
// 1615609866625L         -0.956   -0.331  0.178

//        正确结果
//                         Roll    Pitch    Heading
// 1615609866544L         -0.9,    -0.405,  358.809
// 1615609866585L         -0.929, -0.368, 359.502
// 1615609866625L         -0.956,  -0.331,  0.178

        // 调用EulerAngle2QuatUtil实现姿态角插值的获取
        float roll = -0.9f, pitch = -0.405f, yaw = 358.809f;
        EulerAngle2QuatUtil eq = new EulerAngle2QuatUtil();
        Quaternion p = eq.toQuaternion(pitch, yaw, roll);
        log.info("p: {} {} {} {}", p.w, p.x, p.y, p.z);

        float roll1 = -0.956f, pitch1 = -0.331f, yaw1 = 0.178f;
        Quaternion q = eq.toQuaternion(pitch1, yaw1, roll1);
        log.info("q: {} {} {} {}", q.w, q.x, q.y, q.z);

        long t = 1615609866544L;
        long t1 = 1615609866625L;
        long t_insert = 1615609866585L;
        float scale = (float) ((t_insert - t) / ((t1 - t) * 1.0));

        // Slerp球面线性插值
        Quaternion r = eq.makeInterpolated(q, p, scale);
        EulerAngles eulerAngles = r.toEulerAngles();
        float roll2 = (float) Math.toDegrees(eulerAngles.roll); // 弧度转回角度
        float pitch2 = (float) Math.toDegrees(eulerAngles.pitch); // 弧度转回角度
        float heading2 = (float) (Math.toDegrees(eulerAngles.yaw) > 0 ? Math.toDegrees(eulerAngles.yaw) : Math.toDegrees(eulerAngles.yaw) + 360); // 弧度转回角度(航向角0~360°)

        log.info("{} {} {}", Double.parseDouble(String.format("%.3f", roll2)), Double.parseDouble(String.format("%.3f", pitch2)), Double.parseDouble(String.format("%.3f", heading2)));

        testSlerpInsert(pitch, yaw, roll, pitch1, yaw1, roll1, t, t1, t_insert);

//  0.000     -8.523      0.000
//  0.000     -0.432     93.112
        testSlerpInsert(-8.523f, 0.00f, 0.00f, -0.432f, 93.112f, 0.00f, t, t1, t_insert);
//        0.000      1.054     66.847
//        1.237     -1.956     62.336
        testSlerpInsert(1.054f, 66.847f, 0.00f, -1.956f, 62.336f, 1.237f, t, t1, t_insert);

//        0.411      5.393    338.058
//        0.402      5.395    338.063
        testSlerpInsert(5.393f, 338.058f, 0.411f, 5.395f, 338.063f, 0.402f, t, t1, t_insert);
    }

    private static void testSlerpInsert(float pitch, float yaw, float roll, float pitch1, float yaw1, float roll1, long t, long t1, long t_insert) {
        log.info("==================testSlerpInsert start===============");
        EulerAngle2QuatUtil eq = new EulerAngle2QuatUtil();
        EulerAngles eulerAngles = eq.slerpInsert(pitch, yaw, roll, pitch1, yaw1, roll1, t, t1, t_insert);
        float roll2 = (float) Math.toDegrees(eulerAngles.roll); // 弧度转回角度
        float pitch2 = (float) Math.toDegrees(eulerAngles.pitch); // 弧度转回角度
        float heading2 = (float) (Math.toDegrees(eulerAngles.yaw) > 0 ? Math.toDegrees(eulerAngles.yaw) : Math.toDegrees(eulerAngles.yaw) + 360); // 弧度转回角度(航向角0~360°)

        log.info("slerpInsert {} {} {}", Double.parseDouble(String.format("%.3f", roll2)), Double.parseDouble(String.format("%.3f", pitch2)), Double.parseDouble(String.format("%.3f", heading2)));
        log.info("==================testSlerpInsert end=================");
    }

    private static Quaternion getQuaternion(float roll, float pitch, float yaw) {
        EulerAngle2QuatUtil eq = new EulerAngle2QuatUtil();
        EulerAngles eu = new EulerAngles((float) Math.toRadians(pitch), (float) Math.toRadians(yaw), (float) Math.toRadians(roll));
        Quaternion quaternion = eu.toQuaternion();
        EulerAngles eulerAngles = quaternion.toEulerAngles();
        float roll2 = (float) Math.toDegrees(eulerAngles.roll); // 弧度转回角度
        float pitch2 = (float) Math.toDegrees(eulerAngles.pitch); // 弧度转回角度
        float heading2 = (float) (Math.toDegrees(eulerAngles.yaw) > 0 ? Math.toDegrees(eulerAngles.yaw) : Math.toDegrees(eulerAngles.yaw) + 360); // 弧度转回角度(航向角0~360°)
        log.info("toDegree: {} {} {}", Double.parseDouble(String.format("%.3f", roll2)), Double.parseDouble(String.format("%.3f", pitch2)), Double.parseDouble(String.format("%.3f", heading2)));
        return quaternion;
    }
}

参考

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序媛一枚~

您的鼓励是我创作的最大动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值