Skip to content

Commit bcf3f40

Browse files
committed
Implement Godot methods for Quat
There are still some tests lacking & `get_euler` has a large error for some reason.
1 parent f88f4e5 commit bcf3f40

File tree

3 files changed

+235
-6
lines changed

3 files changed

+235
-6
lines changed

gdnative-core/src/core_types/geom/basis.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,7 @@ mod tests {
709709
fn to_quat() {
710710
let (b, _bn) = test_inputs();
711711

712-
assert!(Quat::new(-0.167156, 0.677813, -0.043058, 0.714685).is_equal_approx(&b.to_quat()));
712+
assert!(Quat::new(-0.167156, 0.677813, -0.043058, 0.714685).is_equal_approx(b.to_quat()));
713713
}
714714

715715
#[test]

gdnative-core/src/core_types/quat.rs

Lines changed: 232 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use super::IsEqualApprox;
1+
use super::{Basis, IsEqualApprox, Vector3, CMP_EPSILON};
2+
use core::f32::consts::FRAC_PI_2;
3+
use core::ops::Mul;
4+
use glam::Mat3;
25

36
#[derive(Copy, Clone, Debug, PartialEq)]
47
#[repr(C)]
@@ -9,23 +12,249 @@ pub struct Quat {
912
pub w: f32,
1013
}
1114

15+
/// Helper methods for `Quat`.
16+
///
17+
/// See the official [`Godot documentation`](https://docs.godotengine.org/en/stable/classes/class_quat.html).
1218
impl Quat {
19+
pub const IDENTITY: Self = Self::new(0.0, 0.0, 0.0, 1.0);
20+
21+
/// Constructs a quaternion defined by the given values.
1322
#[inline]
14-
pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
23+
pub const fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
1524
Self { x, y, z, w }
1625
}
1726

27+
/// Constructs a quaternion from the given [Basis]
28+
#[inline]
29+
pub fn from_basis(basis: &Basis) -> Self {
30+
basis.to_quat()
31+
}
32+
33+
/// Constructs a quaternion that will perform a rotation specified by Euler angles (in the YXZ
34+
/// convention: when decomposing, first Z, then X, and Y last), given in the vector format as
35+
/// (X angle, Y angle, Z angle).
36+
#[inline]
37+
pub fn from_euler(euler: Vector3) -> Self {
38+
Self::gd(glam::Quat::from_rotation_ypr(euler.y, euler.x, euler.z))
39+
}
40+
41+
/// Constructs a quaternion that will rotate around the given axis by the specified angle. The
42+
/// axis must be a normalized vector.
43+
#[inline]
44+
pub fn from_axis_angle(axis: Vector3, angle: f32) -> Self {
45+
debug_assert!(axis.is_normalized(), "Axis is not normalized");
46+
Self::gd(glam::Quat::from_axis_angle(axis.glam().into(), angle))
47+
}
48+
49+
/// Performs a cubic spherical interpolation between quaternions `pre_a`, this quaternion, `b`,
50+
/// and `post_b`, by the given amount `t`.
51+
#[inline]
52+
pub fn cubic_slerp(self, b: Self, pre_a: Self, post_b: Self, t: f32) -> Self {
53+
let t2 = (1.0 - t) * t * 2.0;
54+
let sp = self.slerp(b, t);
55+
let sq = pre_a.slerpni(post_b, t);
56+
sp.slerpni(sq, t2)
57+
}
58+
59+
/// Returns the dot product of two quaternions.
60+
#[inline]
61+
pub fn dot(self, b: Self) -> f32 {
62+
self.glam().dot(b.glam())
63+
}
64+
65+
/// Returns Euler angles (in the YXZ convention: when decomposing, first Z, then X, and Y last)
66+
/// corresponding to the rotation represented by the unit quaternion. Returned vector contains
67+
/// the rotation angles in the format (X angle, Y angle, Z angle).
68+
#[inline]
69+
pub fn get_euler(self) -> Vector3 {
70+
let basis = Mat3::from_quat(self.glam());
71+
let elm = basis.to_cols_array_2d();
72+
// Copied from Basis::get_euler_yxz
73+
let m12 = elm[1][2];
74+
if m12 < 1.0 - CMP_EPSILON as f32 {
75+
if m12 > CMP_EPSILON as f32 - 1.0 {
76+
#[allow(clippy::float_cmp)] // Godot also used direct comparison
77+
if elm[1][0] == 0.0
78+
&& elm[0][1] == 0.0
79+
&& elm[0][2] == 0.0
80+
&& elm[2][0] == 0.0
81+
&& elm[0][0] == 1.0
82+
{
83+
Vector3::new((-m12).atan2(elm[1][1]), 0.0, 0.0)
84+
} else {
85+
Vector3::new(
86+
(-m12).asin(),
87+
elm[0][2].atan2(elm[2][2]),
88+
elm[1][0].atan2(elm[1][1]),
89+
)
90+
}
91+
} else {
92+
Vector3::new(FRAC_PI_2, elm[0][1].atan2(elm[0][0]), 0.0)
93+
}
94+
} else {
95+
Vector3::new(-FRAC_PI_2, (-elm[0][1]).atan2(elm[0][0]), 0.0)
96+
}
97+
}
98+
99+
/// Returns the inverse of the quaternion.
100+
#[inline]
101+
pub fn inverse(self) -> Self {
102+
Self::gd(self.glam().inverse())
103+
}
104+
105+
/// Returns `true` if this quaternion and `quat` are approximately equal, by running
106+
/// `is_equal_approx` on each component
18107
#[inline]
19-
pub fn is_equal_approx(self, to: &Self) -> bool {
108+
pub fn is_equal_approx(self, to: Self) -> bool {
20109
self.x.is_equal_approx(to.x)
21110
&& self.y.is_equal_approx(to.y)
22111
&& self.z.is_equal_approx(to.z)
23112
&& self.w.is_equal_approx(to.w)
24113
}
25114

115+
/// Returns whether the quaternion is normalized or not.
116+
#[inline]
117+
pub fn is_normalized(self) -> bool {
118+
self.glam().is_normalized()
119+
}
120+
121+
/// Returns the length of the quaternion.
122+
#[inline]
123+
pub fn length(self) -> f32 {
124+
self.glam().length()
125+
}
126+
127+
/// Returns the length of the quaternion, squared.
128+
#[inline]
129+
pub fn length_squared(self) -> f32 {
130+
self.glam().length_squared()
131+
}
132+
133+
/// Returns a copy of the quaternion, normalized to unit length.
134+
#[inline]
135+
pub fn normalized(self) -> Self {
136+
Self::gd(self.glam().normalize())
137+
}
138+
139+
/// Sets the quaternion to a rotation which rotates around axis by the specified angle, in
140+
/// radians. The axis must be a normalized vector.
141+
#[inline]
142+
pub fn set_axis_angle(&mut self, axis: Vector3, angle: f32) {
143+
*self = Self::from_axis_angle(axis, angle)
144+
}
145+
146+
/// Sets the quaternion to a rotation specified by Euler angles (in the YXZ convention: when
147+
/// decomposing, first Z, then X, and Y last), given in the vector format as (X angle, Y angle,
148+
/// Z angle).
149+
#[inline]
150+
pub fn set_euler(&mut self, euler: Vector3) {
151+
*self = Self::from_euler(euler);
152+
}
153+
154+
/// Returns the result of the spherical linear interpolation between this quaternion and to by
155+
/// amount weight.
156+
///
157+
/// **Note:** Both quaternions must be normalized.
158+
#[inline]
159+
pub fn slerp(self, b: Self, t: f32) -> Self {
160+
debug_assert!(self.is_normalized(), "Quaternion `self` is not normalized");
161+
debug_assert!(b.is_normalized(), "Quaternion `b` is not normalized");
162+
Self::gd(self.glam().slerp(b.glam(), t))
163+
}
164+
165+
/// Returns the result of the spherical linear interpolation between this quaternion and `t` by
166+
/// amount `t`, but without checking if the rotation path is not bigger than 90 degrees.
167+
#[inline]
168+
pub fn slerpni(self, b: Self, t: f32) -> Self {
169+
debug_assert!(self.is_normalized(), "Quaternion `self` is not normalized");
170+
debug_assert!(b.is_normalized(), "Quaternion `b` is not normalized");
171+
let dot = self.dot(b);
172+
if dot.abs() > 0.9999 {
173+
self
174+
} else {
175+
let theta = dot.acos();
176+
let sin_t = 1.0 / theta.sin();
177+
let new_f = (t * theta).sin() * sin_t;
178+
let inv_f = ((1.0 - t) * theta).sin() * sin_t;
179+
Self::new(
180+
inv_f * self.x + new_f * b.x,
181+
inv_f * self.y + new_f * b.y,
182+
inv_f * self.z + new_f * b.z,
183+
inv_f * self.w + new_f * b.w,
184+
)
185+
}
186+
}
187+
188+
/// Returns a vector transformed (multiplied) by this quaternion.
189+
#[inline]
190+
pub fn xform(self, v: Vector3) -> Vector3 {
191+
self * v
192+
}
193+
194+
#[inline]
195+
fn gd(quat: glam::Quat) -> Self {
196+
Self::new(quat.x, quat.y, quat.z, quat.w)
197+
}
198+
26199
#[inline]
27200
#[allow(unused)]
28201
fn glam(self) -> glam::Quat {
29202
glam::Quat::from_xyzw(self.x, self.y, self.z, self.w)
30203
}
31204
}
205+
206+
impl Mul<Vector3> for Quat {
207+
type Output = Vector3;
208+
209+
#[inline]
210+
fn mul(self, with: Vector3) -> Vector3 {
211+
debug_assert!(self.is_normalized(), "Quaternion is not normalized");
212+
Vector3::gd(self.glam() * with.glam())
213+
}
214+
}
215+
216+
#[cfg(test)]
217+
mod test {
218+
use super::*;
219+
220+
#[test]
221+
fn from_euler() {
222+
let euler = Vector3::new(0.25, 5.24, 3.0);
223+
let expect = Quat::new(0.485489, 0.142796, -0.862501, 0.001113);
224+
assert!(Quat::from_euler(euler).is_equal_approx(expect));
225+
}
226+
227+
#[test]
228+
fn to_basis() {
229+
let quat = Quat::new(0.485489, 0.142796, -0.862501, 0.001113);
230+
let expect = Basis::from_elements([
231+
Vector3::new(-0.528598, 0.140572, -0.837152),
232+
Vector3::new(0.136732, -0.959216, -0.247404),
233+
Vector3::new(-0.837788, -0.245243, 0.487819),
234+
]);
235+
let basis = Mat3::from_quat(quat.glam()).to_cols_array_2d();
236+
let basis = [
237+
Vector3::new(basis[0][0], basis[1][0], basis[2][0]),
238+
Vector3::new(basis[0][1], basis[1][1], basis[2][1]),
239+
Vector3::new(basis[0][2], basis[1][2], basis[2][2]),
240+
];
241+
let basis = Basis::from_elements(basis);
242+
assert!(basis.is_equal_approx(&expect));
243+
}
244+
245+
#[test]
246+
fn get_euler() {
247+
let quat = Quat::new(0.485489, 0.142796, -0.862501, 0.001113);
248+
let expect = Vector3::new(0.25, -1.043185, 3.00001);
249+
dbg!(quat.get_euler());
250+
assert!(quat.get_euler().is_equal_approx(expect));
251+
}
252+
253+
#[test]
254+
fn mul() {
255+
let quat = Quat::new(0.485489, 0.142796, -0.862501, 0.001113);
256+
let vec = Vector3::new(2.2, 0.8, 1.65);
257+
let expect = Vector3::new(-2.43176, -0.874777, -1.234427);
258+
assert!(expect.is_equal_approx(quat * vec));
259+
}
260+
}

gdnative-core/src/core_types/vector3.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -380,12 +380,12 @@ impl Vector3 {
380380
}
381381

382382
#[inline]
383-
fn glam(self) -> Vec3A {
383+
pub(super) fn glam(self) -> Vec3A {
384384
Vec3A::new(self.x, self.y, self.z)
385385
}
386386

387387
#[inline]
388-
fn gd(from: Vec3A) -> Self {
388+
pub(super) fn gd(from: Vec3A) -> Self {
389389
Self::new(from.x, from.y, from.z)
390390
}
391391
}

0 commit comments

Comments
 (0)