use super::so3::{vec3_norm, SO3};
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct UnitQuat<S: ControlScalar> {
pub w: S,
pub x: S,
pub y: S,
pub z: S,
}
impl<S: ControlScalar> UnitQuat<S> {
#[inline]
pub fn identity() -> Self {
Self {
w: S::ONE,
x: S::ZERO,
y: S::ZERO,
z: S::ZERO,
}
}
pub fn from_axis_angle(axis: [S; 3], angle: S) -> Self {
let n = vec3_norm(axis);
if n < S::EPSILON * S::from_f64(1e3) {
return Self::identity();
}
let inv_n = S::ONE / n;
let half = angle * S::HALF;
let s = half.sin();
let c = half.cos();
Self {
w: c,
x: axis[0] * inv_n * s,
y: axis[1] * inv_n * s,
z: axis[2] * inv_n * s,
}
}
#[inline]
fn raw(w: S, x: S, y: S, z: S) -> Self {
Self { w, x, y, z }
}
pub fn normalize(&self) -> Self {
let n = self.norm();
if n < S::EPSILON * S::from_f64(1e3) {
return Self::identity();
}
let inv = S::ONE / n;
Self {
w: self.w * inv,
x: self.x * inv,
y: self.y * inv,
z: self.z * inv,
}
}
#[inline]
fn norm(&self) -> S {
(self.w * self.w + self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
}
}
impl<S: ControlScalar> UnitQuat<S> {
pub fn multiply(&self, other: &UnitQuat<S>) -> UnitQuat<S> {
let (w1, x1, y1, z1) = (self.w, self.x, self.y, self.z);
let (w2, x2, y2, z2) = (other.w, other.x, other.y, other.z);
UnitQuat::raw(
w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2,
)
}
#[inline]
pub fn conjugate(&self) -> Self {
Self {
w: self.w,
x: -self.x,
y: -self.y,
z: -self.z,
}
}
pub fn rotate_vector(&self, v: [S; 3]) -> [S; 3] {
let qv = [self.x, self.y, self.z];
let t = [
S::TWO * (qv[1] * v[2] - qv[2] * v[1]),
S::TWO * (qv[2] * v[0] - qv[0] * v[2]),
S::TWO * (qv[0] * v[1] - qv[1] * v[0]),
];
[
v[0] + self.w * t[0] + (qv[1] * t[2] - qv[2] * t[1]),
v[1] + self.w * t[1] + (qv[2] * t[0] - qv[0] * t[2]),
v[2] + self.w * t[2] + (qv[0] * t[1] - qv[1] * t[0]),
]
}
pub fn error(&self, q_d: &UnitQuat<S>) -> UnitQuat<S> {
q_d.conjugate().multiply(self)
}
pub fn slerp(&self, other: &UnitQuat<S>, t: S) -> UnitQuat<S> {
let mut cos_half =
self.w * other.w + self.x * other.x + self.y * other.y + self.z * other.z;
let other_flip;
let other_ref: &UnitQuat<S>;
if cos_half < S::ZERO {
other_flip = UnitQuat::raw(-other.w, -other.x, -other.y, -other.z);
cos_half = -cos_half;
other_ref = &other_flip;
} else {
other_ref = other;
}
let (s0, s1);
if cos_half > S::from_f64(0.9995) {
s0 = S::ONE - t;
s1 = t;
} else {
let half_theta = cos_half.clamp_val(S::from_f64(-1.0), S::ONE).acos();
let sin_half = half_theta.sin();
s0 = ((S::ONE - t) * half_theta).sin() / sin_half;
s1 = (t * half_theta).sin() / sin_half;
}
UnitQuat::raw(
s0 * self.w + s1 * other_ref.w,
s0 * self.x + s1 * other_ref.x,
s0 * self.y + s1 * other_ref.y,
s0 * self.z + s1 * other_ref.z,
)
}
}
impl<S: ControlScalar> UnitQuat<S> {
pub fn to_so3(&self) -> SO3<S> {
let q = [self.w, self.x, self.y, self.z];
SO3::from_quaternion(q).unwrap_or_else(|_| SO3::identity())
}
pub fn from_so3(r: &SO3<S>) -> UnitQuat<S> {
let q = r.to_quaternion();
UnitQuat::raw(q[0], q[1], q[2], q[3])
}
#[inline]
pub fn as_array(&self) -> [S; 4] {
[self.w, self.x, self.y, self.z]
}
}
pub struct QuatKinematics;
impl QuatKinematics {
pub fn integrate<S: ControlScalar>(q: &UnitQuat<S>, omega: [S; 3], dt: S) -> UnitQuat<S> {
let theta = vec3_norm(omega) * dt;
if theta < S::from_f64(1e-10) {
return q.normalize();
}
let delta = UnitQuat::from_axis_angle(omega, theta);
q.multiply(&delta).normalize()
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-10;
const EPS_MED: f64 = 1e-7;
fn nearly_equal(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn quat_nearly_equal(a: &UnitQuat<f64>, b: &UnitQuat<f64>, tol: f64) -> bool {
let same = nearly_equal(a.w, b.w, tol)
&& nearly_equal(a.x, b.x, tol)
&& nearly_equal(a.y, b.y, tol)
&& nearly_equal(a.z, b.z, tol);
let flip = nearly_equal(a.w, -b.w, tol)
&& nearly_equal(a.x, -b.x, tol)
&& nearly_equal(a.y, -b.y, tol)
&& nearly_equal(a.z, -b.z, tol);
same || flip
}
#[test]
fn identity_rotate_vector() {
let q = UnitQuat::<f64>::identity();
let v = [1.0, 2.0, 3.0];
let rv = q.rotate_vector(v);
for i in 0..3 {
assert!(
nearly_equal(rv[i], v[i], EPS),
"rv[{}]={} v[{}]={}",
i,
rv[i],
i,
v[i]
);
}
}
#[test]
fn multiply_associativity() {
let q1 = UnitQuat::<f64>::from_axis_angle([1.0, 0.0, 0.0], 0.5);
let q2 = UnitQuat::<f64>::from_axis_angle([0.0, 1.0, 0.0], 0.8);
let q3 = UnitQuat::<f64>::from_axis_angle([0.0, 0.0, 1.0], 1.2);
let left = q1.multiply(&q2).multiply(&q3);
let right = q1.multiply(&q2.multiply(&q3));
assert!(quat_nearly_equal(&left, &right, EPS_MED));
}
#[test]
fn rotate_then_un_rotate() {
let axis = [1.0_f64 / 3.0_f64.sqrt(); 3];
let angle = 1.1_f64;
let q = UnitQuat::<f64>::from_axis_angle(axis, angle);
let q_c = q.conjugate();
let v = [2.0, -1.0, 3.0];
let rv = q.rotate_vector(v);
let v2 = q_c.rotate_vector(rv);
for i in 0..3 {
assert!(
nearly_equal(v2[i], v[i], EPS_MED),
"v2[{}]={} v[{}]={}",
i,
v2[i],
i,
v[i]
);
}
}
#[test]
fn conjugate_is_inverse() {
let q = UnitQuat::<f64>::from_axis_angle([0.0, 1.0, 0.0], 1.0);
let qc = q.conjugate();
let eye = q.multiply(&qc);
assert!(nearly_equal(eye.w, 1.0, EPS_MED));
assert!(nearly_equal(eye.x, 0.0, EPS_MED));
assert!(nearly_equal(eye.y, 0.0, EPS_MED));
assert!(nearly_equal(eye.z, 0.0, EPS_MED));
}
#[test]
fn to_so3_from_so3_roundtrip() {
let q1 = UnitQuat::<f64>::from_axis_angle([1.0, 1.0, 0.0], 0.9).normalize();
let r = q1.to_so3();
let q2 = UnitQuat::<f64>::from_so3(&r);
assert!(quat_nearly_equal(&q1, &q2, 1e-9));
}
#[test]
fn slerp_endpoints() {
let q1 = UnitQuat::<f64>::from_axis_angle([0.0, 0.0, 1.0], 0.0);
let q2 = UnitQuat::<f64>::from_axis_angle([0.0, 0.0, 1.0], 1.0);
let s0 = q1.slerp(&q2, 0.0);
let s1 = q1.slerp(&q2, 1.0);
assert!(quat_nearly_equal(&s0, &q1, EPS_MED));
assert!(quat_nearly_equal(&s1, &q2, EPS_MED));
}
#[test]
fn kinematics_integrate_small_step() {
let n = 1000_usize;
let total_angle = core::f64::consts::FRAC_PI_2;
let omega_z = total_angle; let dt = 1.0 / (n as f64);
let mut q = UnitQuat::<f64>::identity();
for _ in 0..n {
q = QuatKinematics::integrate(&q, [0.0, 0.0, omega_z], dt);
}
let expected_w = (total_angle / 2.0).cos();
let expected_z = (total_angle / 2.0).sin();
let expected = UnitQuat::<f64>::raw(expected_w, 0.0, 0.0, expected_z);
assert!(
quat_nearly_equal(&q, &expected, 1e-4),
"q=[{},{},{},{}] expected=[{},{},{},{}]",
q.w,
q.x,
q.y,
q.z,
expected_w,
0.0,
0.0,
expected_z
);
}
#[test]
fn normalize_corrects_drift() {
let mut q = UnitQuat::<f64>::from_axis_angle([1.0, 0.0, 0.0], 0.5);
q.w += 0.1;
let qn = q.normalize();
let norm_sq = qn.w * qn.w + qn.x * qn.x + qn.y * qn.y + qn.z * qn.z;
assert!(nearly_equal(norm_sq, 1.0, EPS_MED));
}
}