use std::cmp::Ordering;
use std::fmt::Display;
use std::ops::{Mul, MulAssign};
use godot_ffi as sys;
use sys::{ExtVariantType, GodotFfi, ffi_methods};
use crate::builtin::math::{ApproxEq, FloatExt, GlamConv, GlamType, XformInv};
use crate::builtin::real_consts::FRAC_PI_2;
use crate::builtin::{EulerOrder, Quaternion, RMat3, RQuat, RVec2, RVec3, Vector3, real};
#[derive(Copy, Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct Basis {
pub rows: [Vector3; 3],
}
impl Basis {
pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0, 1.0);
pub const FLIP_X: Self = Self::from_diagonal(-1.0, 1.0, 1.0);
pub const FLIP_Y: Self = Self::from_diagonal(1.0, -1.0, 1.0);
pub const FLIP_Z: Self = Self::from_diagonal(1.0, 1.0, -1.0);
pub const fn from_rows(x: Vector3, y: Vector3, z: Vector3) -> Self {
Self { rows: [x, y, z] }
}
pub const fn from_cols(a: Vector3, b: Vector3, c: Vector3) -> Self {
Self::from_rows_array(&[a.x, b.x, c.x, a.y, b.y, c.y, a.z, b.z, c.z])
}
pub fn from_axis_angle(axis: Vector3, angle: real) -> Self {
RMat3::from_axis_angle(axis.to_glam(), angle).to_front()
}
pub const fn from_diagonal(x: real, y: real, z: real) -> Self {
Self {
rows: [
Vector3::new(x, 0.0, 0.0),
Vector3::new(0.0, y, 0.0),
Vector3::new(0.0, 0.0, z),
],
}
}
pub const fn from_scale(scale: Vector3) -> Self {
Self::from_diagonal(scale.x, scale.y, scale.z)
}
const fn from_rows_array(rows: &[real; 9]) -> Self {
let [ax, bx, cx, ay, by, cy, az, bz, cz] = rows;
Self::from_rows(
Vector3::new(*ax, *bx, *cx),
Vector3::new(*ay, *by, *cy),
Vector3::new(*az, *bz, *cz),
)
}
pub fn from_quaternion(quat: Quaternion) -> Self {
RMat3::from_quat(quat.to_glam()).to_front()
}
pub fn from_euler(order: EulerOrder, angles: Vector3) -> Self {
let Vector3 { x: a, y: b, z: c } = angles;
let xmat =
Basis::from_rows_array(&[1.0, 0.0, 0.0, 0.0, a.cos(), -a.sin(), 0.0, a.sin(), a.cos()]);
let ymat =
Basis::from_rows_array(&[b.cos(), 0.0, b.sin(), 0.0, 1.0, 0.0, -b.sin(), 0.0, b.cos()]);
let zmat =
Basis::from_rows_array(&[c.cos(), -c.sin(), 0.0, c.sin(), c.cos(), 0.0, 0.0, 0.0, 1.0]);
match order {
EulerOrder::XYZ => xmat * ymat * zmat,
EulerOrder::XZY => xmat * zmat * ymat,
EulerOrder::YXZ => ymat * xmat * zmat,
EulerOrder::YZX => ymat * zmat * xmat,
EulerOrder::ZXY => zmat * xmat * ymat,
EulerOrder::ZYX => zmat * ymat * xmat,
}
}
pub fn to_cols(&self) -> [Vector3; 3] {
self.transposed().rows
}
const fn to_rows_array(self) -> [real; 9] {
let [
Vector3 {
x: ax,
y: bx,
z: cx,
},
Vector3 {
x: ay,
y: by,
z: cy,
},
Vector3 {
x: az,
y: bz,
z: cz,
},
] = self.rows;
[ax, bx, cx, ay, by, cy, az, bz, cz]
}
#[doc(alias = "get_rotation_quaternion")]
pub fn get_quaternion(&self) -> Quaternion {
RQuat::from_mat3(&self.orthonormalized().to_glam()).to_front()
}
#[must_use]
pub fn get_scale(&self) -> Vector3 {
let det = self.determinant();
let det_sign = if det < 0.0 { -1.0 } else { 1.0 };
Vector3::new(
self.col_a().length(),
self.col_b().length(),
self.col_c().length(),
) * det_sign
}
pub fn get_euler(&self) -> Vector3 {
self.get_euler_with(EulerOrder::YXZ)
}
pub fn get_euler_with(&self, order: EulerOrder) -> Vector3 {
use glam::swizzles::Vec3Swizzles as _;
let col_a = self.col_a().to_glam();
let col_b = self.col_b().to_glam();
let col_c = self.col_c().to_glam();
let row_a = self.rows[0].to_glam();
let row_b = self.rows[1].to_glam();
let row_c = self.rows[2].to_glam();
let major = match order {
EulerOrder::XYZ => self.rows[0].z,
EulerOrder::XZY => self.rows[0].y,
EulerOrder::YXZ => self.rows[1].z,
EulerOrder::YZX => self.rows[1].x,
EulerOrder::ZXY => self.rows[2].y,
EulerOrder::ZYX => self.rows[2].x,
};
if let Some(pure_rotation) = match order {
EulerOrder::XYZ => self
.to_euler_pure_rotation(major, 1, row_a.zx())
.map(RVec3::yxz),
EulerOrder::YXZ => {
self.to_euler_pure_rotation(major, 0, RVec2::new(-major, self.rows[1].y))
}
_ => None,
} {
return pure_rotation.to_front();
}
match order {
EulerOrder::XYZ => {
-Self::to_euler_inner(major, col_c.yz(), row_a.yx(), col_b.zy()).yxz()
}
EulerOrder::XZY => {
Self::to_euler_inner(major, col_b.zy(), row_a.zx(), col_c.yz()).yzx()
}
EulerOrder::YXZ => {
let mut vec = Self::to_euler_inner(major, col_c.xz(), row_b.xy(), row_a.yx());
if Self::is_between_neg1_1(major).is_lt() {
vec.y = -vec.y;
}
vec
}
EulerOrder::YZX => {
-Self::to_euler_inner(major, row_b.zy(), col_a.zx(), row_c.yz()).yzx()
}
EulerOrder::ZXY => {
-Self::to_euler_inner(major, row_c.xz(), col_b.xy(), row_a.zx()).xyz()
}
EulerOrder::ZYX => {
Self::to_euler_inner(major, col_a.yx(), row_c.yz(), col_b.xy()).zxy()
}
}
.to_front()
}
fn is_between_neg1_1(f: real) -> Ordering {
if f >= (1.0 - real::CMP_EPSILON) {
Ordering::Greater
} else if f <= -(1.0 - real::CMP_EPSILON) {
Ordering::Less
} else {
Ordering::Equal
}
}
fn is_identity_index(&self, index: usize) -> bool {
let row = self.rows[index];
let col = self.transposed().rows[index];
if row != col {
return false;
}
match index {
0 => row == Vector3::RIGHT,
1 => row == Vector3::UP,
2 => row == Vector3::BACK,
_ => panic!("Unknown Index {index}"),
}
}
#[allow(clippy::wrong_self_convention)]
fn to_euler_pure_rotation(
&self,
major: real,
index: usize,
rotation_vec: RVec2,
) -> Option<RVec3> {
if Self::is_between_neg1_1(major).is_ne() {
return None;
}
if !self.is_identity_index(index) {
return None;
}
Some(RVec3::new(
real::atan2(rotation_vec.x, rotation_vec.y),
0.0,
0.0,
))
}
#[allow(clippy::wrong_self_convention)]
fn to_euler_inner(major: real, pair0: RVec2, pair1: RVec2, pair2: RVec2) -> RVec3 {
match Self::is_between_neg1_1(major) {
Ordering::Less => RVec3::new(FRAC_PI_2, -real::atan2(pair2.x, pair2.y), 0.0),
Ordering::Equal => RVec3::new(
real::asin(-major),
real::atan2(pair0.x, pair0.y),
real::atan2(pair1.x, pair1.y),
),
Ordering::Greater => RVec3::new(-FRAC_PI_2, -real::atan2(pair2.x, pair2.y), 0.0),
}
}
pub fn determinant(&self) -> real {
self.to_glam().determinant()
}
#[must_use]
pub fn scaled(&self, scale: Vector3) -> Self {
Self::from_diagonal(scale.x, scale.y, scale.z) * (*self)
}
#[must_use]
pub fn inverse(&self) -> Basis {
self.glam(|mat| mat.inverse())
}
#[must_use]
pub fn transposed(&self) -> Self {
Self::from_cols(self.rows[0], self.rows[1], self.rows[2])
}
#[must_use]
pub fn orthonormalized(&self) -> Self {
assert!(
!self.determinant().is_zero_approx(),
"Determinant should not be zero."
);
let mut x = self.col_a();
let mut y = self.col_b();
let mut z = self.col_c();
x = x.normalized();
y = y - x * x.dot(y);
y = y.normalized();
z = z - x * x.dot(z) - y * y.dot(z);
z = z.normalized();
Self::from_cols(x, y, z)
}
#[must_use]
pub fn rotated(&self, axis: Vector3, angle: real) -> Self {
Self::from_axis_angle(axis, angle) * (*self)
}
#[must_use]
pub fn slerp(&self, other: &Self, weight: real) -> Self {
let from = self.get_quaternion();
let to = other.get_quaternion();
let mut result = Self::from_quaternion(from.slerp(to, weight));
for i in 0..3 {
result.rows[i] *= self.rows[i].length().lerp(other.rows[i].length(), weight);
}
result
}
#[must_use]
pub fn tdotx(&self, with: Vector3) -> real {
self.col_a().dot(with)
}
#[must_use]
pub fn tdoty(&self, with: Vector3) -> real {
self.col_b().dot(with)
}
#[must_use]
pub fn tdotz(&self, with: Vector3) -> real {
self.col_c().dot(with)
}
pub fn is_finite(&self) -> bool {
self.rows[0].is_finite() && self.rows[1].is_finite() && self.rows[2].is_finite()
}
#[doc(alias = "x")]
#[must_use]
pub fn col_a(&self) -> Vector3 {
Vector3::new(self.rows[0].x, self.rows[1].x, self.rows[2].x)
}
pub fn set_col_a(&mut self, col: Vector3) {
self.rows[0].x = col.x;
self.rows[1].x = col.y;
self.rows[2].x = col.z;
}
#[doc(alias = "y")]
#[must_use]
pub fn col_b(&self) -> Vector3 {
Vector3::new(self.rows[0].y, self.rows[1].y, self.rows[2].y)
}
pub fn set_col_b(&mut self, col: Vector3) {
self.rows[0].y = col.x;
self.rows[1].y = col.y;
self.rows[2].y = col.z;
}
#[doc(alias = "z")]
#[must_use]
pub fn col_c(&self) -> Vector3 {
Vector3::new(self.rows[0].z, self.rows[1].z, self.rows[2].z)
}
pub fn set_col_c(&mut self, col: Vector3) {
self.rows[0].z = col.x;
self.rows[1].z = col.y;
self.rows[2].z = col.z;
}
}
impl Display for Basis {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let [a, b, c] = self.to_cols();
write!(f, "[a: {a}, b: {b}, c: {c}]")
}
}
impl ApproxEq for Basis {
fn approx_eq(&self, other: &Self) -> bool {
Vector3::approx_eq(&self.rows[0], &other.rows[0])
&& Vector3::approx_eq(&self.rows[1], &other.rows[1])
&& Vector3::approx_eq(&self.rows[2], &other.rows[2])
}
}
impl GlamConv for Basis {
type Glam = RMat3;
}
impl GlamType for RMat3 {
type Mapped = Basis;
fn to_front(&self) -> Self::Mapped {
Basis::from_rows_array(&self.to_cols_array()).transposed()
}
fn from_front(mapped: &Self::Mapped) -> Self {
Self::from_cols_array(&mapped.to_rows_array()).transpose()
}
}
#[cfg(not(feature = "double-precision"))] #[cfg_attr(published_docs, doc(cfg(not(feature = "double-precision"))))]
impl GlamType for glam::Mat3A {
type Mapped = Basis;
fn to_front(&self) -> Self::Mapped {
Basis::from_rows_array(&self.to_cols_array()).transposed()
}
fn from_front(mapped: &Self::Mapped) -> Self {
Self::from_cols_array(&mapped.to_rows_array()).transpose()
}
}
impl Default for Basis {
fn default() -> Self {
Self::IDENTITY
}
}
impl Mul for Basis {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
self.glam2(&rhs, |a, b| a * b)
}
}
impl Mul<real> for Basis {
type Output = Self;
fn mul(mut self, rhs: real) -> Self::Output {
self *= rhs;
self
}
}
impl MulAssign<real> for Basis {
fn mul_assign(&mut self, rhs: real) {
self.rows[0] *= rhs;
self.rows[1] *= rhs;
self.rows[2] *= rhs;
}
}
impl Mul<Vector3> for Basis {
type Output = Vector3;
fn mul(self, rhs: Vector3) -> Self::Output {
self.glam2(&rhs, |a, b| a * b)
}
}
impl XformInv<Vector3> for Basis {
fn xform_inv(&self, rhs: Vector3) -> Vector3 {
Vector3::new(
self.col_a().dot(rhs),
self.col_b().dot(rhs),
self.col_c().dot(rhs),
)
}
}
unsafe impl GodotFfi for Basis {
const VARIANT_TYPE: ExtVariantType = ExtVariantType::Concrete(sys::VariantType::BASIS);
ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. }
}
crate::meta::impl_godot_as_self!(Basis: ByValue);
#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
mod test {
use super::*;
use crate::assert_eq_approx;
use crate::builtin::real_consts::{FRAC_PI_2, PI};
fn deg_to_rad(rotation: Vector3) -> Vector3 {
Vector3::new(
rotation.x.to_radians(),
rotation.y.to_radians(),
rotation.z.to_radians(),
)
}
fn test_rotation(deg_original_euler: Vector3, rot_order: EulerOrder) {
let original_euler: Vector3 = deg_to_rad(deg_original_euler);
let to_rotation: Basis = Basis::from_euler(rot_order, original_euler);
let euler_from_rotation: Vector3 = to_rotation.get_euler_with(rot_order);
let rotation_from_computed_euler: Basis = Basis::from_euler(rot_order, euler_from_rotation);
let res: Basis = to_rotation.inverse() * rotation_from_computed_euler;
assert!(
(res.col_a() - Vector3::RIGHT).length() <= 0.1,
"Fail due to X {} with {deg_original_euler} using {rot_order:?}",
res.col_a()
);
assert!(
(res.col_b() - Vector3::UP).length() <= 0.1,
"Fail due to Y {} with {deg_original_euler} using {rot_order:?}",
res.col_b()
);
assert!(
(res.col_c() - Vector3::BACK).length() <= 0.1,
"Fail due to Z {} with {deg_original_euler} using {rot_order:?}",
res.col_c()
);
let euler_xyz_from_rotation: Vector3 = to_rotation.get_euler_with(EulerOrder::XYZ);
let rotation_from_xyz_computed_euler: Basis =
Basis::from_euler(EulerOrder::XYZ, euler_xyz_from_rotation);
let res = to_rotation.inverse() * rotation_from_xyz_computed_euler;
let col_a = res.col_a();
let col_b = res.col_b();
let col_c = res.col_c();
assert!(
(col_a - Vector3::new(1.0, 0.0, 0.0)).length() <= 0.1,
"Double check with XYZ rot order failed, due to A {col_a} with {deg_original_euler} using {rot_order:?}",
);
assert!(
(col_b - Vector3::new(0.0, 1.0, 0.0)).length() <= 0.1,
"Double check with XYZ rot order failed, due to B {col_b} with {deg_original_euler} using {rot_order:?}",
);
assert!(
(col_c - Vector3::new(0.0, 0.0, 1.0)).length() <= 0.1,
"Double check with XYZ rot order failed, due to C {col_c} with {deg_original_euler} using {rot_order:?}",
);
}
#[test]
fn consts_behavior_correct() {
let v = Vector3::new(1.0, 2.0, 3.0);
assert_eq_approx!(Basis::IDENTITY * v, v);
assert_eq_approx!(Basis::FLIP_X * v, Vector3::new(-v.x, v.y, v.z),);
assert_eq_approx!(Basis::FLIP_Y * v, Vector3::new(v.x, -v.y, v.z),);
assert_eq_approx!(Basis::FLIP_Z * v, Vector3::new(v.x, v.y, -v.z),);
}
#[test]
fn basic_rotation_correct() {
assert_eq_approx!(
Basis::from_axis_angle(Vector3::FORWARD, 0.0) * Vector3::RIGHT,
Vector3::RIGHT,
);
assert_eq_approx!(
Basis::from_axis_angle(Vector3::FORWARD, FRAC_PI_2) * Vector3::RIGHT,
Vector3::DOWN,
);
assert_eq_approx!(
Basis::from_axis_angle(Vector3::FORWARD, PI) * Vector3::RIGHT,
Vector3::LEFT,
);
assert_eq_approx!(
Basis::from_axis_angle(Vector3::FORWARD, PI + FRAC_PI_2) * Vector3::RIGHT,
Vector3::UP,
);
}
#[test]
fn basis_euler_conversions() {
let euler_order_to_test: Vec<EulerOrder> = vec![
EulerOrder::XYZ,
EulerOrder::XZY,
EulerOrder::YZX,
EulerOrder::YXZ,
EulerOrder::ZXY,
EulerOrder::ZYX,
];
let vectors_to_test: Vec<Vector3> = vec![
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(0.5, 0.5, 0.5),
Vector3::new(-0.5, -0.5, -0.5),
Vector3::new(40.0, 40.0, 40.0),
Vector3::new(-40.0, -40.0, -40.0),
Vector3::new(0.0, 0.0, -90.0),
Vector3::new(0.0, -90.0, 0.0),
Vector3::new(-90.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 90.0),
Vector3::new(0.0, 90.0, 0.0),
Vector3::new(90.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, -30.0),
Vector3::new(0.0, -30.0, 0.0),
Vector3::new(-30.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 30.0),
Vector3::new(0.0, 30.0, 0.0),
Vector3::new(30.0, 0.0, 0.0),
Vector3::new(0.5, 50.0, 20.0),
Vector3::new(-0.5, -50.0, -20.0),
Vector3::new(0.5, 0.0, 90.0),
Vector3::new(0.5, 0.0, -90.0),
Vector3::new(360.0, 360.0, 360.0),
Vector3::new(-360.0, -360.0, -360.0),
Vector3::new(-90.0, 60.0, -90.0),
Vector3::new(90.0, 60.0, -90.0),
Vector3::new(90.0, -60.0, -90.0),
Vector3::new(-90.0, -60.0, -90.0),
Vector3::new(-90.0, 60.0, 90.0),
Vector3::new(90.0, 60.0, 90.0),
Vector3::new(90.0, -60.0, 90.0),
Vector3::new(-90.0, -60.0, 90.0),
Vector3::new(60.0, 90.0, -40.0),
Vector3::new(60.0, -90.0, -40.0),
Vector3::new(-60.0, -90.0, -40.0),
Vector3::new(-60.0, 90.0, 40.0),
Vector3::new(60.0, 90.0, 40.0),
Vector3::new(60.0, -90.0, 40.0),
Vector3::new(-60.0, -90.0, 40.0),
Vector3::new(-90.0, 90.0, -90.0),
Vector3::new(90.0, 90.0, -90.0),
Vector3::new(90.0, -90.0, -90.0),
Vector3::new(-90.0, -90.0, -90.0),
Vector3::new(-90.0, 90.0, 90.0),
Vector3::new(90.0, 90.0, 90.0),
Vector3::new(90.0, -90.0, 90.0),
Vector3::new(20.0, 150.0, 30.0),
Vector3::new(20.0, -150.0, 30.0),
Vector3::new(-120.0, -150.0, 30.0),
Vector3::new(-120.0, -150.0, -130.0),
Vector3::new(120.0, -150.0, -130.0),
Vector3::new(120.0, 150.0, -130.0),
Vector3::new(120.0, 150.0, 130.0),
];
for order in euler_order_to_test.iter() {
for vector in vectors_to_test.iter() {
test_rotation(*vector, *order);
}
}
}
#[test]
fn basis_finite_number_test() {
let x: Vector3 = Vector3::new(0.0, 1.0, 2.0);
let infinite: Vector3 = Vector3::new(real::NAN, real::NAN, real::NAN);
assert!(
Basis::from_cols(x, x, x).is_finite(),
"Basis with all components finite should be finite"
);
assert!(
!Basis::from_cols(infinite, x, x).is_finite(),
"Basis with one component infinite should not be finite."
);
assert!(
!Basis::from_cols(x, infinite, x).is_finite(),
"Basis with one component infinite should not be finite."
);
assert!(
!Basis::from_cols(x, x, infinite).is_finite(),
"Basis with one component infinite should not be finite."
);
assert!(
!Basis::from_cols(infinite, infinite, x).is_finite(),
"Basis with two components infinite should not be finite."
);
assert!(
!Basis::from_cols(infinite, x, infinite).is_finite(),
"Basis with two components infinite should not be finite."
);
assert!(
!Basis::from_cols(x, infinite, infinite).is_finite(),
"Basis with two components infinite should not be finite."
);
assert!(
!Basis::from_cols(infinite, infinite, infinite).is_finite(),
"Basis with three components infinite should not be finite."
);
}
#[cfg(feature = "serde")] #[cfg_attr(published_docs, doc(cfg(feature = "serde")))]
#[test]
fn serde_roundtrip() {
let basis = Basis::IDENTITY;
let expected_json = "{\"rows\":[{\"x\":1.0,\"y\":0.0,\"z\":0.0},{\"x\":0.0,\"y\":1.0,\"z\":0.0},{\"x\":0.0,\"y\":0.0,\"z\":1.0}]}";
crate::builtin::test_utils::roundtrip(&basis, expected_json);
}
}