#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Quat {
pub x: f32,
pub y: f32,
pub z: f32,
pub w: f32,
}
impl Quat {
pub const IDENTITY: Quat = Quat {
x: 0.0,
y: 0.0,
z: 0.0,
w: 1.0,
};
#[must_use]
pub fn from_axis_angle(axis: [f32; 3], rad: f32) -> Quat {
let len = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
if len < 1e-12 {
return Quat::IDENTITY;
}
let (s, c) = (rad * 0.5).sin_cos();
let k = s / len;
Quat {
x: axis[0] * k,
y: axis[1] * k,
z: axis[2] * k,
w: c,
}
}
#[must_use]
pub fn normalize(self) -> Quat {
let n = (self.x * self.x + self.y * self.y + self.z * self.z + self.w * self.w).sqrt();
if n < 1e-12 {
return Quat::IDENTITY;
}
Quat {
x: self.x / n,
y: self.y / n,
z: self.z / n,
w: self.w / n,
}
}
#[must_use]
pub fn rotate(self, v: [f32; 3]) -> [f32; 3] {
let (qx, qy, qz, qw) = (self.x, self.y, self.z, self.w);
let tx = 2.0 * (qy * v[2] - qz * v[1]);
let ty = 2.0 * (qz * v[0] - qx * v[2]);
let tz = 2.0 * (qx * v[1] - qy * v[0]);
[
v[0] + qw * tx + (qy * tz - qz * ty),
v[1] + qw * ty + (qz * tx - qx * tz),
v[2] + qw * tz + (qx * ty - qy * tx),
]
}
#[must_use]
pub fn nlerp(self, b: Quat, t: f32) -> Quat {
let dot = self.x * b.x + self.y * b.y + self.z * b.z + self.w * b.w;
let b = if dot < 0.0 {
Quat {
x: -b.x,
y: -b.y,
z: -b.z,
w: -b.w,
}
} else {
b
};
Quat {
x: self.x + (b.x - self.x) * t,
y: self.y + (b.y - self.y) * t,
z: self.z + (b.z - self.z) * t,
w: self.w + (b.w - self.w) * t,
}
.normalize()
}
#[must_use]
pub fn conjugate(self) -> Quat {
Quat {
x: -self.x,
y: -self.y,
z: -self.z,
w: self.w,
}
}
#[must_use]
pub fn from_basis(s: [f32; 3], h: [f32; 3], f: [f32; 3]) -> Quat {
let (m00, m11, m22) = (s[0], h[1], f[2]);
let trace = m00 + m11 + m22;
let q = if trace > 0.0 {
let d = (trace + 1.0).sqrt() * 2.0; Quat {
w: 0.25 * d,
x: (h[2] - f[1]) / d,
y: (f[0] - s[2]) / d,
z: (s[1] - h[0]) / d,
}
} else if m00 > m11 && m00 > m22 {
let d = (1.0 + m00 - m11 - m22).sqrt() * 2.0; Quat {
w: (h[2] - f[1]) / d,
x: 0.25 * d,
y: (h[0] + s[1]) / d,
z: (f[0] + s[2]) / d,
}
} else if m11 > m22 {
let d = (1.0 + m11 - m00 - m22).sqrt() * 2.0; Quat {
w: (f[0] - s[2]) / d,
x: (h[0] + s[1]) / d,
y: 0.25 * d,
z: (f[1] + h[2]) / d,
}
} else {
let d = (1.0 + m22 - m00 - m11).sqrt() * 2.0; Quat {
w: (s[1] - h[0]) / d,
x: (f[0] + s[2]) / d,
y: (f[1] + h[2]) / d,
z: 0.25 * d,
}
};
q.normalize()
}
#[must_use]
pub fn from_euler(roll_x: f32, pitch_y: f32, yaw_z: f32) -> Quat {
Quat::from_axis_angle([0.0, 0.0, 1.0], yaw_z)
* Quat::from_axis_angle([0.0, 1.0, 0.0], pitch_y)
* Quat::from_axis_angle([1.0, 0.0, 0.0], roll_x)
}
#[must_use]
pub fn to_euler(self) -> [f32; 3] {
let Quat { x, y, z, w } = self;
let roll = (2.0 * (w * x + y * z)).atan2(1.0 - 2.0 * (x * x + y * y));
let sin_pitch = (2.0 * (w * y - z * x)).clamp(-1.0, 1.0);
let pitch = sin_pitch.asin();
let yaw = (2.0 * (w * z + x * y)).atan2(1.0 - 2.0 * (y * y + z * z));
[roll, pitch, yaw]
}
}
impl core::ops::Mul for Quat {
type Output = Quat;
fn mul(self, r: Quat) -> Quat {
Quat {
w: self.w * r.w - self.x * r.x - self.y * r.y - self.z * r.z,
x: self.w * r.x + self.x * r.w + self.y * r.z - self.z * r.y,
y: self.w * r.y - self.x * r.z + self.y * r.w + self.z * r.x,
z: self.w * r.z + self.x * r.y - self.y * r.x + self.z * r.w,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BoneXform {
pub t: [f32; 3],
pub r: Quat,
pub s: [f32; 3],
}
impl BoneXform {
pub const IDENTITY: BoneXform = BoneXform {
t: [0.0, 0.0, 0.0],
r: Quat::IDENTITY,
s: [1.0, 1.0, 1.0],
};
#[must_use]
pub fn from_hinge_angle(axis: [f32; 3], val: i16) -> BoneXform {
let rad = f32::from(val) * (core::f32::consts::PI * 2.0 / 65536.0);
BoneXform {
t: [0.0, 0.0, 0.0],
r: Quat::from_axis_angle(axis, -rad),
s: [1.0, 1.0, 1.0],
}
}
#[must_use]
pub fn hinge_angle(self, axis: [f32; 3]) -> i16 {
let len = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
if len < 1e-12 {
return 0;
}
let s = (self.r.x * axis[0] + self.r.y * axis[1] + self.r.z * axis[2]) / len;
let phi = 2.0 * s.atan2(self.r.w); let val = -phi * (65536.0 / (core::f32::consts::PI * 2.0));
val.round() as i32 as i16
}
#[must_use]
pub fn blend(self, b: BoneXform, t: f32) -> BoneXform {
let lerp3 = |x: [f32; 3], y: [f32; 3]| {
[
x[0] + (y[0] - x[0]) * t,
x[1] + (y[1] - x[1]) * t,
x[2] + (y[2] - x[2]) * t,
]
};
BoneXform {
t: lerp3(self.t, b.t),
r: self.r.nlerp(b.r, t),
s: lerp3(self.s, b.s),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: [f32; 3], b: [f32; 3]) -> bool {
(0..3).all(|i| (a[i] - b[i]).abs() < 1e-5)
}
#[test]
fn axis_angle_rotates_in_the_right_hand_sense() {
let q = Quat::from_axis_angle([0.0, 0.0, 1.0], core::f32::consts::FRAC_PI_2);
assert!(approx(q.rotate([1.0, 0.0, 0.0]), [0.0, 1.0, 0.0]));
assert!(approx(q.rotate([0.0, 0.0, 1.0]), [0.0, 0.0, 1.0]));
}
#[test]
fn identity_and_normalize() {
assert!(approx(
Quat::IDENTITY.rotate([3.0, -2.0, 5.0]),
[3.0, -2.0, 5.0]
));
let n = Quat {
x: 0.0,
y: 0.0,
z: 2.0,
w: 0.0,
}
.normalize();
assert!((n.x * n.x + n.y * n.y + n.z * n.z + n.w * n.w - 1.0).abs() < 1e-6);
}
#[test]
fn nlerp_hits_the_endpoints() {
let a = Quat::IDENTITY;
let b = Quat::from_axis_angle([0.0, 1.0, 0.0], 1.0);
assert!((a.nlerp(b, 0.0).w - a.w).abs() < 1e-6);
let end = a.nlerp(b, 1.0);
assert!((end.w - b.w).abs() < 1e-6 && (end.y - b.y).abs() < 1e-6);
}
#[test]
fn hinge_angle_inverts_from_hinge_angle() {
let axis = [0.0, 0.0, 1.0];
for val in [0i16, 1000, 16384, -16384, 30000, -30000] {
let got = BoneXform::from_hinge_angle(axis, val).hinge_angle(axis);
assert!(
(i32::from(got) - i32::from(val)).abs() <= 1,
"{val} -> {got}"
);
}
}
#[test]
fn euler_round_trips_away_from_gimbal_lock() {
let e = [0.3_f32, -0.7, 1.1];
let got = Quat::from_euler(e[0], e[1], e[2]).to_euler();
assert!(
(0..3).all(|i| (got[i] - e[i]).abs() < 1e-4),
"{got:?} vs {e:?}"
);
let qz = Quat::from_euler(0.0, 0.0, core::f32::consts::FRAC_PI_2);
assert!(approx(qz.rotate([1.0, 0.0, 0.0]), [0.0, 1.0, 0.0]));
}
#[test]
fn from_basis_recovers_the_rotation() {
let q = Quat::from_axis_angle([0.3, 0.5, 0.8], 1.2);
let s = q.rotate([1.0, 0.0, 0.0]);
let h = q.rotate([0.0, 1.0, 0.0]);
let f = q.rotate([0.0, 0.0, 1.0]);
let got = Quat::from_basis(s, h, f);
let v = [0.2, -0.9, 0.4];
assert!(approx(got.rotate(v), q.rotate(v)));
}
#[test]
fn conjugate_inverts() {
let q = Quat::from_axis_angle([0.0, 1.0, 0.0], 0.9);
let id = q * q.conjugate();
assert!(approx(id.rotate([1.0, 2.0, 3.0]), [1.0, 2.0, 3.0]));
}
#[test]
fn mul_composes_rotations() {
let h = Quat::from_axis_angle([0.0, 0.0, 1.0], core::f32::consts::FRAC_PI_4);
let full = h * h;
assert!(approx(full.rotate([1.0, 0.0, 0.0]), [0.0, 1.0, 0.0]));
}
}