From 3bd19dabc8336c8787f4402780d86c072339b2dd Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Thu, 16 Feb 2023 16:15:56 +0100 Subject: [PATCH 01/15] Initial impl of matrix-types --- godot-core/src/builtin/basis.rs | 368 +++++++++++++++++++ godot-core/src/builtin/euler_order.rs | 55 +++ godot-core/src/builtin/mod.rs | 10 + godot-core/src/builtin/others.rs | 4 - godot-core/src/builtin/projection.rs | 316 +++++++++++++++++ godot-core/src/builtin/quaternion.rs | 7 +- godot-core/src/builtin/transform2d.rs | 454 ++++++++++++++++++++++++ godot-core/src/builtin/transform3d.rs | 184 ++++++++++ godot-core/src/builtin/variant/impls.rs | 8 +- 9 files changed, 1395 insertions(+), 11 deletions(-) create mode 100644 godot-core/src/builtin/basis.rs create mode 100644 godot-core/src/builtin/euler_order.rs create mode 100644 godot-core/src/builtin/projection.rs create mode 100644 godot-core/src/builtin/transform2d.rs create mode 100644 godot-core/src/builtin/transform3d.rs diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs new file mode 100644 index 000000000..a6e300937 --- /dev/null +++ b/godot-core/src/builtin/basis.rs @@ -0,0 +1,368 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +use std::ops::*; + +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use super::{ + glam_helpers::{GlamConv, GlamType}, + math::*, + EulerOrder, Quaternion, Vector3, +}; + +use glam::f32::Mat3; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[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); + + #[must_use] + pub const fn new(rows: [Vector3; 3]) -> Self { + Self { rows } + } + + #[must_use] + pub const fn from_diagonal(x: f32, y: f32, z: f32) -> Self { + Self { + rows: [ + Vector3::new(x, 0.0, 0.0), + Vector3::new(0.0, y, 0.0), + Vector3::new(0.0, 0.0, z), + ], + } + } + + #[must_use] + pub const fn from_rows(x: Vector3, y: Vector3, z: Vector3) -> Self { + Self { rows: [x, y, z] } + } + + #[must_use] + pub const fn from_cols(x: Vector3, y: Vector3, z: Vector3) -> Self { + Self::from_rows(x, y, z).transpose() + } + + #[must_use] + pub const fn from_rows_array(rows: &[f32; 9]) -> Self { + let [x0, y0, z0, x1, y1, z1, x2, y2, z2] = rows; + Self { + rows: [ + Vector3::new(*x0, *y0, *z0), + Vector3::new(*x1, *y1, *z1), + Vector3::new(*x2, *y2, *z2), + ], + } + } + + #[must_use] + pub const fn from_rows_array_2d(rows: &[[f32; 3]; 3]) -> Self { + let [[x0, y0, z0], [x1, y1, z1], [x2, y2, z2]] = rows; + Self { + rows: [ + Vector3::new(*x0, *y0, *z0), + Vector3::new(*x1, *y1, *z1), + Vector3::new(*x2, *y2, *z2), + ], + } + } + + #[must_use] + pub const fn to_rows_array(&self) -> [f32; 9] { + let [Vector3 { + x: x0, + y: y0, + z: z0, + }, Vector3 { + x: x1, + y: y1, + z: z1, + }, Vector3 { + x: x2, + y: y2, + z: z2, + }] = self.rows; + [x0, y0, z0, x1, y1, z1, x2, y2, z2] + } + + #[must_use] + pub const fn to_rows_array_2d(&self) -> [[f32; 3]; 3] { + let [Vector3 { + x: x0, + y: y0, + z: z0, + }, Vector3 { + x: x1, + y: y1, + z: z1, + }, Vector3 { + x: x2, + y: y2, + z: z2, + }] = self.rows; + [[x0, y0, z0], [x1, y1, z1], [x2, y2, z2]] + } + + #[must_use] + pub fn to_quat(&self) -> Quaternion { + glam::Quat::from_mat3(&self.to_glam()).to_front() + } + + #[must_use] + pub fn from_axis_angle(axis: Vector3, angle: f32) -> Self { + Mat3::from_axis_angle(axis.to_glam(), angle).to_front() + } + + #[must_use] + pub fn from_quat(quat: Quaternion) -> Self { + Mat3::from_quat(quat.to_glam()).to_front() + } + + #[must_use] + pub fn from_euler(order: EulerOrder, a: f32, b: f32, c: f32) -> Self { + Mat3::from_euler(order.to_glam(), a, b, c).to_front() + } + + #[must_use] + pub fn from_scale(x: f32, y: f32, z: f32) -> Self { + Self::from_diagonal(x, y, z) + } + + #[must_use] + pub fn looking_at(target: Vector3, up: Vector3) -> Self { + super::inner::InnerBasis::looking_at(target, up) + } + + #[must_use] + pub fn determinant(&self) -> f32 { + self.to_glam().determinant() + } + + #[must_use] + pub fn euler(&self, order: EulerOrder) -> Vector3 { + self.to_quat().get_euler(order) + } + + #[must_use] + pub fn scale(&self) -> Vector3 { + let det = self.determinant(); + let det_sign = if det < 0.0 { -1.0 } else { 1.0 }; + + Vector3::new( + self.col_x().length(), + self.col_y().length(), + self.col_z().length(), + ) * det_sign + } + + pub fn scaled(&self, scale: Vector3) -> Self { + *self * Self::from_scale(scale.x, scale.y, scale.z) + } + + #[must_use] + pub fn inverse(&self) -> Basis { + self.glam(|mat| mat.inverse()) + } + + #[must_use] + pub fn is_finite(&self) -> bool { + self.to_glam().is_finite() + } + + #[must_use] + pub fn orthonormalize(&self) -> Self { + assert!( + is_equal_approx(self.determinant(), 0.0), + "Determinant should not be zero." + ); + + // Gram-Schmidt Process + let mut x = self.col_x(); + let mut y = self.col_y(); + let mut z = self.col_z(); + + 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 rotate(self, axis: Vector3, angle: f32) -> Self { + self * Self::from_axis_angle(axis, angle) + } + + #[must_use] + pub fn slerp(self, other: Self, weight: f32) -> Self { + let from = self.to_quat(); + let to = other.to_quat(); + + let mut result = Self::from_quat(from.slerp(to, weight)); + + for i in 0..3 { + result.rows[i] *= lerp(self.rows[i].length(), other.rows[i].length(), weight); + } + + result + } + + #[must_use] + pub fn tdotx(&self, with: Vector3) -> f32 { + self.col_x().dot(with) + } + + #[must_use] + pub fn tdoty(&self, with: Vector3) -> f32 { + self.col_y().dot(with) + } + + #[must_use] + pub fn tdotz(&self, with: Vector3) -> f32 { + self.col_z().dot(with) + } + + #[must_use] + pub const fn transpose(self) -> Self { + Self { + rows: [ + Vector3::new(self.rows[0].x, self.rows[1].x, self.rows[2].x), + Vector3::new(self.rows[0].y, self.rows[1].y, self.rows[2].y), + Vector3::new(self.rows[0].z, self.rows[1].z, self.rows[2].z), + ], + } + } + + #[must_use] + pub fn col_x(&self) -> Vector3 { + Vector3::new(self[0].x, self[1].x, self[2].x) + } + + pub fn col_x_mut(&mut self) -> [&mut f32; 3] { + let [x, y, z] = self.rows.as_mut_slice() else { unreachable!() }; + [&mut x.x, &mut y.x, &mut z.x] + } + + #[must_use] + pub fn col_y(&self) -> Vector3 { + Vector3::new(self[0].y, self[1].y, self[2].y) + } + + pub fn col_y_mut(&mut self) -> [&mut f32; 3] { + let [x, y, z] = self.rows.as_mut_slice() else { unreachable!() }; + [&mut x.y, &mut y.y, &mut z.y] + } + + #[must_use] + pub fn col_z(&self) -> Vector3 { + Vector3::new(self[0].z, self[1].z, self[2].z) + } + + pub fn col_z_mut(&mut self) -> [&mut f32; 3] { + let [x, y, z] = self.rows.as_mut_slice() else { unreachable!() }; + [&mut x.z, &mut y.z, &mut z.z] + } + + pub fn is_equal_approx(&self, other: &Self) -> bool { + self[0].is_equal_approx(other[0]) + && self[1].is_equal_approx(other[1]) + && self[2].is_equal_approx(other[2]) + } +} + +impl GlamConv for Basis { + type Glam = glam::f32::Mat3; +} + +impl GlamType for glam::f32::Mat3 { + type Mapped = Basis; + + fn to_front(&self) -> Self::Mapped { + Basis::from_rows_array_2d(&self.to_cols_array_2d()).transpose() + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self::from_cols_array(&mapped.to_rows_array()).transpose() + } +} + +impl GlamType for glam::f32::Mat3A { + type Mapped = Basis; + + fn to_front(&self) -> Self::Mapped { + Basis::from_rows_array_2d(&self.to_cols_array_2d()).transpose() + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self::from_cols_array(&mapped.to_rows_array()).transpose() + } +} + +impl Index for Basis { + type Output = Vector3; + + fn index(&self, index: usize) -> &Self::Output { + &self.rows[index] + } +} + +impl IndexMut for Basis { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.rows[index] + } +} + +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 for Basis { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + (self.to_glam() * rhs).to_front() + } +} + +impl MulAssign for Basis { + fn mul_assign(&mut self, rhs: f32) { + self[0] *= rhs; + self[1] *= rhs; + self[2] *= rhs; + } +} + +impl Mul for Basis { + type Output = Vector3; + + fn mul(self, rhs: Vector3) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl GodotFfi for Basis { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} diff --git a/godot-core/src/builtin/euler_order.rs b/godot-core/src/builtin/euler_order.rs new file mode 100644 index 000000000..042ff31d0 --- /dev/null +++ b/godot-core/src/builtin/euler_order.rs @@ -0,0 +1,55 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +use glam::EulerRot; +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use super::glam_helpers::{GlamConv, GlamType}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub enum EulerOrder { + XYZ = 0, + XZY = 1, + YXZ = 2, + YZX = 3, + ZXY = 4, + ZYX = 5, +} + +impl GlamConv for EulerOrder { + type Glam = EulerRot; +} + +impl GlamType for EulerRot { + type Mapped = EulerOrder; + + fn to_front(&self) -> Self::Mapped { + match self { + EulerRot::XYZ => EulerOrder::XYZ, + EulerRot::XZY => EulerOrder::XZY, + EulerRot::YXZ => EulerOrder::YXZ, + EulerRot::YZX => EulerOrder::YZX, + EulerRot::ZXY => EulerOrder::ZXY, + EulerRot::ZYX => EulerOrder::ZYX, + } + } + + fn from_front(mapped: &Self::Mapped) -> Self { + match mapped { + EulerOrder::XYZ => EulerRot::XYZ, + EulerOrder::XZY => EulerRot::XZY, + EulerOrder::YXZ => EulerRot::YXZ, + EulerOrder::YZX => EulerRot::YZX, + EulerOrder::ZXY => EulerRot::ZXY, + EulerOrder::ZYX => EulerRot::ZYX, + } + } +} + +impl GodotFfi for EulerOrder { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 95cb8514b..96fe16a7f 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -36,15 +36,20 @@ pub use crate::{array, dict, varray}; pub use array_inner::{Array, TypedArray}; +pub use basis::*; pub use color::*; pub use dictionary_inner::Dictionary; +pub use euler_order::*; pub use math::*; pub use node_path::*; pub use others::*; pub use packed_array::*; +pub use projection::*; pub use quaternion::*; pub use string::*; pub use string_name::*; +pub use transform2d::*; +pub use transform3d::*; pub use variant::*; pub use vector2::*; pub use vector2i::*; @@ -55,6 +60,7 @@ pub use vector4i::*; /// Meta-information about variant types, properties and class names. pub mod meta; +mod projection; /// Specialized types related to arrays. pub mod array { @@ -79,7 +85,9 @@ mod array_inner; #[path = "dictionary.rs"] mod dictionary_inner; +mod basis; mod color; +mod euler_order; mod glam_helpers; mod math; mod node_path; @@ -88,6 +96,8 @@ mod packed_array; mod quaternion; mod string; mod string_name; +mod transform2d; +mod transform3d; mod variant; mod vector2; mod vector2i; diff --git a/godot-core/src/builtin/others.rs b/godot-core/src/builtin/others.rs index 99f40a32f..5ed6e88c2 100644 --- a/godot-core/src/builtin/others.rs +++ b/godot-core/src/builtin/others.rs @@ -17,10 +17,6 @@ impl_builtin_stub!(Rect2, OpaqueRect2); impl_builtin_stub!(Rect2i, OpaqueRect2i); impl_builtin_stub!(Plane, OpaquePlane); impl_builtin_stub!(Aabb, OpaqueAabb); -impl_builtin_stub!(Basis, OpaqueBasis); -impl_builtin_stub!(Transform2D, OpaqueTransform2D); -impl_builtin_stub!(Transform3D, OpaqueTransform3D); -impl_builtin_stub!(Projection, OpaqueProjection); impl_builtin_stub!(Rid, OpaqueRid); impl_builtin_stub!(Callable, OpaqueCallable); impl_builtin_stub!(Signal, OpaqueSignal); diff --git a/godot-core/src/builtin/projection.rs b/godot-core/src/builtin/projection.rs new file mode 100644 index 000000000..b659d4e62 --- /dev/null +++ b/godot-core/src/builtin/projection.rs @@ -0,0 +1,316 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +use std::ops::*; + +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use super::{ + glam_helpers::{GlamConv, GlamType}, + inner::InnerProjection, + Transform3D, Vector2, Vector4, +}; + +use glam::f32::Mat4; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(C)] +pub struct Projection { + pub cols: [Vector4; 4], +} + +impl Projection { + pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0, 1.0, 1.0); + pub const ZERO: Self = Self::from_diagonal(0.0, 0.0, 0.0, 0.0); + + #[must_use] + pub const fn new(cols: [Vector4; 4]) -> Self { + Self { cols } + } + + #[must_use] + pub const fn from_diagonal(x: f32, y: f32, z: f32, w: f32) -> Self { + Self { + cols: [ + Vector4::new(x, 0.0, 0.0, 0.0), + Vector4::new(0.0, y, 0.0, 0.0), + Vector4::new(0.0, 0.0, z, 0.0), + Vector4::new(0.0, 0.0, 0.0, w), + ], + } + } + + #[must_use] + pub const fn from_cols(x: Vector4, y: Vector4, z: Vector4, w: Vector4) -> Self { + Self { cols: [x, y, z, w] } + } + + pub fn create_depth_correction(flip_y: bool) -> Self { + InnerProjection::create_depth_correction(flip_y) + } + + #[allow(clippy::too_many_arguments)] + pub fn create_for_hmd( + eye: Eye, + aspect: f64, + intraocular_dist: f64, + display_width: f64, + display_to_lens: f64, + oversample: f64, + near: f64, + far: f64, + ) -> Self { + InnerProjection::create_for_hmd( + eye as i64, + aspect, + intraocular_dist, + display_width, + display_to_lens, + oversample, + near, + far, + ) + } + + pub fn create_frustum( + left: f64, + right: f64, + bottom: f64, + top: f64, + near: f64, + far: f64, + ) -> Self { + InnerProjection::create_frustum(left, right, bottom, top, near, far) + } + + pub fn create_frustum_aspect( + size: f64, + aspect: f64, + offset: Vector2, + near: f64, + far: f64, + flip_fov: bool, + ) -> Self { + InnerProjection::create_frustum_aspect(size, aspect, offset, near, far, flip_fov) + } + + pub fn create_orthogonal( + left: f64, + right: f64, + bottom: f64, + top: f64, + near: f64, + far: f64, + ) -> Self { + InnerProjection::create_orthogonal(left, right, bottom, top, near, far) + } + + pub fn create_orthogonal_aspect( + size: f64, + aspect: f64, + near: f64, + far: f64, + flip_fov: bool, + ) -> Self { + InnerProjection::create_orthogonal_aspect(size, aspect, near, far, flip_fov) + } + + pub fn create_perspective( + fov_y: f64, + aspect: f64, + near: f64, + far: f64, + flip_fov: bool, + ) -> Self { + InnerProjection::create_perspective(fov_y, aspect, near, far, flip_fov) + } + + #[allow(clippy::too_many_arguments)] + pub fn create_perspective_hmd( + fov_y: f64, + aspect: f64, + near: f64, + far: f64, + flip_fov: bool, + eye: Eye, + intraocular_dist: f64, + convergence_dist: f64, + ) -> Self { + InnerProjection::create_perspective_hmd( + fov_y, + aspect, + near, + far, + flip_fov, + eye as i64, + intraocular_dist, + convergence_dist, + ) + } + + pub fn determinant(&self) -> f32 { + self.glam(|mat| mat.determinant()) + } + + pub fn flipped_y(&self) -> Self { + Self { + cols: [self[0], -self[1], self[2], self[3]], + } + } + + pub fn aspect(&self) -> f64 { + self.as_inner().get_aspect() + } + + pub fn far_plane_half_extents(&self) -> Vector2 { + self.as_inner().get_far_plane_half_extents() + } + + pub fn fov(&self) -> f64 { + self.as_inner().get_fov() + } + + pub fn fov_y_of(fov_x: f64, aspect: f64) -> f64 { + InnerProjection::get_fovy(fov_x, aspect) + } + + pub fn lod_multiplier(&self) -> f64 { + self.as_inner().get_lod_multiplier() + } + + pub fn pixels_per_meter(&self, pixel_width: i64) -> i64 { + self.as_inner().get_pixels_per_meter(pixel_width) + } + + pub fn viewport_half_extents(&self) -> Vector2 { + self.as_inner().get_viewport_half_extents() + } + + pub fn z_far(&self) -> f64 { + self.as_inner().get_z_far() + } + + pub fn z_near(&self) -> f64 { + self.as_inner().get_z_near() + } + + pub fn inverted(&self) -> Self { + self.glam(|mat| mat.inverse()) + } + + pub fn is_orthogonal(&self) -> bool { + self.as_inner().is_orthogonal() + } + + pub fn jitter_offseted(&self, offset: Vector2) -> Self { + self.as_inner().jitter_offseted(offset) + } + + pub fn perspective_znear_adjusted(&self, new_znear: f64) -> Self { + self.as_inner().perspective_znear_adjusted(new_znear) + } + + #[doc(hidden)] + pub(crate) fn as_inner(&self) -> InnerProjection { + InnerProjection::from_outer(self) + } +} + +impl From for Projection { + fn from(trans: Transform3D) -> Self { + trans.glam(Mat4::from) + } +} + +impl Default for Projection { + fn default() -> Self { + Self::IDENTITY + } +} + +impl Mul for Projection { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl Mul for Projection { + type Output = Vector4; + + fn mul(self, rhs: Vector4) -> Self::Output { + self.glam2(&rhs, |m, v| m * v) + } +} + +impl Index for Projection { + type Output = Vector4; + + fn index(&self, index: usize) -> &Self::Output { + &self.cols[index] + } +} + +impl IndexMut for Projection { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.cols[index] + } +} + +impl GlamType for Mat4 { + type Mapped = Projection; + + fn to_front(&self) -> Self::Mapped { + Projection { + cols: [ + self.col(0).to_front(), + self.col(1).to_front(), + self.col(2).to_front(), + self.col(3).to_front(), + ], + } + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self { + x_axis: mapped[0].to_glam(), + y_axis: mapped[1].to_glam(), + z_axis: mapped[2].to_glam(), + w_axis: mapped[3].to_glam(), + } + } +} + +impl GlamConv for Projection { + type Glam = Mat4; +} + +impl GodotFfi for Projection { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(C)] +pub enum ProjectionPlane { + Near = 0, + Far = 1, + Left = 2, + Top = 3, + Right = 4, + Bottom = 5, +} + +impl GodotFfi for ProjectionPlane { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(C)] +pub enum Eye { + Left = 1, + Right = 2, +} diff --git a/godot-core/src/builtin/quaternion.rs b/godot-core/src/builtin/quaternion.rs index 0d33d1a16..93c7258c8 100644 --- a/godot-core/src/builtin/quaternion.rs +++ b/godot-core/src/builtin/quaternion.rs @@ -11,6 +11,8 @@ use sys::{ffi_methods, GodotFfi}; use crate::builtin::glam_helpers::{GlamConv, GlamType}; use crate::builtin::{inner, math::*, vector3::*}; +use super::EulerOrder; + #[derive(Copy, Clone, Debug, PartialEq)] #[repr(C)] pub struct Quaternion { @@ -97,9 +99,8 @@ impl Quaternion { } // TODO: Figure out how godot actually treats "order", then make a match/if chain - pub fn get_euler(self, order: Option) -> Vector3 { - let _o = order.unwrap_or(2); - let vt = self.glam(|quat| quat.to_euler(glam::EulerRot::XYZ)); + pub fn get_euler(self, order: EulerOrder) -> Vector3 { + let vt = self.glam(|quat| quat.to_euler(order.to_glam())); Vector3::new(vt.0, vt.1, vt.2) } diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs new file mode 100644 index 000000000..801687095 --- /dev/null +++ b/godot-core/src/builtin/transform2d.rs @@ -0,0 +1,454 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +use std::{f32::consts::PI, ops::*}; + +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use super::{ + glam_helpers::{GlamConv, GlamType}, + math::*, + Vector2, +}; + +use glam::f32::Affine2; +use glam::f32::Mat2; + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +#[repr(C)] +pub struct Transform2D { + pub basis: Basis2D, + pub origin: Vector2, +} + +impl Transform2D { + pub const IDENTITY: Self = Self::new(Basis2D::IDENTITY, Vector2::ZERO); + pub const FLIP_X: Self = Self::new(Basis2D::FLIP_X, Vector2::ZERO); + pub const FLIP_Y: Self = Self::new(Basis2D::FLIP_Y, Vector2::ZERO); + + pub const fn new(basis: Basis2D, origin: Vector2) -> Self { + Self { basis, origin } + } + + pub fn from_angle(angle: f32) -> Self { + Self::from_angle_origin(angle, Vector2::ZERO) + } + + pub fn from_angle_origin(angle: f32, origin: Vector2) -> Self { + Self { + basis: Basis2D::from_angle(angle), + origin, + } + } + + pub fn from_angle_scale_skew_origin( + angle: f32, + scale: Vector2, + skew: f32, + origin: Vector2, + ) -> Self { + Self { + basis: Basis2D { + cols: [ + // Translated from Godot's implementation + Vector2::new(angle.cos() * scale.x, (angle + skew).cos() * scale.y), + Vector2::new(-(angle + skew).sin() * scale.y, angle.sin() * scale.x), + ], + }, + origin, + } + } + + pub fn affine_inverse(&self) -> Self { + self.glam(|aff| aff.inverse()) + } + + pub fn rotation(&self) -> f32 { + self.basis.rotation() + } + + pub fn scale(&self) -> Vector2 { + self.basis.scale() + } + + pub fn skew(&self) -> f32 { + self.basis.skew() + } + + pub fn interpolate_with(&self, other: &Self, weight: f32) -> Self { + Self::from_angle_scale_skew_origin( + lerp_angle(self.rotation(), other.rotation(), weight), + self.scale().lerp(other.scale(), weight), + lerp_angle(self.skew(), other.skew(), weight), + self.origin.lerp(other.origin, weight), + ) + } + + pub fn is_equal_approx(&self, other: &Self) -> bool { + self.basis.is_equal_approx(&other.basis) && self.origin.is_equal_approx(other.origin) + } + + pub fn is_finite(&self) -> bool { + self.basis.is_finite() && self.origin.is_finite() + } + + pub fn orthonormalize(&self) -> Self { + Self { + basis: self.basis.orthonormalize(), + origin: self.origin, + } + } + + pub fn rotated(&self, angle: f32) -> Self { + *self * Self::from_angle(angle) + } + + pub fn rotated_local(&self, angle: f32) -> Self { + Self::from_angle(angle) * *self + } + + pub fn scaled(&self, scale: Vector2) -> Self { + Self { + basis: self.basis.scaled(scale), + origin: self.origin * scale, + } + } + + pub fn scaled_local(&self, scale: Vector2) -> Self { + Self { + basis: self.basis.scaled(scale), + origin: self.origin, + } + } + + pub fn translated(&self, offset: Vector2) -> Self { + Self { + basis: self.basis, + origin: self.origin + offset, + } + } + + pub fn translated_local(&self, offset: Vector2) -> Self { + Self { + basis: self.basis, + origin: self.origin + (self.basis * offset), + } + } +} + +impl Index for Transform2D { + type Output = Vector2; + + fn index(&self, index: usize) -> &Self::Output { + match index { + 0 | 1 => &self.basis[index], + 2 => &self.origin, + _ => panic!("Index {index} out of bounds"), + } + } +} + +impl IndexMut for Transform2D { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + match index { + 0 | 1 => &mut self.basis[index], + 2 => &mut self.origin, + _ => panic!("Index {index} out of bounds"), + } + } +} + +impl Mul for Transform2D { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl Mul for Transform2D { + type Output = Vector2; + + fn mul(self, rhs: Vector2) -> Self::Output { + self.glam2(&rhs, |t, v| t.transform_point2(v)) + } +} + +impl Mul for Transform2D { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self { + basis: self.basis * rhs, + origin: self.origin * rhs, + } + } +} + +impl GlamType for Affine2 { + type Mapped = Transform2D; + + fn to_front(&self) -> Self::Mapped { + Transform2D { + basis: self.matrix2.to_front(), + origin: self.translation.to_front(), + } + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self { + matrix2: mapped.basis.to_glam(), + translation: mapped.origin.to_glam(), + } + } +} + +impl GlamConv for Transform2D { + type Glam = Affine2; +} + +impl GodotFfi for Transform2D { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(C)] +pub struct Basis2D { + pub cols: [Vector2; 2], +} + +impl Index for Basis2D { + type Output = Vector2; + + fn index(&self, index: usize) -> &Self::Output { + &self.cols[index] + } +} + +impl IndexMut for Basis2D { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.cols[index] + } +} + +impl Basis2D { + pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0); + pub const FLIP_X: Self = Self::from_diagonal(-1.0, 1.0); + pub const FLIP_Y: Self = Self::from_diagonal(1.0, -1.0); + + #[must_use] + pub const fn from_diagonal(x: f32, y: f32) -> Self { + Self { + cols: [Vector2::new(x, 0.0), Vector2::new(0.0, y)], + } + } + + #[must_use] + pub const fn new(cols: [Vector2; 2]) -> Self { + Self { cols } + } + + #[must_use] + pub const fn from_cols(x: Vector2, y: Vector2) -> Self { + Self { cols: [x, y] } + } + + #[must_use] + pub const fn from_rows(x: Vector2, y: Vector2) -> Self { + Self::from_cols(x, y).transpose() + } + + #[must_use] + pub const fn from_cols_array(cols: &[f32; 4]) -> Self { + let [x0, y0, x1, y1] = cols; + Self { + cols: [Vector2::new(*x0, *y0), Vector2::new(*x1, *y1)], + } + } + + #[must_use] + pub const fn from_cols_array_2d(cols: &[[f32; 2]; 2]) -> Self { + let [[x0, y0], [x1, y1]] = cols; + Self { + cols: [Vector2::new(*x0, *y0), Vector2::new(*x1, *y1)], + } + } + + #[must_use] + pub const fn to_rows_array(&self) -> [f32; 4] { + let [Vector2 { x: x0, y: y0 }, Vector2 { x: x1, y: y1 }] = self.cols; + [x0, y0, x1, y1] + } + + #[must_use] + pub const fn to_rows_array_2d(&self) -> [[f32; 2]; 2] { + let [Vector2 { x: x0, y: y0 }, Vector2 { x: x1, y: y1 }] = self.cols; + [[x0, y0], [x1, y1]] + } + + #[must_use] + pub fn from_scale(scale: Vector2) -> Self { + Mat2::from_scale_angle(scale.to_glam(), 0.0).to_front() + } + + #[must_use] + pub fn get_scale(&self) -> Vector2 { + let det = self.determinant(); + let det_sign = if det < 0.0 { -1.0 } else { 1.0 }; + + Vector2::new(self[0].length(), self[1].length()) * det_sign + } + + #[must_use] + pub fn from_angle(angle: f32) -> Self { + Mat2::from_angle(angle).to_front() + } + + #[must_use] + pub fn determinant(&self) -> f32 { + self.glam(|mat| mat.determinant()) + } + + #[must_use] + pub fn inverse(self) -> Self { + self.glam(|mat| mat.inverse()) + } + + #[must_use] + pub fn is_finite(&self) -> bool { + self.glam(|mat| mat.is_finite()) + } + + #[must_use] + pub fn orthonormalize(&self) -> Self { + assert!( + is_equal_approx(self.determinant(), 0.0), + "Determinant should not be zero." + ); + + // Gram-Schmidt Process + let mut x = self[0]; + let mut y = self[1]; + + x = x.normalized(); + y = y - x * (x.dot(y)); + y = y.normalized(); + + Self::from_cols(x, y) + } + + #[must_use] + pub fn rotate(&self, angle: f32) -> Self { + *self * Self::from_angle(angle) + } + + #[must_use] + pub fn tdotx(&self, with: Vector2) -> f32 { + self[0].dot(with) + } + + #[must_use] + pub fn tdoty(&self, with: Vector2) -> f32 { + self[1].dot(with) + } + + #[must_use] + pub fn rotation(&self) -> f32 { + // Translated from Godot + self[0].y.atan2(self[0].x) + } + + #[must_use] + pub fn scale(&self) -> Vector2 { + let det_sign = self.determinant().signum(); + Vector2::new(self[0].length(), self[1].length() * det_sign) + } + + #[must_use] + pub fn skew(&self) -> f32 { + // Translated from Godot + let det_sign = self.determinant().signum(); + self[0] + .normalized() + .dot(det_sign * self[1].normalized()) + .acos() + - PI * 0.5 + } + + pub fn is_equal_approx(&self, other: &Self) -> bool { + self[0].is_equal_approx(other[0]) && self[1].is_equal_approx(other[1]) + } + + pub const fn transpose(self) -> Self { + Self { + cols: [ + Vector2::new(self.cols[0].x, self.cols[1].x), + Vector2::new(self.cols[0].y, self.cols[1].y), + ], + } + } + + fn scaled(&self, scale: Vector2) -> Self { + Self { + cols: [self[0] * scale.x, self[1] * scale.y], + } + } +} + +impl Default for Basis2D { + fn default() -> Self { + Self::IDENTITY + } +} + +impl Mul for Basis2D { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl Mul for Basis2D { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + (self.to_glam() * rhs).to_front() + } +} + +impl MulAssign for Basis2D { + fn mul_assign(&mut self, rhs: f32) { + self[0] *= rhs; + self[1] *= rhs; + } +} + +impl Mul for Basis2D { + type Output = Vector2; + + fn mul(self, rhs: Vector2) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl GlamType for Mat2 { + type Mapped = Basis2D; + + fn to_front(&self) -> Self::Mapped { + Basis2D { + cols: [self.col(0).to_front(), self.col(1).to_front()], + } + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self::from_cols(mapped[0].to_glam(), mapped[1].to_glam()) + } +} + +impl GlamConv for Basis2D { + type Glam = Mat2; +} diff --git a/godot-core/src/builtin/transform3d.rs b/godot-core/src/builtin/transform3d.rs new file mode 100644 index 000000000..17633c224 --- /dev/null +++ b/godot-core/src/builtin/transform3d.rs @@ -0,0 +1,184 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +use std::ops::*; + +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +use super::{ + glam_helpers::{GlamConv, GlamType}, + Basis, Vector3, +}; + +use glam::f32::Affine3A; + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +#[repr(C)] +pub struct Transform3D { + pub basis: Basis, + pub origin: Vector3, +} + +impl Transform3D { + pub const IDENTITY: Self = Self::new(Basis::IDENTITY, Vector3::ZERO); + pub const FLIP_X: Self = Self::new(Basis::FLIP_X, Vector3::ZERO); + pub const FLIP_Y: Self = Self::new(Basis::FLIP_Y, Vector3::ZERO); + pub const FLIP_Z: Self = Self::new(Basis::FLIP_Z, Vector3::ZERO); + + pub const fn new(basis: Basis, origin: Vector3) -> Self { + Self { basis, origin } + } + + pub const fn from_cols(x: Vector3, y: Vector3, z: Vector3, origin: Vector3) -> Self { + Self { + basis: Basis::from_cols(x, y, z), + origin, + } + } + + pub fn inverted(&self) -> Self { + self.glam(|aff| aff.inverse()) + } + + pub fn interpolate_with(&self, other: &Self, weight: f32) -> Self { + Self { + basis: self + .basis + .slerp(other.basis, weight) + .scaled(self.basis.scale().lerp(other.basis.scale(), weight)), + origin: self.origin.lerp(other.origin, weight), + } + } + + pub fn is_equal_approx(&self, other: &Self) -> bool { + self.basis.is_equal_approx(&other.basis) && self.origin.is_equal_approx(other.origin) + } + + pub fn is_finite(&self) -> bool { + self.basis.is_finite() && self.origin.is_finite() + } + + pub fn looking_at(&self, target: Vector3, up: Vector3) -> Self { + Self { + basis: Basis::looking_at(target, up), + origin: self.origin, + } + } + + pub fn orthonormalized(&self) -> Self { + Self { + basis: self.basis.orthonormalize(), + origin: self.origin, + } + } + + pub fn rotated(&self, axis: Vector3, angle: f32) -> Self { + let basis = self.basis.rotate(axis, angle); + Self { + basis, + origin: basis * self.origin, + } + } + + pub fn rotated_local(&self, axis: Vector3, angle: f32) -> Self { + let basis = self.basis.rotate(axis, angle); + Self { + basis, + origin: self.origin, + } + } + + pub fn scaled(&self, scale: Vector3) -> Self { + Self { + basis: self.basis.scaled(scale), + origin: self.origin * scale, + } + } + + pub fn scaled_local(&self, scale: Vector3) -> Self { + Self { + basis: self.basis.scaled(scale), + origin: self.origin, + } + } + + pub fn translated(&self, offset: Vector3) -> Self { + Self { + basis: self.basis, + origin: self.origin + offset, + } + } + + pub fn translated_local(&self, offset: Vector3) -> Self { + Self { + basis: self.basis, + origin: self.origin + (self.basis * offset), + } + } + + pub fn col(&self, index: usize) -> Vector3 { + match index { + 0 => self.basis.col_x(), + 1 => self.basis.col_y(), + 2 => self.basis.col_z(), + 3 => self.origin, + _ => panic!("Index {index} out of bounds"), + } + } +} + +impl Mul for Transform3D { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + self.glam2(&rhs, |a, b| a * b) + } +} + +impl Mul for Transform3D { + type Output = Vector3; + + fn mul(self, rhs: Vector3) -> Self::Output { + self.glam2(&rhs, |t, v| t.transform_point3(v)) + } +} + +impl Mul for Transform3D { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self { + basis: self.basis * rhs, + origin: self.origin * rhs, + } + } +} + +impl GlamType for Affine3A { + type Mapped = Transform3D; + + fn to_front(&self) -> Self::Mapped { + Transform3D { + basis: self.matrix3.to_front(), + origin: self.translation.to_front(), + } + } + + fn from_front(mapped: &Self::Mapped) -> Self { + Self { + matrix3: mapped.basis.to_glam().into(), + translation: mapped.origin.to_glam().into(), + } + } +} + +impl GlamConv for Transform3D { + type Glam = Affine3A; +} + +impl GodotFfi for Transform3D { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index b0c8f9728..a43ac893f 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -141,6 +141,7 @@ mod impls { use super::*; impl_variant_traits!(bool, bool_to_variant, bool_from_variant, Bool); + impl_variant_traits!(Basis, basis_to_variant, basis_from_variant, Basis); impl_variant_traits!(Vector2, vector2_to_variant, vector2_from_variant, Vector2); impl_variant_traits!(Vector3, vector3_to_variant, vector3_from_variant, Vector3); impl_variant_traits!(Vector4, vector4_to_variant, vector4_from_variant, Vector4); @@ -156,10 +157,6 @@ mod impls { impl_variant_metadata!(Plane, /* plane_to_variant, plane_from_variant, */ Plane); impl_variant_metadata!(Quaternion, /* quaternion_to_variant, quaternion_from_variant, */ Quaternion); impl_variant_metadata!(Aabb, /* aabb_to_variant, aabb_from_variant, */ Aabb); - impl_variant_metadata!(Basis, /* basis_to_variant, basis_from_variant, */ Basis); - impl_variant_metadata!(Transform2D, /* transform_2d_to_variant, transform_2d_from_variant, */ Transform2D); - impl_variant_metadata!(Transform3D, /* transform_3d_to_variant, transform_3d_from_variant, */ Transform3D); - impl_variant_metadata!(Projection, /* projection_to_variant, projection_from_variant, */ Projection); impl_variant_metadata!(Rid, /* rid_to_variant, rid_from_variant, */ Rid); impl_variant_metadata!(Callable, /* callable_to_variant, callable_from_variant, */ Callable); impl_variant_metadata!(Signal, /* signal_to_variant, signal_from_variant, */ Signal); @@ -172,6 +169,9 @@ mod impls { impl_variant_traits!(PackedVector2Array, packed_vector2_array_to_variant, packed_vector2_array_from_variant, PackedVector2Array); impl_variant_traits!(PackedVector3Array, packed_vector3_array_to_variant, packed_vector3_array_from_variant, PackedVector3Array); impl_variant_traits!(PackedColorArray, packed_color_array_to_variant, packed_color_array_from_variant, PackedColorArray); + impl_variant_traits!(Projection, projection_to_variant, projection_from_variant, Projection); + impl_variant_traits!(Transform2D, transform_2d_to_variant, transform_2d_from_variant, Transform2D); + impl_variant_traits!(Transform3D, transform_3d_to_variant, transform_3d_from_variant, Transform3D); impl_variant_traits!(Dictionary, dictionary_to_variant, dictionary_from_variant, Dictionary); impl_variant_traits!(i64, int_to_variant, int_from_variant, Int, GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT64); From 0f967bc3630bdb983bbf2936bb9a2de30f57c529 Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Thu, 16 Feb 2023 02:19:40 +0100 Subject: [PATCH 02/15] Cleanup basis.rs Add docs to basis.rs --- godot-core/src/builtin/basis.rs | 286 +++++++++++++++++--------- godot-core/src/builtin/transform3d.rs | 8 +- 2 files changed, 191 insertions(+), 103 deletions(-) diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index a6e300937..f1e3dec7e 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -16,23 +16,48 @@ use super::{ use glam::f32::Mat3; +/// A 3x3 matrix, typically used as an orthogonal basis for [`Transform3D`](crate::builtin::Transform3D). +/// +/// Indexing into a `Basis` is done in row-major order. So `mat[1]` would return the first *row* and not +/// the first *column*/basis vector. +/// +/// The basis vectors are the columns of the matrix, whereas the [`rows`](Self::rows) field represents +/// the row vectors. #[derive(Debug, Clone, Copy, PartialEq)] #[repr(C)] pub struct Basis { + /// The rows of the matrix. These are *not* the basis vectors. + /// + /// To access the basis vectors see [`col_x()`](Self::col_x), [`set_col_x()`](Self::set_col_x), + /// [`col_y()`](Self::col_y), [`set_col_y()`](Self::set_col_y), [`col_x`](Self::col_z()), + /// [`set_col_z()`](Self::set_col_z). pub rows: [Vector3; 3], } impl Basis { + /// The identity basis, with no rotation or scaling applied. + /// + /// _Godot equivalent: `Basis.IDENTITY`_ pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0, 1.0); + + /// The basis that will flip something along the X axis when used in a transformation. + /// + /// _Godot equivalent: `Basis.FLIP_X`_ pub const FLIP_X: Self = Self::from_diagonal(-1.0, 1.0, 1.0); + + /// The basis that will flip something along the Y axis when used in a transformation. + /// + /// _Godot equivalent: `Basis.FLIP_Y`_ 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); - #[must_use] - pub const fn new(rows: [Vector3; 3]) -> Self { - Self { rows } - } + /// The basis that will flip something along the Z axis when used in a transformation. + /// + /// _Godot equivalent: `Basis.FLIP_Z`_ + pub const FLIP_Z: Self = Self::from_diagonal(1.0, 1.0, -1.0); + /// Create a diagonal matrix from the given vector. + /// + /// _Godot equivalent: `Basis.from_scale(Vector3 scale)` #[must_use] pub const fn from_diagonal(x: f32, y: f32, z: f32) -> Self { Self { @@ -44,18 +69,20 @@ impl Basis { } } + /// Create a new basis from 3 row vectors. These are *not* basis vectors. #[must_use] pub const fn from_rows(x: Vector3, y: Vector3, z: Vector3) -> Self { Self { rows: [x, y, z] } } + /// Create a new basis from 3 column vectors. #[must_use] pub const fn from_cols(x: Vector3, y: Vector3, z: Vector3) -> Self { - Self::from_rows(x, y, z).transpose() + Self::from_rows(x, y, z).transposed() } #[must_use] - pub const fn from_rows_array(rows: &[f32; 9]) -> Self { + const fn from_rows_array(rows: &[f32; 9]) -> Self { let [x0, y0, z0, x1, y1, z1, x2, y2, z2] = rows; Self { rows: [ @@ -66,94 +93,80 @@ impl Basis { } } - #[must_use] - pub const fn from_rows_array_2d(rows: &[[f32; 3]; 3]) -> Self { - let [[x0, y0, z0], [x1, y1, z1], [x2, y2, z2]] = rows; - Self { - rows: [ - Vector3::new(*x0, *y0, *z0), - Vector3::new(*x1, *y1, *z1), - Vector3::new(*x2, *y2, *z2), - ], - } - } - - #[must_use] - pub const fn to_rows_array(&self) -> [f32; 9] { - let [Vector3 { - x: x0, - y: y0, - z: z0, - }, Vector3 { - x: x1, - y: y1, - z: z1, - }, Vector3 { - x: x2, - y: y2, - z: z2, - }] = self.rows; - [x0, y0, z0, x1, y1, z1, x2, y2, z2] - } - - #[must_use] - pub const fn to_rows_array_2d(&self) -> [[f32; 3]; 3] { - let [Vector3 { - x: x0, - y: y0, - z: z0, - }, Vector3 { - x: x1, - y: y1, - z: z1, - }, Vector3 { - x: x2, - y: y2, - z: z2, - }] = self.rows; - [[x0, y0, z0], [x1, y1, z1], [x2, y2, z2]] - } - - #[must_use] - pub fn to_quat(&self) -> Quaternion { - glam::Quat::from_mat3(&self.to_glam()).to_front() - } - + /// Create a `Basis` from an axis and angle. + /// + /// _Godot equivalent: `Basis(Vector3 axis, float angle)`_ #[must_use] pub fn from_axis_angle(axis: Vector3, angle: f32) -> Self { Mat3::from_axis_angle(axis.to_glam(), angle).to_front() } + /// Create a `Basis` from a `Quaternion`. + /// + /// _Godot equivalent: `Basis(Quaternion from)`_ #[must_use] pub fn from_quat(quat: Quaternion) -> Self { Mat3::from_quat(quat.to_glam()).to_front() } + /// Create a `Basis` from three angles `a`, `b`, and `c` interpreted + /// as Euler angles according to the given `EulerOrder`. + /// + /// _Godot equivalent: `Basis.from_euler(Vector3 euler, int order)`_ #[must_use] pub fn from_euler(order: EulerOrder, a: f32, b: f32, c: f32) -> Self { Mat3::from_euler(order.to_glam(), a, b, c).to_front() } + /// Creates a `Basis` with a rotation such that the forward axis (-Z) points + /// towards the `target` position. + /// + /// The up axis (+Y) points as close to the `up` vector as possible while + /// staying perpendicular to the forward axis. The resulting Basis is + /// orthonormalized. The `target` and `up` vectors cannot be zero, and + /// cannot be parallel to each other. + /// + /// _Godot equivalent: `Basis.looking_at(Vector3 target, Vector3 up)`_ #[must_use] - pub fn from_scale(x: f32, y: f32, z: f32) -> Self { - Self::from_diagonal(x, y, z) + pub fn new_looking_at(target: Vector3, up: Vector3) -> Self { + super::inner::InnerBasis::looking_at(target, up) } + /// Creates a `[Vector3; 3]` with the columns of the `Basis`. #[must_use] - pub fn looking_at(target: Vector3, up: Vector3) -> Self { - super::inner::InnerBasis::looking_at(target, up) + pub fn to_cols(self) -> [Vector3; 3] { + self.transposed().rows } + /// Creates a [`Quaternion`] representing the same rotation as this basis. + /// + /// _Godot equivalent: `Basis(Quaternion from)`_ #[must_use] - pub fn determinant(&self) -> f32 { - self.to_glam().determinant() + pub fn to_quat(&self) -> Quaternion { + glam::Quat::from_mat3(&self.to_glam()).to_front() } #[must_use] - pub fn euler(&self, order: EulerOrder) -> Vector3 { - self.to_quat().get_euler(order) + const fn to_rows_array(self) -> [f32; 9] { + let [Vector3 { + x: x0, + y: y0, + z: z0, + }, Vector3 { + x: x1, + y: y1, + z: z1, + }, Vector3 { + x: x2, + y: y2, + z: z2, + }] = self.rows; + [x0, y0, z0, x1, y1, z1, x2, y2, z2] } + /// Returns the scale of the matrix. + /// + /// _Godot equivalent: `Basis.get_scale()`_ #[must_use] pub fn scale(&self) -> Vector3 { let det = self.determinant(); @@ -166,22 +179,61 @@ impl Basis { ) * det_sign } - pub fn scaled(&self, scale: Vector3) -> Self { - *self * Self::from_scale(scale.x, scale.y, scale.z) + /// Returns the rotation of the matrix in euler angles. + /// + /// The order of the angles are given by `order`. + /// + /// _Godot equivalent: `Basis.get_euler(int order)`_ + #[must_use] + pub fn euler_angles(&self, order: EulerOrder) -> Vector3 { + self.to_quat().get_euler(order) + } + + /// Returns the determinant of the matrix. + /// + /// _Godot equivalent: `Basis.determinant()`_ + #[must_use] + pub fn determinant(&self) -> f32 { + self.to_glam().determinant() + } + + /// Introduce an additional scaling specified by the given 3D scaling factor. + /// + /// _Godot equivalent: `Basis.scaled(Vector3 scale)`_ + #[must_use] + pub fn scaled(self, scale: Vector3) -> Self { + self * Self::from_diagonal(scale.x, scale.y, scale.z) } + /// Returns the inverse of the matrix. + /// + /// _Godot equivalent: `Basis.inverse()`_ #[must_use] pub fn inverse(&self) -> Basis { self.glam(|mat| mat.inverse()) } + /// Returns the transposed version of the matrix. + /// + /// _Godot equivalent: `Basis.transposed()`_ #[must_use] - pub fn is_finite(&self) -> bool { - self.to_glam().is_finite() + pub const fn transposed(self) -> Self { + Self { + rows: [ + Vector3::new(self.rows[0].x, self.rows[1].x, self.rows[2].x), + Vector3::new(self.rows[0].y, self.rows[1].y, self.rows[2].y), + Vector3::new(self.rows[0].z, self.rows[1].z, self.rows[2].z), + ], + } } + /// Returns the orthonormalized version of the matrix (useful to call from + /// time to time to avoid rounding error for orthogonal matrices). This + /// performs a Gram-Schmidt orthonormalization on the basis of the matrix. + /// + /// _Godot equivalent: `Basis.orthonormalized()`_ #[must_use] - pub fn orthonormalize(&self) -> Self { + pub fn orthonormalized(&self) -> Self { assert!( is_equal_approx(self.determinant(), 0.0), "Determinant should not be zero." @@ -201,11 +253,19 @@ impl Basis { Self::from_cols(x, y, z) } + /// Introduce an additional rotation around the given `axis` by `angle` + /// (in radians). The axis must be a normalized vector. + /// + /// _Godot equivalent: `Basis.rotated(Vector3 axis, float angle)`_ #[must_use] - pub fn rotate(self, axis: Vector3, angle: f32) -> Self { + pub fn rotated(self, axis: Vector3, angle: f32) -> Self { self * Self::from_axis_angle(axis, angle) } + /// Assuming that the matrix is a proper rotation matrix, slerp performs + /// a spherical-linear interpolation with another rotation matrix. + /// + /// _Godot equivalent: `Basis.slerp(Basis to, float weight)`_ #[must_use] pub fn slerp(self, other: Self, weight: f32) -> Self { let from = self.to_quat(); @@ -220,66 +280,92 @@ impl Basis { result } + /// Transposed dot product with the X axis (column) of the matrix. + /// + /// _Godot equivalent: `Basis.tdotx(Vector3 with)`_ #[must_use] pub fn tdotx(&self, with: Vector3) -> f32 { self.col_x().dot(with) } + /// Transposed dot product with the Y axis (column) of the matrix. + /// + /// _Godot equivalent: `Basis.tdoty(Vector3 with)`_ #[must_use] pub fn tdoty(&self, with: Vector3) -> f32 { self.col_y().dot(with) } + /// Transposed dot product with the Z axis (column) of the matrix. + /// + /// _Godot equivalent: `Basis.tdotz(Vector3 with)`_ #[must_use] pub fn tdotz(&self, with: Vector3) -> f32 { self.col_z().dot(with) } + /// Returns `true` if this basis is finite. Meaning each element of the + /// matrix is not `NaN`, positive infinity, or negative infinity. + /// + /// _Godot equivalent: `Basis.is_finite()`_ #[must_use] - pub const fn transpose(self) -> Self { - Self { - rows: [ - Vector3::new(self.rows[0].x, self.rows[1].x, self.rows[2].x), - Vector3::new(self.rows[0].y, self.rows[1].y, self.rows[2].y), - Vector3::new(self.rows[0].z, self.rows[1].z, self.rows[2].z), - ], - } + pub fn is_finite(&self) -> bool { + self.to_glam().is_finite() + } + + /// Returns `true` if this basis and `other` are approximately equal, + /// by calling `is_equal_approx` on each row. + /// + /// _Godot equivalent: `Basis.is_equal_approx(Basis b)`_ + pub fn is_equal_approx(&self, other: &Self) -> bool { + self[0].is_equal_approx(other[0]) + && self[1].is_equal_approx(other[1]) + && self[2].is_equal_approx(other[2]) } + /// Returns the first column of the matrix, + /// + /// _Godot equivalent: `Basis.x`_ #[must_use] pub fn col_x(&self) -> Vector3 { Vector3::new(self[0].x, self[1].x, self[2].x) } - pub fn col_x_mut(&mut self) -> [&mut f32; 3] { - let [x, y, z] = self.rows.as_mut_slice() else { unreachable!() }; - [&mut x.x, &mut y.x, &mut z.x] + /// Set the values of the first column of the matrix. + pub fn set_col_x(&mut self, col: Vector3) { + self[0].x = col.x; + self[1].x = col.y; + self[2].x = col.z; } + /// Returns the second column of the matrix, + /// + /// _Godot equivalent: `Basis.y`_ #[must_use] pub fn col_y(&self) -> Vector3 { Vector3::new(self[0].y, self[1].y, self[2].y) } - pub fn col_y_mut(&mut self) -> [&mut f32; 3] { - let [x, y, z] = self.rows.as_mut_slice() else { unreachable!() }; - [&mut x.y, &mut y.y, &mut z.y] + /// Set the values of the second column of the matrix. + pub fn set_col_y(&mut self, col: Vector3) { + self[0].y = col.x; + self[1].y = col.y; + self[2].y = col.z; } + /// Returns the third column of the matrix, + /// + /// _Godot equivalent: `Basis.z`_ #[must_use] pub fn col_z(&self) -> Vector3 { Vector3::new(self[0].z, self[1].z, self[2].z) } - pub fn col_z_mut(&mut self) -> [&mut f32; 3] { - let [x, y, z] = self.rows.as_mut_slice() else { unreachable!() }; - [&mut x.z, &mut y.z, &mut z.z] - } - - pub fn is_equal_approx(&self, other: &Self) -> bool { - self[0].is_equal_approx(other[0]) - && self[1].is_equal_approx(other[1]) - && self[2].is_equal_approx(other[2]) + /// Set the values of the third column of the matrix. + pub fn set_col_z(&mut self, col: Vector3) { + self[0].z = col.x; + self[1].z = col.y; + self[2].z = col.z; } } @@ -291,7 +377,7 @@ impl GlamType for glam::f32::Mat3 { type Mapped = Basis; fn to_front(&self) -> Self::Mapped { - Basis::from_rows_array_2d(&self.to_cols_array_2d()).transpose() + Basis::from_rows_array(&self.to_cols_array()).transposed() } fn from_front(mapped: &Self::Mapped) -> Self { @@ -303,7 +389,7 @@ impl GlamType for glam::f32::Mat3A { type Mapped = Basis; fn to_front(&self) -> Self::Mapped { - Basis::from_rows_array_2d(&self.to_cols_array_2d()).transpose() + Basis::from_rows_array(&self.to_cols_array()).transposed() } fn from_front(mapped: &Self::Mapped) -> Self { @@ -314,12 +400,14 @@ impl GlamType for glam::f32::Mat3A { impl Index for Basis { type Output = Vector3; + /// Indexes into the basis by rows. fn index(&self, index: usize) -> &Self::Output { &self.rows[index] } } impl IndexMut for Basis { + /// Indexes into the basis by rows. fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.rows[index] } diff --git a/godot-core/src/builtin/transform3d.rs b/godot-core/src/builtin/transform3d.rs index 17633c224..f5d9fe372 100644 --- a/godot-core/src/builtin/transform3d.rs +++ b/godot-core/src/builtin/transform3d.rs @@ -63,20 +63,20 @@ impl Transform3D { pub fn looking_at(&self, target: Vector3, up: Vector3) -> Self { Self { - basis: Basis::looking_at(target, up), + basis: Basis::new_looking_at(target, up), origin: self.origin, } } pub fn orthonormalized(&self) -> Self { Self { - basis: self.basis.orthonormalize(), + basis: self.basis.orthonormalized(), origin: self.origin, } } pub fn rotated(&self, axis: Vector3, angle: f32) -> Self { - let basis = self.basis.rotate(axis, angle); + let basis = self.basis.rotated(axis, angle); Self { basis, origin: basis * self.origin, @@ -84,7 +84,7 @@ impl Transform3D { } pub fn rotated_local(&self, axis: Vector3, angle: f32) -> Self { - let basis = self.basis.rotate(axis, angle); + let basis = self.basis.rotated(axis, angle); Self { basis, origin: self.origin, From c521f2e9f84c5bdbfc19ff70470617db66558a1b Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Thu, 16 Feb 2023 20:16:58 +0100 Subject: [PATCH 03/15] Add some tests for basis. Fix euler angle functions --- godot-core/src/builtin/basis.rs | 561 ++++++++++++++++++++++++-- godot-core/src/builtin/euler_order.rs | 55 --- godot-core/src/builtin/mod.rs | 2 - godot-core/src/builtin/quaternion.rs | 6 +- itest/rust/src/basis_test.rs | 135 +++++++ itest/rust/src/lib.rs | 2 + itest/rust/src/variant_test.rs | 14 +- 7 files changed, 690 insertions(+), 85 deletions(-) delete mode 100644 godot-core/src/builtin/euler_order.rs create mode 100644 itest/rust/src/basis_test.rs diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index f1e3dec7e..656077d39 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::ops::*; +use std::{f32::consts::FRAC_PI_2, fmt::Display, ops::*}; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; @@ -11,7 +11,7 @@ use sys::{ffi_methods, GodotFfi}; use super::{ glam_helpers::{GlamConv, GlamType}, math::*, - EulerOrder, Quaternion, Vector3, + Quaternion, Vector3, }; use glam::f32::Mat3; @@ -19,7 +19,8 @@ use glam::f32::Mat3; /// A 3x3 matrix, typically used as an orthogonal basis for [`Transform3D`](crate::builtin::Transform3D). /// /// Indexing into a `Basis` is done in row-major order. So `mat[1]` would return the first *row* and not -/// the first *column*/basis vector. +/// the first *column*/basis vector. This means that indexing into the matrix happens in the same order +/// it usually does in math, except that we index starting at 0. /// /// The basis vectors are the columns of the matrix, whereas the [`rows`](Self::rows) field represents /// the row vectors. @@ -78,7 +79,7 @@ impl Basis { /// Create a new basis from 3 column vectors. #[must_use] pub const fn from_cols(x: Vector3, y: Vector3, z: Vector3) -> Self { - Self::from_rows(x, y, z).transposed() + Self::from_rows_array(&[x.x, y.x, z.x, x.y, y.y, z.y, x.z, y.z, z.z]) } #[must_use] @@ -115,7 +116,27 @@ impl Basis { /// _Godot equivalent: `Basis.from_euler(Vector3 euler, int order)`_ #[must_use] pub fn from_euler(order: EulerOrder, a: f32, b: f32, c: f32) -> Self { - Mat3::from_euler(order.to_glam(), a, b, c).to_front() + // Translated from "Basis::from_euler" in + // https://github.com/godotengine/godot/blob/master/core/math/basis.cpp + + // We can't use glam to do these conversions since glam uses intrinsic rotations + // whereas godot uses extrinsic rotations. + // see https://github.com/bitshifter/glam-rs/issues/337 + 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, + } } /// Creates a `Basis` with a rotation such that the forward axis (-Z) points @@ -186,7 +207,183 @@ impl Basis { /// _Godot equivalent: `Basis.get_euler(int order)`_ #[must_use] pub fn euler_angles(&self, order: EulerOrder) -> Vector3 { - self.to_quat().get_euler(order) + // Translated from "Basis::get_euler" in + // https://github.com/godotengine/godot/blob/master/core/math/basis.cpp + + // We can't use glam to do these conversions since glam uses intrinsic rotations + // whereas godot uses extrinsic rotations. + // see https://github.com/bitshifter/glam-rs/issues/337 + match order { + EulerOrder::XYZ => { + // Euler angles in XYZ convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cy*cz -cy*sz sy + // cz*sx*sy+cx*sz cx*cz-sx*sy*sz -cy*sx + // -cx*cz*sy+sx*sz cz*sx+cx*sy*sz cx*cy + + let sy: f32 = self[0][2]; + if sy < 1.0 - CMP_EPSILON { + if sy > -(1.0 - CMP_EPSILON) { + // is this a pure Y rotation? + if self[1][0] == 0.0 + && self[0][1] == 0.0 + && self[1][2] == 0.0 + && self[2][1] == 0.0 + && self[1][1] == 1.0 + { + // return the simplest form (human friendlier in editor and scripts) + Vector3::new(0.0, f32::atan2(self[0][2], self[0][0]), 0.0) + } else { + Vector3::new( + f32::atan2(-self[1][2], self[2][2]), + f32::asin(sy), + f32::atan2(-self[0][1], self[0][0]), + ) + } + } else { + Vector3::new(f32::atan2(self[2][1], self[1][1]), -FRAC_PI_2, 0.0) + } + } else { + Vector3::new(f32::atan2(self[2][1], self[1][1]), FRAC_PI_2, 0.0) + } + } + EulerOrder::XZY => { + // Euler angles in XZY convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cz*cy -sz cz*sy + // sx*sy+cx*cy*sz cx*cz cx*sz*sy-cy*sx + // cy*sx*sz cz*sx cx*cy+sx*sz*sy + + let sz: f32 = self[0][1]; + if sz < (1.0 - CMP_EPSILON) { + if sz > -(1.0 - CMP_EPSILON) { + Vector3::new( + f32::atan2(self[2][1], self[1][1]), + f32::atan2(self[0][2], self[0][0]), + f32::asin(-sz), + ) + } else { + // It's -1 + Vector3::new(-f32::atan2(self[1][2], self[2][2]), 0.0, FRAC_PI_2) + } + } else { + // It's 1 + Vector3::new(-f32::atan2(self[1][2], self[2][2]), 0.0, -FRAC_PI_2) + } + } + EulerOrder::YXZ => { + // Euler angles in YXZ convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cy*cz+sy*sx*sz cz*sy*sx-cy*sz cx*sy + // cx*sz cx*cz -sx + // cy*sx*sz-cz*sy cy*cz*sx+sy*sz cy*cx + + let m12: f32 = self[1][2]; + + if m12 < (1.0 - CMP_EPSILON) { + if m12 > -(1.0 - CMP_EPSILON) { + // is this a pure X rotation? + if self[1][0] == 0.0 + && self[0][1] == 0.0 + && self[0][2] == 0.0 + && self[2][0] == 0.0 + && self[0][0] == 1.0 + { + // return the simplest form (human friendlier in editor and scripts) + Vector3::new(f32::atan2(-m12, self[1][1]), 0.0, 0.0) + } else { + Vector3::new( + f32::asin(-m12), + f32::atan2(self[0][2], self[2][2]), + f32::atan2(self[1][0], self[1][1]), + ) + } + } else { + // m12 == -1 + Vector3::new(FRAC_PI_2, f32::atan2(self[0][1], self[0][0]), 0.0) + } + } else { + // m12 == 1 + Vector3::new(-FRAC_PI_2, -f32::atan2(self[0][1], self[0][0]), 0.0) + } + } + EulerOrder::YZX => { + // Euler angles in YZX convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cy*cz sy*sx-cy*cx*sz cx*sy+cy*sz*sx + // sz cz*cx -cz*sx + // -cz*sy cy*sx+cx*sy*sz cy*cx-sy*sz*sx + + let sz: f32 = self[1][0]; + if sz < (1.0 - CMP_EPSILON) { + if sz > -(1.0 - CMP_EPSILON) { + Vector3::new( + f32::atan2(-self[1][2], self[1][1]), + f32::atan2(-self[2][0], self[0][0]), + f32::asin(sz), + ) + } else { + // It's -1 + Vector3::new(f32::atan2(self[2][1], self[2][2]), 0.0, -FRAC_PI_2) + } + } else { + // It's 1 + Vector3::new(f32::atan2(self[2][1], self[2][2]), 0.0, FRAC_PI_2) + } + } + EulerOrder::ZXY => { + // Euler angles in ZXY convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cz*cy-sz*sx*sy -cx*sz cz*sy+cy*sz*sx + // cy*sz+cz*sx*sy cz*cx sz*sy-cz*cy*sx + // -cx*sy sx cx*cy + let sx: f32 = self[2][1]; + if sx < (1.0 - CMP_EPSILON) { + if sx > -(1.0 - CMP_EPSILON) { + Vector3::new( + f32::asin(sx), + f32::atan2(-self[2][0], self[2][2]), + f32::atan2(-self[0][1], self[1][1]), + ) + } else { + // It's -1 + Vector3::new(-FRAC_PI_2, f32::atan2(self[0][2], self[0][0]), 0.0) + } + } else { + // It's 1 + Vector3::new(FRAC_PI_2, f32::atan2(self[0][2], self[0][0]), 0.0) + } + } + EulerOrder::ZYX => { + // Euler angles in ZYX convention. + // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix + // + // rot = cz*cy cz*sy*sx-cx*sz sz*sx+cz*cx*cy + // cy*sz cz*cx+sz*sy*sx cx*sz*sy-cz*sx + // -sy cy*sx cy*cx + let sy: f32 = self[2][0]; + if sy < (1.0 - CMP_EPSILON) { + if sy > -(1.0 - CMP_EPSILON) { + Vector3::new( + f32::atan2(self[2][1], self[2][2]), + f32::asin(-sy), + f32::atan2(self[1][0], self[0][0]), + ) + } else { + // It's -1 + Vector3::new(0.0, FRAC_PI_2, -f32::atan2(self[0][1], self[1][1])) + } + } else { + // It's 1 + Vector3::new(0.0, -FRAC_PI_2, -f32::atan2(self[0][1], self[1][1])) + } + } + } } /// Returns the determinant of the matrix. @@ -217,12 +414,12 @@ impl Basis { /// /// _Godot equivalent: `Basis.transposed()`_ #[must_use] - pub const fn transposed(self) -> Self { + pub fn transposed(self) -> Self { Self { rows: [ - Vector3::new(self.rows[0].x, self.rows[1].x, self.rows[2].x), - Vector3::new(self.rows[0].y, self.rows[1].y, self.rows[2].y), - Vector3::new(self.rows[0].z, self.rows[1].z, self.rows[2].z), + Vector3::new(self[0][0], self[1][0], self[2][0]), + Vector3::new(self[0][1], self[1][1], self[2][1]), + Vector3::new(self[0][2], self[1][2], self[2][2]), ], } } @@ -274,7 +471,7 @@ impl Basis { let mut result = Self::from_quat(from.slerp(to, weight)); for i in 0..3 { - result.rows[i] *= lerp(self.rows[i].length(), other.rows[i].length(), weight); + result[i] *= lerp(self[i].length(), other[i].length(), weight); } result @@ -328,14 +525,14 @@ impl Basis { /// _Godot equivalent: `Basis.x`_ #[must_use] pub fn col_x(&self) -> Vector3 { - Vector3::new(self[0].x, self[1].x, self[2].x) + Vector3::new(self[0][0], self[1][0], self[2][0]) } /// Set the values of the first column of the matrix. pub fn set_col_x(&mut self, col: Vector3) { - self[0].x = col.x; - self[1].x = col.y; - self[2].x = col.z; + self[0][0] = col.x; + self[1][0] = col.y; + self[2][0] = col.z; } /// Returns the second column of the matrix, @@ -343,14 +540,14 @@ impl Basis { /// _Godot equivalent: `Basis.y`_ #[must_use] pub fn col_y(&self) -> Vector3 { - Vector3::new(self[0].y, self[1].y, self[2].y) + Vector3::new(self[0][1], self[1][1], self[2][1]) } /// Set the values of the second column of the matrix. pub fn set_col_y(&mut self, col: Vector3) { - self[0].y = col.x; - self[1].y = col.y; - self[2].y = col.z; + self[0][1] = col.x; + self[1][1] = col.y; + self[2][1] = col.z; } /// Returns the third column of the matrix, @@ -358,14 +555,41 @@ impl Basis { /// _Godot equivalent: `Basis.z`_ #[must_use] pub fn col_z(&self) -> Vector3 { - Vector3::new(self[0].z, self[1].z, self[2].z) + Vector3::new(self[0][2], self[1][2], self[2][2]) } /// Set the values of the third column of the matrix. pub fn set_col_z(&mut self, col: Vector3) { - self[0].z = col.x; - self[1].z = col.y; - self[2].z = col.z; + self[0][2] = col.x; + self[1][2] = col.y; + self[2][2] = col.z; + } +} + +impl Display for Basis { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Godot output: + // [X: (1, 0, 0), Y: (0, 1, 0), Z: (0, 0, 1)] + // Where X,Y,Z are the columns + let Vector3 { + x: x1, + y: x2, + z: x3, + } = self.col_x(); + let Vector3 { + x: y1, + y: y2, + z: y3, + } = self.col_y(); + let Vector3 { + x: z1, + y: z2, + z: z3, + } = self.col_z(); + + f.write_fmt(format_args!( + "[X: ({x1}, {x2}, {x3}), Y: ({y1}, {y2}, {y3}), Z: ({z1}, {z2}, {z3})]" + )) } } @@ -454,3 +678,294 @@ impl Mul for Basis { impl GodotFfi for Basis { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub enum EulerOrder { + XYZ = 0, + XZY = 1, + YXZ = 2, + YZX = 3, + ZXY = 4, + ZYX = 5, +} + +impl GodotFfi for EulerOrder { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +#[cfg(test)] +mod test { + use std::f32::consts::{FRAC_PI_2, PI}; + + use super::*; + + fn deg_to_rad(rotation: Vector3) -> Vector3 { + Vector3::new( + rotation.x.to_radians(), + rotation.y.to_radians(), + rotation.z.to_radians(), + ) + } + + // Translated from Godot + fn test_rotation(deg_original_euler: Vector3, rot_order: EulerOrder) { + // This test: + // 1. Converts the rotation vector from deg to rad. + // 2. Converts euler to basis. + // 3. Converts the above basis back into euler. + // 4. Converts the above euler into basis again. + // 5. Compares the basis obtained in step 2 with the basis of step 4 + // + // The conversion "basis to euler", done in the step 3, may be different from + // the original euler, even if the final rotation are the same. + // This happens because there are more ways to represents the same rotation, + // both valid, using eulers. + // For this reason is necessary to convert that euler back to basis and finally + // compares it. + // + // In this way we can assert that both functions: basis to euler / euler to basis + // are correct. + + // Euler to rotation + let original_euler: Vector3 = deg_to_rad(deg_original_euler); + let to_rotation: Basis = Basis::from_euler( + rot_order, + original_euler.x, + original_euler.y, + original_euler.z, + ); + + // Euler from rotation + let euler_from_rotation: Vector3 = to_rotation.euler_angles(rot_order); + let rotation_from_computed_euler: Basis = Basis::from_euler( + rot_order, + euler_from_rotation.x, + euler_from_rotation.y, + euler_from_rotation.z, + ); + + let res: Basis = to_rotation.inverse() * rotation_from_computed_euler; + + assert!( + (res.col_x() - Vector3::new(1.0, 0.0, 0.0)).length() <= 0.1, + "Fail due to X {} with {deg_original_euler} using {rot_order:?}", + res.col_x() + ); + assert!( + (res.col_y() - Vector3::new(0.0, 1.0, 0.0)).length() <= 0.1, + "Fail due to Y {} with {deg_original_euler} using {rot_order:?}", + res.col_y() + ); + assert!( + (res.col_z() - Vector3::new(0.0, 0.0, 1.0)).length() <= 0.1, + "Fail due to Z {} with {deg_original_euler} using {rot_order:?}", + res.col_z() + ); + + // Double check `to_rotation` decomposing with XYZ rotation order. + let euler_xyz_from_rotation: Vector3 = to_rotation.euler_angles(EulerOrder::XYZ); + let rotation_from_xyz_computed_euler: Basis = Basis::from_euler( + EulerOrder::XYZ, + euler_xyz_from_rotation.x, + euler_xyz_from_rotation.y, + euler_xyz_from_rotation.z, + ); + + let res = to_rotation.inverse() * rotation_from_xyz_computed_euler; + + assert!( + (res.col_x() - Vector3::new(1.0, 0.0, 0.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to X {} with {deg_original_euler} using {rot_order:?}", + res.col_x(), + ); + assert!( + (res.col_y() - Vector3::new(0.0, 1.0, 0.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to Y {} with {deg_original_euler} using {rot_order:?}", + res.col_y(), + ); + assert!( + (res.col_z() - Vector3::new(0.0, 0.0, 1.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to Z {} with {deg_original_euler} using {rot_order:?}", + res.col_z(), + ); + } + + #[test] + fn consts_behavior_correct() { + let v = Vector3::new(1.0, 2.0, 3.0); + + assert!( + (Basis::IDENTITY * v).is_equal_approx(v), + "got {got}, expected {v}", + got = Basis::IDENTITY * v + ); + let u = Vector3::new(-v.x, v.y, v.z); + assert!( + (Basis::FLIP_X * v).is_equal_approx(u), + "got {got}, expected {u}", + got = Basis::FLIP_X * v + ); + let u = Vector3::new(v.x, -v.y, v.z); + assert!( + (Basis::FLIP_Y * v).is_equal_approx(u), + "got {got}, expected {u}", + got = Basis::FLIP_X * v + ); + let u = Vector3::new(v.x, v.y, -v.z); + assert!( + (Basis::FLIP_Z * v).is_equal_approx(u), + "got {got}, expected {u}", + got = Basis::FLIP_X * v + ); + } + + #[test] + fn basic_rotation_correct() { + assert!( + (Basis::from_axis_angle(Vector3::FORWARD, 0.0) * Vector3::RIGHT) + .is_equal_approx(Vector3::RIGHT), + "got {got}, expected {expected}", + got = Basis::from_axis_angle(Vector3::FORWARD, 0.0) * Vector3::RIGHT, + expected = Vector3::RIGHT + ); + assert!( + (Basis::from_axis_angle(Vector3::FORWARD, FRAC_PI_2) * Vector3::RIGHT) + .is_equal_approx(Vector3::DOWN), + "got {got}, expected {expected}", + got = Basis::from_axis_angle(Vector3::FORWARD, FRAC_PI_2) * Vector3::RIGHT, + expected = Vector3::DOWN + ); + assert!( + (Basis::from_axis_angle(Vector3::FORWARD, PI) * Vector3::RIGHT) + .is_equal_approx(Vector3::LEFT), + "got {got}, expected {expected}", + got = Basis::from_axis_angle(Vector3::FORWARD, PI) * Vector3::RIGHT, + expected = Vector3::LEFT + ); + assert!( + (Basis::from_axis_angle(Vector3::FORWARD, PI + FRAC_PI_2) * Vector3::RIGHT) + .is_equal_approx(Vector3::UP), + "got {got}, expected {expected}", + got = Basis::from_axis_angle(Vector3::FORWARD, PI + FRAC_PI_2) * Vector3::RIGHT, + expected = Vector3::UP + ); + } + + // Translated from Godot + #[test] + fn basis_euler_conversions() { + let euler_order_to_test: Vec = vec![ + EulerOrder::XYZ, + EulerOrder::XZY, + EulerOrder::YZX, + EulerOrder::YXZ, + EulerOrder::ZXY, + EulerOrder::ZYX, + ]; + + let vectors_to_test: Vec = 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); + } + } + } + + // Translated from Godot + #[test] + fn basis_finite_number_test() { + let x: Vector3 = Vector3::new(0.0, 1.0, 2.0); + let infinite: Vector3 = Vector3::new(f32::NAN, f32::NAN, f32::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." + ); + } +} diff --git a/godot-core/src/builtin/euler_order.rs b/godot-core/src/builtin/euler_order.rs deleted file mode 100644 index 042ff31d0..000000000 --- a/godot-core/src/builtin/euler_order.rs +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -use glam::EulerRot; -use godot_ffi as sys; -use sys::{ffi_methods, GodotFfi}; - -use super::glam_helpers::{GlamConv, GlamType}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(C)] -pub enum EulerOrder { - XYZ = 0, - XZY = 1, - YXZ = 2, - YZX = 3, - ZXY = 4, - ZYX = 5, -} - -impl GlamConv for EulerOrder { - type Glam = EulerRot; -} - -impl GlamType for EulerRot { - type Mapped = EulerOrder; - - fn to_front(&self) -> Self::Mapped { - match self { - EulerRot::XYZ => EulerOrder::XYZ, - EulerRot::XZY => EulerOrder::XZY, - EulerRot::YXZ => EulerOrder::YXZ, - EulerRot::YZX => EulerOrder::YZX, - EulerRot::ZXY => EulerOrder::ZXY, - EulerRot::ZYX => EulerOrder::ZYX, - } - } - - fn from_front(mapped: &Self::Mapped) -> Self { - match mapped { - EulerOrder::XYZ => EulerRot::XYZ, - EulerOrder::XZY => EulerRot::XZY, - EulerOrder::YXZ => EulerRot::YXZ, - EulerOrder::YZX => EulerRot::YZX, - EulerOrder::ZXY => EulerRot::ZXY, - EulerOrder::ZYX => EulerRot::ZYX, - } - } -} - -impl GodotFfi for EulerOrder { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } -} diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 96fe16a7f..656f38bc8 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -39,7 +39,6 @@ pub use array_inner::{Array, TypedArray}; pub use basis::*; pub use color::*; pub use dictionary_inner::Dictionary; -pub use euler_order::*; pub use math::*; pub use node_path::*; pub use others::*; @@ -87,7 +86,6 @@ mod dictionary_inner; mod basis; mod color; -mod euler_order; mod glam_helpers; mod math; mod node_path; diff --git a/godot-core/src/builtin/quaternion.rs b/godot-core/src/builtin/quaternion.rs index 93c7258c8..c1580dd1f 100644 --- a/godot-core/src/builtin/quaternion.rs +++ b/godot-core/src/builtin/quaternion.rs @@ -11,7 +11,7 @@ use sys::{ffi_methods, GodotFfi}; use crate::builtin::glam_helpers::{GlamConv, GlamType}; use crate::builtin::{inner, math::*, vector3::*}; -use super::EulerOrder; +use super::{Basis, EulerOrder}; #[derive(Copy, Clone, Debug, PartialEq)] #[repr(C)] @@ -100,9 +100,7 @@ impl Quaternion { // TODO: Figure out how godot actually treats "order", then make a match/if chain pub fn get_euler(self, order: EulerOrder) -> Vector3 { - let vt = self.glam(|quat| quat.to_euler(order.to_glam())); - - Vector3::new(vt.0, vt.1, vt.2) + Basis::from_quat(self).euler_angles(order) } pub fn inverse(self) -> Self { diff --git a/itest/rust/src/basis_test.rs b/itest/rust/src/basis_test.rs new file mode 100644 index 000000000..23c77eac2 --- /dev/null +++ b/itest/rust/src/basis_test.rs @@ -0,0 +1,135 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::itest; +use godot::prelude::{inner::InnerBasis, *}; + +const TEST_BASIS: Basis = Basis::from_rows( + Vector3::new(0.942155, -0.270682, 0.197677), + Vector3::new(0.294044, 0.950564, -0.099833), + Vector3::new(-0.160881, 0.152184, 0.97517), +); + +pub(crate) fn run() -> bool { + let mut ok = true; + ok &= basis_multiply_same(); + ok &= basis_euler_angles_same(); + + ok +} + +#[itest] +fn basis_multiply_same() { + let rust_res = TEST_BASIS * Basis::IDENTITY; + let godot_res = TEST_BASIS + .to_variant() + .evaluate(&Basis::IDENTITY.to_variant(), VariantOperator::Multiply) + .unwrap() + .to::(); + assert!( + (rust_res).is_equal_approx(&godot_res), + "got = {rust_res}, expected = {godot_res}" + ); + + let rhs = Basis::from_axis_angle(Vector3::new(1.0, 2.0, 3.0).normalized(), 0.5); + let rust_res = TEST_BASIS * rhs; + let godot_res = TEST_BASIS + .to_variant() + .evaluate(&rhs.to_variant(), VariantOperator::Multiply) + .unwrap() + .to::(); + assert!( + (rust_res).is_equal_approx(&godot_res), + "got = {rust_res}, expected = {godot_res}" + ); +} + +#[itest] +fn basis_euler_angles_same() { + let euler_order_to_test: Vec = vec![ + EulerOrder::XYZ, + EulerOrder::XZY, + EulerOrder::YZX, + EulerOrder::YXZ, + EulerOrder::ZXY, + EulerOrder::ZYX, + ]; + + let vectors_to_test: Vec = 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.into_iter() { + for vector in vectors_to_test.iter() { + let vector = deg_to_rad(*vector); + let rust_basis = Basis::from_euler(order, vector.x, vector.y, vector.z); + let godot_basis = InnerBasis::from_euler(vector, order as i64); + assert!( + (rust_basis).is_equal_approx(&godot_basis), + "got = {rust_basis}, expected = {godot_basis}" + ); + } + } +} + +fn deg_to_rad(rotation: Vector3) -> Vector3 { + Vector3::new( + rotation.x.to_radians(), + rotation.y.to_radians(), + rotation.z.to_radians(), + ) +} diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 010e92235..60d62892a 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -11,6 +11,7 @@ use std::panic::UnwindSafe; mod array_test; mod base_test; +mod basis_test; mod builtin_test; mod codegen_test; mod dictionary_test; @@ -31,6 +32,7 @@ fn run_tests() -> bool { let mut ok = true; ok &= array_test::run(); ok &= base_test::run(); + ok &= basis_test::run(); ok &= builtin_test::run(); ok &= codegen_test::run(); ok &= dictionary_test::run(); diff --git a/itest/rust/src/variant_test.rs b/itest/rust/src/variant_test.rs index ff1c497b0..1211baeb7 100644 --- a/itest/rust/src/variant_test.rs +++ b/itest/rust/src/variant_test.rs @@ -10,11 +10,17 @@ use godot::builtin::{ }; use godot::engine::Node3D; use godot::obj::InstanceId; -use godot::prelude::{Array, Dictionary, VariantConversionError}; +use godot::prelude::{Array, Basis, Dictionary, VariantConversionError}; use godot::sys::{GodotFfi, VariantOperator, VariantType}; use std::cmp::Ordering; use std::fmt::{Debug, Display}; +const TEST_BASIS: Basis = Basis::from_rows( + Vector3::new(1.0, 2.0, 3.0), + Vector3::new(4.0, 5.0, 6.0), + Vector3::new(7.0, 8.0, 9.0), +); + pub fn run() -> bool { let mut ok = true; ok &= variant_nil(); @@ -73,6 +79,9 @@ fn variant_conversions() { let str_val = "abcdefghijklmnop"; let back = String::from_variant(&str_val.to_variant()); assert_eq!(str_val, back.as_str()); + + // basis + roundtrip(TEST_BASIS); } #[itest] @@ -93,6 +102,9 @@ fn variant_get_type() { let variant = gstr("hello").to_variant(); assert_eq!(variant.get_type(), VariantType::String); + + let variant = TEST_BASIS.to_variant(); + assert_eq!(variant.get_type(), VariantType::Basis) } #[itest] From 139a79d8fa4d6d049cc1cbd2648943b6d177fbd2 Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Sat, 18 Feb 2023 18:00:46 +0100 Subject: [PATCH 04/15] Transform2D: cleanup + document + test --- godot-core/src/builtin/basis.rs | 153 ++++---- godot-core/src/builtin/math.rs | 25 ++ godot-core/src/builtin/transform2d.rs | 505 ++++++++++++++++++++------ 3 files changed, 499 insertions(+), 184 deletions(-) diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index 656077d39..eb6683886 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -8,11 +8,8 @@ use std::{f32::consts::FRAC_PI_2, fmt::Display, ops::*}; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; -use super::{ - glam_helpers::{GlamConv, GlamType}, - math::*, - Quaternion, Vector3, -}; +use super::glam_helpers::{GlamConv, GlamType}; +use super::{math::*, Quaternion, Vector3}; use glam::f32::Mat3; @@ -56,7 +53,7 @@ impl Basis { /// _Godot equivalent: `Basis.FLIP_Z`_ pub const FLIP_Z: Self = Self::from_diagonal(1.0, 1.0, -1.0); - /// Create a diagonal matrix from the given vector. + /// Create a diagonal matrix from the given values. /// /// _Godot equivalent: `Basis.from_scale(Vector3 scale)` #[must_use] @@ -147,7 +144,7 @@ impl Basis { /// orthonormalized. The `target` and `up` vectors cannot be zero, and /// cannot be parallel to each other. /// - /// _Godot equivalent: `Basis.looking_at(Vector3 target, Vector3 up)`_ + /// _Godot equivalent: `Basis.looking_at()`_ #[must_use] pub fn new_looking_at(target: Vector3, up: Vector3) -> Self { super::inner::InnerBasis::looking_at(target, up) @@ -161,7 +158,7 @@ impl Basis { /// Creates a [`Quaternion`] representing the same rotation as this basis. /// - /// _Godot equivalent: `Basis(Quaternion from)`_ + /// _Godot equivalent: `Basis()`_ #[must_use] pub fn to_quat(&self) -> Quaternion { glam::Quat::from_mat3(&self.to_glam()).to_front() @@ -204,7 +201,7 @@ impl Basis { /// /// The order of the angles are given by `order`. /// - /// _Godot equivalent: `Basis.get_euler(int order)`_ + /// _Godot equivalent: `Basis.get_euler()`_ #[must_use] pub fn euler_angles(&self, order: EulerOrder) -> Vector3 { // Translated from "Basis::get_euler" in @@ -222,30 +219,30 @@ impl Basis { // cz*sx*sy+cx*sz cx*cz-sx*sy*sz -cy*sx // -cx*cz*sy+sx*sz cz*sx+cx*sy*sz cx*cy - let sy: f32 = self[0][2]; + let sy: f32 = self[0].z; if sy < 1.0 - CMP_EPSILON { if sy > -(1.0 - CMP_EPSILON) { // is this a pure Y rotation? - if self[1][0] == 0.0 - && self[0][1] == 0.0 - && self[1][2] == 0.0 - && self[2][1] == 0.0 - && self[1][1] == 1.0 + if self[1].x == 0.0 + && self[0].y == 0.0 + && self[1].z == 0.0 + && self[2].y == 0.0 + && self[1].y == 1.0 { // return the simplest form (human friendlier in editor and scripts) - Vector3::new(0.0, f32::atan2(self[0][2], self[0][0]), 0.0) + Vector3::new(0.0, f32::atan2(self[0].z, self[0].x), 0.0) } else { Vector3::new( - f32::atan2(-self[1][2], self[2][2]), + f32::atan2(-self[1].z, self[2].z), f32::asin(sy), - f32::atan2(-self[0][1], self[0][0]), + f32::atan2(-self[0].y, self[0].x), ) } } else { - Vector3::new(f32::atan2(self[2][1], self[1][1]), -FRAC_PI_2, 0.0) + Vector3::new(f32::atan2(self[2].y, self[1].y), -FRAC_PI_2, 0.0) } } else { - Vector3::new(f32::atan2(self[2][1], self[1][1]), FRAC_PI_2, 0.0) + Vector3::new(f32::atan2(self[2].y, self[1].y), FRAC_PI_2, 0.0) } } EulerOrder::XZY => { @@ -256,21 +253,21 @@ impl Basis { // sx*sy+cx*cy*sz cx*cz cx*sz*sy-cy*sx // cy*sx*sz cz*sx cx*cy+sx*sz*sy - let sz: f32 = self[0][1]; + let sz: f32 = self[0].y; if sz < (1.0 - CMP_EPSILON) { if sz > -(1.0 - CMP_EPSILON) { Vector3::new( - f32::atan2(self[2][1], self[1][1]), - f32::atan2(self[0][2], self[0][0]), + f32::atan2(self[2].y, self[1].y), + f32::atan2(self[0].z, self[0].x), f32::asin(-sz), ) } else { // It's -1 - Vector3::new(-f32::atan2(self[1][2], self[2][2]), 0.0, FRAC_PI_2) + Vector3::new(-f32::atan2(self[1].z, self[2].z), 0.0, FRAC_PI_2) } } else { // It's 1 - Vector3::new(-f32::atan2(self[1][2], self[2][2]), 0.0, -FRAC_PI_2) + Vector3::new(-f32::atan2(self[1].z, self[2].z), 0.0, -FRAC_PI_2) } } EulerOrder::YXZ => { @@ -281,33 +278,33 @@ impl Basis { // cx*sz cx*cz -sx // cy*sx*sz-cz*sy cy*cz*sx+sy*sz cy*cx - let m12: f32 = self[1][2]; + let m12: f32 = self[1].z; if m12 < (1.0 - CMP_EPSILON) { if m12 > -(1.0 - CMP_EPSILON) { // is this a pure X rotation? - if self[1][0] == 0.0 - && self[0][1] == 0.0 - && self[0][2] == 0.0 - && self[2][0] == 0.0 - && self[0][0] == 1.0 + if self[1].x == 0.0 + && self[0].y == 0.0 + && self[0].z == 0.0 + && self[2].x == 0.0 + && self[0].x == 1.0 { // return the simplest form (human friendlier in editor and scripts) - Vector3::new(f32::atan2(-m12, self[1][1]), 0.0, 0.0) + Vector3::new(f32::atan2(-m12, self[1].y), 0.0, 0.0) } else { Vector3::new( f32::asin(-m12), - f32::atan2(self[0][2], self[2][2]), - f32::atan2(self[1][0], self[1][1]), + f32::atan2(self[0].z, self[2].z), + f32::atan2(self[1].x, self[1].y), ) } } else { // m12 == -1 - Vector3::new(FRAC_PI_2, f32::atan2(self[0][1], self[0][0]), 0.0) + Vector3::new(FRAC_PI_2, f32::atan2(self[0].y, self[0].x), 0.0) } } else { // m12 == 1 - Vector3::new(-FRAC_PI_2, -f32::atan2(self[0][1], self[0][0]), 0.0) + Vector3::new(-FRAC_PI_2, -f32::atan2(self[0].y, self[0].x), 0.0) } } EulerOrder::YZX => { @@ -318,21 +315,21 @@ impl Basis { // sz cz*cx -cz*sx // -cz*sy cy*sx+cx*sy*sz cy*cx-sy*sz*sx - let sz: f32 = self[1][0]; + let sz: f32 = self[1].x; if sz < (1.0 - CMP_EPSILON) { if sz > -(1.0 - CMP_EPSILON) { Vector3::new( - f32::atan2(-self[1][2], self[1][1]), - f32::atan2(-self[2][0], self[0][0]), + f32::atan2(-self[1].z, self[1].y), + f32::atan2(-self[2].x, self[0].x), f32::asin(sz), ) } else { // It's -1 - Vector3::new(f32::atan2(self[2][1], self[2][2]), 0.0, -FRAC_PI_2) + Vector3::new(f32::atan2(self[2].y, self[2].z), 0.0, -FRAC_PI_2) } } else { // It's 1 - Vector3::new(f32::atan2(self[2][1], self[2][2]), 0.0, FRAC_PI_2) + Vector3::new(f32::atan2(self[2].y, self[2].z), 0.0, FRAC_PI_2) } } EulerOrder::ZXY => { @@ -342,21 +339,21 @@ impl Basis { // rot = cz*cy-sz*sx*sy -cx*sz cz*sy+cy*sz*sx // cy*sz+cz*sx*sy cz*cx sz*sy-cz*cy*sx // -cx*sy sx cx*cy - let sx: f32 = self[2][1]; + let sx: f32 = self[2].y; if sx < (1.0 - CMP_EPSILON) { if sx > -(1.0 - CMP_EPSILON) { Vector3::new( f32::asin(sx), - f32::atan2(-self[2][0], self[2][2]), - f32::atan2(-self[0][1], self[1][1]), + f32::atan2(-self[2].x, self[2].z), + f32::atan2(-self[0].y, self[1].y), ) } else { // It's -1 - Vector3::new(-FRAC_PI_2, f32::atan2(self[0][2], self[0][0]), 0.0) + Vector3::new(-FRAC_PI_2, f32::atan2(self[0].z, self[0].x), 0.0) } } else { // It's 1 - Vector3::new(FRAC_PI_2, f32::atan2(self[0][2], self[0][0]), 0.0) + Vector3::new(FRAC_PI_2, f32::atan2(self[0].z, self[0].x), 0.0) } } EulerOrder::ZYX => { @@ -366,21 +363,21 @@ impl Basis { // rot = cz*cy cz*sy*sx-cx*sz sz*sx+cz*cx*cy // cy*sz cz*cx+sz*sy*sx cx*sz*sy-cz*sx // -sy cy*sx cy*cx - let sy: f32 = self[2][0]; + let sy: f32 = self[2].x; if sy < (1.0 - CMP_EPSILON) { if sy > -(1.0 - CMP_EPSILON) { Vector3::new( - f32::atan2(self[2][1], self[2][2]), + f32::atan2(self[2].y, self[2].z), f32::asin(-sy), - f32::atan2(self[1][0], self[0][0]), + f32::atan2(self[1].x, self[0].x), ) } else { // It's -1 - Vector3::new(0.0, FRAC_PI_2, -f32::atan2(self[0][1], self[1][1])) + Vector3::new(0.0, FRAC_PI_2, -f32::atan2(self[0].y, self[1].y)) } } else { // It's 1 - Vector3::new(0.0, -FRAC_PI_2, -f32::atan2(self[0][1], self[1][1])) + Vector3::new(0.0, -FRAC_PI_2, -f32::atan2(self[0].y, self[1].y)) } } } @@ -396,7 +393,7 @@ impl Basis { /// Introduce an additional scaling specified by the given 3D scaling factor. /// - /// _Godot equivalent: `Basis.scaled(Vector3 scale)`_ + /// _Godot equivalent: `Basis.scaled()`_ #[must_use] pub fn scaled(self, scale: Vector3) -> Self { self * Self::from_diagonal(scale.x, scale.y, scale.z) @@ -416,11 +413,7 @@ impl Basis { #[must_use] pub fn transposed(self) -> Self { Self { - rows: [ - Vector3::new(self[0][0], self[1][0], self[2][0]), - Vector3::new(self[0][1], self[1][1], self[2][1]), - Vector3::new(self[0][2], self[1][2], self[2][2]), - ], + rows: [self.col_x(), self.col_y(), self.col_z()], } } @@ -453,7 +446,7 @@ impl Basis { /// Introduce an additional rotation around the given `axis` by `angle` /// (in radians). The axis must be a normalized vector. /// - /// _Godot equivalent: `Basis.rotated(Vector3 axis, float angle)`_ + /// _Godot equivalent: `Basis.rotated()`_ #[must_use] pub fn rotated(self, axis: Vector3, angle: f32) -> Self { self * Self::from_axis_angle(axis, angle) @@ -462,7 +455,7 @@ impl Basis { /// Assuming that the matrix is a proper rotation matrix, slerp performs /// a spherical-linear interpolation with another rotation matrix. /// - /// _Godot equivalent: `Basis.slerp(Basis to, float weight)`_ + /// _Godot equivalent: `Basis.slerp()`_ #[must_use] pub fn slerp(self, other: Self, weight: f32) -> Self { let from = self.to_quat(); @@ -479,7 +472,7 @@ impl Basis { /// Transposed dot product with the X axis (column) of the matrix. /// - /// _Godot equivalent: `Basis.tdotx(Vector3 with)`_ + /// _Godot equivalent: `Basis.tdotx()`_ #[must_use] pub fn tdotx(&self, with: Vector3) -> f32 { self.col_x().dot(with) @@ -487,7 +480,7 @@ impl Basis { /// Transposed dot product with the Y axis (column) of the matrix. /// - /// _Godot equivalent: `Basis.tdoty(Vector3 with)`_ + /// _Godot equivalent: `Basis.tdoty()`_ #[must_use] pub fn tdoty(&self, with: Vector3) -> f32 { self.col_y().dot(with) @@ -495,7 +488,7 @@ impl Basis { /// Transposed dot product with the Z axis (column) of the matrix. /// - /// _Godot equivalent: `Basis.tdotz(Vector3 with)`_ + /// _Godot equivalent: `Basis.tdotz()`_ #[must_use] pub fn tdotz(&self, with: Vector3) -> f32 { self.col_z().dot(with) @@ -525,14 +518,14 @@ impl Basis { /// _Godot equivalent: `Basis.x`_ #[must_use] pub fn col_x(&self) -> Vector3 { - Vector3::new(self[0][0], self[1][0], self[2][0]) + Vector3::new(self[0].x, self[1].x, self[2].x) } /// Set the values of the first column of the matrix. pub fn set_col_x(&mut self, col: Vector3) { - self[0][0] = col.x; - self[1][0] = col.y; - self[2][0] = col.z; + self[0].x = col.x; + self[1].x = col.y; + self[2].x = col.z; } /// Returns the second column of the matrix, @@ -540,14 +533,14 @@ impl Basis { /// _Godot equivalent: `Basis.y`_ #[must_use] pub fn col_y(&self) -> Vector3 { - Vector3::new(self[0][1], self[1][1], self[2][1]) + Vector3::new(self[0].y, self[1].y, self[2].y) } /// Set the values of the second column of the matrix. pub fn set_col_y(&mut self, col: Vector3) { - self[0][1] = col.x; - self[1][1] = col.y; - self[2][1] = col.z; + self[0].y = col.x; + self[1].y = col.y; + self[2].y = col.z; } /// Returns the third column of the matrix, @@ -555,14 +548,14 @@ impl Basis { /// _Godot equivalent: `Basis.z`_ #[must_use] pub fn col_z(&self) -> Vector3 { - Vector3::new(self[0][2], self[1][2], self[2][2]) + Vector3::new(self[0].z, self[1].z, self[2].z) } /// Set the values of the third column of the matrix. pub fn set_col_z(&mut self, col: Vector3) { - self[0][2] = col.x; - self[1][2] = col.y; - self[2][2] = col.z; + self[0].z = col.x; + self[1].z = col.y; + self[2].z = col.z; } } @@ -571,21 +564,19 @@ impl Display for Basis { // Godot output: // [X: (1, 0, 0), Y: (0, 1, 0), Z: (0, 0, 1)] // Where X,Y,Z are the columns - let Vector3 { + let [Vector3 { x: x1, y: x2, z: x3, - } = self.col_x(); - let Vector3 { + }, Vector3 { x: y1, y: y2, z: y3, - } = self.col_y(); - let Vector3 { + }, Vector3 { x: z1, y: z2, z: z3, - } = self.col_z(); + }] = self.to_cols(); f.write_fmt(format_args!( "[X: ({x1}, {x2}, {x3}), Y: ({y1}, {y2}, {y3}), Z: ({z1}, {z2}, {z3})]" @@ -625,6 +616,7 @@ impl Index for Basis { type Output = Vector3; /// Indexes into the basis by rows. + #[inline] fn index(&self, index: usize) -> &Self::Output { &self.rows[index] } @@ -632,6 +624,7 @@ impl Index for Basis { impl IndexMut for Basis { /// Indexes into the basis by rows. + #[inline] fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.rows[index] } diff --git a/godot-core/src/builtin/math.rs b/godot-core/src/builtin/math.rs index 880c0cb78..3f4eb142b 100644 --- a/godot-core/src/builtin/math.rs +++ b/godot-core/src/builtin/math.rs @@ -23,6 +23,31 @@ pub fn is_equal_approx(a: f32, b: f32) -> bool { (a - b).abs() < tolerance } +#[macro_export] +macro_rules! assert_eq_approx { + ($a:expr, $b:expr, $func:expr $(,)?) => { + match ($a, $b) { + (a, b) => { + assert!(($func)(a,b), "\n left: {:?},\n right = {:?}", $a, $b); + } + } + }; + ($a:expr, $b:expr, $func:expr, $($t:tt)+) => { + match ($a, $b) { + (a, b) => { + assert!(($func)(a,b), "\n left: {:?},\n right = {:?},\n{}", $a, $b, format_args!($($t)+)); + } + } + }; +} + +#[macro_export] +macro_rules! assert_ne_approx { + ($a:expr, $b:expr, $func:expr $(, $($t:tt)*)?) => { + assert_eq_approx!($a, $b, |a,b| !($func)(a,b) $(, $($t)*)?) + }; +} + pub fn is_zero_approx(s: f32) -> bool { s.abs() < CMP_EPSILON } diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs index 801687095..aca8d1bff 100644 --- a/godot-core/src/builtin/transform2d.rs +++ b/godot-core/src/builtin/transform2d.rs @@ -17,26 +17,65 @@ use super::{ use glam::f32::Affine2; use glam::f32::Mat2; +/// Affine 2D transform (2x3 matrix). +/// +/// Represents transformations such as translation, rotation, or scaling. +/// +/// Expressed as a 2x3 matrix, this transform consists of a 2d rotation matrix +/// `basis` containing two (column) vectors, as well as an origin `origin`: +/// ```text +/// [ basis[0].x basis[1].x origin.x ] +/// [ basis[0].y basis[1].y origin.y ] +/// ``` +/// +/// For methods that don't take translation into account, see [`Basis2D`]. #[derive(Debug, Default, Clone, Copy, PartialEq)] #[repr(C)] pub struct Transform2D { + /// The basis of the transform. + /// + /// This is equivalent to the `x` and `y` fields from godot. pub basis: Basis2D, + + /// The origin of the transform. The coordinate space defined by this transform + /// starts at this point. + /// + /// _Godot equivalent: `Transform2D.origin`_ pub origin: Vector2, } impl Transform2D { + /// The identity transform, with no translation, rotation or scaling + /// applied. When applied to other data structures, `IDENTITY` performs no + /// transformation. + /// + /// _Godot equivalent: `Transform2D.IDENTITY`_ pub const IDENTITY: Self = Self::new(Basis2D::IDENTITY, Vector2::ZERO); + + /// The `Transform2D` that will flip something along its x axis. + /// + /// _Godot equivalent: `Transform2D.FLIP_X`_ pub const FLIP_X: Self = Self::new(Basis2D::FLIP_X, Vector2::ZERO); + + /// The `Transform2D` that will flip something along its y axis. + /// + /// _Godot equivalent: `Transform2D.FLIP_Y`_ pub const FLIP_Y: Self = Self::new(Basis2D::FLIP_Y, Vector2::ZERO); + /// Create a new `Transform2D` from the given basis and origin. pub const fn new(basis: Basis2D, origin: Vector2) -> Self { Self { basis, origin } } + /// Create a new `Transform2D` which will rotate by the given angle. pub fn from_angle(angle: f32) -> Self { Self::from_angle_origin(angle, Vector2::ZERO) } + /// Create a new `Transform2D` which will rotate by `angle` and translate + /// by `origin`. + /// + /// _Godot equivalent: `Transform2D(float rotation, Vector2 position)`_ pub fn from_angle_origin(angle: f32, origin: Vector2) -> Self { Self { basis: Basis2D::from_angle(angle), @@ -44,40 +83,58 @@ impl Transform2D { } } + /// Create a new `Transform2D` which will rotate by `angle`, scale by + /// `scale`, skew by `skew` and translate by `origin`. + /// + /// _Godot equivalent: `Transform2D(float rotation, Vector2 scale, float skew, Vector2 position)`_ pub fn from_angle_scale_skew_origin( angle: f32, scale: Vector2, skew: f32, origin: Vector2, ) -> Self { - Self { - basis: Basis2D { - cols: [ - // Translated from Godot's implementation - Vector2::new(angle.cos() * scale.x, (angle + skew).cos() * scale.y), - Vector2::new(-(angle + skew).sin() * scale.y, angle.sin() * scale.x), - ], - }, - origin, - } + // Translated from Godot's implementation + let mut basis = Basis2D::IDENTITY; + basis[0].x = angle.cos() * scale.x; + basis[1].y = (angle + skew).cos() * scale.y; + basis[1].x = -(angle + skew).sin() * scale.y; + basis[0].y = angle.sin() * scale.x; + Self { basis, origin } } + /// Returns the inverse of the transform, under the assumption that the + /// transformation is composed of rotation, scaling and translation. + /// + /// _Godot equivalent: `Transform2D.affine_inverse()`_ pub fn affine_inverse(&self) -> Self { self.glam(|aff| aff.inverse()) } + /// Returns the transform's rotation (in radians). + /// + /// _Godot equivalent: `Transform2D.get_rotation()`_ pub fn rotation(&self) -> f32 { self.basis.rotation() } + /// Returns the transform's scale. + /// + /// _Godot equivalent: `Transform2D.get_scale()`_ pub fn scale(&self) -> Vector2 { self.basis.scale() } + /// Returns the transform's skew (in radians). + /// + /// _Godot equivalent: `Transform2D.get_skew()`_ pub fn skew(&self) -> f32 { self.basis.skew() } + /// Returns a transform interpolated between this transform and another by + /// a given `weight` (on the range of 0.0 to 1.0). + /// + /// _Godot equivalent: `Transform2D.interpolate_with()`_ pub fn interpolate_with(&self, other: &Self, weight: f32) -> Self { Self::from_angle_scale_skew_origin( lerp_angle(self.rotation(), other.rotation(), weight), @@ -87,51 +144,102 @@ impl Transform2D { ) } + /// Returns `true` if this transform and transform are approximately equal, + /// by calling `is_equal_approx` on each component. + /// + /// _Godot equivalent: `Transform2D.is_equal_approx()`_ pub fn is_equal_approx(&self, other: &Self) -> bool { self.basis.is_equal_approx(&other.basis) && self.origin.is_equal_approx(other.origin) } + /// Returns `true` if this transform is finite, by calling + /// [`Basis2D::is_finite()`] and [`Vector2::is_finite()`]. + /// + /// _Godot equivalent: `Transform2D.is_finite()`_ pub fn is_finite(&self) -> bool { self.basis.is_finite() && self.origin.is_finite() } - pub fn orthonormalize(&self) -> Self { + /// Returns the transform with the basis orthogonal (90 degrees), and + /// normalized axis vectors (scale of 1 or -1). + /// + /// _Godot equivalent: `Transform2D.orthonormalized()`_ + pub fn orthonormalized(&self) -> Self { Self { - basis: self.basis.orthonormalize(), + basis: self.basis.orthonormalized(), origin: self.origin, } } - pub fn rotated(&self, angle: f32) -> Self { - *self * Self::from_angle(angle) - } - - pub fn rotated_local(&self, angle: f32) -> Self { - Self::from_angle(angle) * *self - } - - pub fn scaled(&self, scale: Vector2) -> Self { + /// Returns a copy of the transform rotated by the given `angle` (in radians). + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding rotation transform `R` from the left, i.e., `R * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.rotated()`_ + pub fn rotated(self, angle: f32) -> Self { + Self::from_angle(angle) * self + } + + /// Returns a copy of the transform rotated by the given `angle` (in radians). + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding rotation transform `R` from the right, i.e., `X * R`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.rotated_local()`_ + pub fn rotated_local(self, angle: f32) -> Self { + self * Self::from_angle(angle) + } + + /// Returns a copy of the transform scaled by the given scale factor. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding scaling transform `S` from the left, i.e., `S * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.scaled()`_ + pub fn scaled(self, scale: Vector2) -> Self { + let mut basis = self.basis; + basis.set_row_x(basis.row_x() * scale.x); + basis.set_row_y(basis.row_y() * scale.y); Self { - basis: self.basis.scaled(scale), + basis, origin: self.origin * scale, } } - pub fn scaled_local(&self, scale: Vector2) -> Self { + /// Returns a copy of the transform scaled by the given scale factor. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding scaling transform `S` from the right, i.e., `X * S`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.scaled_local()`_ + pub fn scaled_local(self, scale: Vector2) -> Self { Self { basis: self.basis.scaled(scale), origin: self.origin, } } - pub fn translated(&self, offset: Vector2) -> Self { + /// Returns a copy of the transform translated by the given offset. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding translation transform `T` from the left, i.e., `T * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.translated()`_ + pub fn translated(self, offset: Vector2) -> Self { Self { basis: self.basis, origin: self.origin + offset, } } - pub fn translated_local(&self, offset: Vector2) -> Self { + /// Returns a copy of the transform translated by the given offset. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding translation transform `T` from the right, i.e., `X * T`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.translated()`_ + pub fn translated_local(self, offset: Vector2) -> Self { Self { basis: self.basis, origin: self.origin + (self.basis * offset), @@ -142,6 +250,7 @@ impl Transform2D { impl Index for Transform2D { type Output = Vector2; + #[inline] fn index(&self, index: usize) -> &Self::Output { match index { 0 | 1 => &self.basis[index], @@ -152,6 +261,7 @@ impl Index for Transform2D { } impl IndexMut for Transform2D { + #[inline] fn index_mut(&mut self, index: usize) -> &mut Self::Output { match index { 0 | 1 => &mut self.basis[index], @@ -214,31 +324,33 @@ impl GodotFfi for Transform2D { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } +/// A 2x2 matrix, typically used as an orthogonal basis for [`Transform2D`]. +/// +/// Indexing into a `Basis2D` is done in a column-major order, meaning that +/// `basis[0]` is the first basis-vector. +/// +/// This has no direct equivalent in Godot, but is the same as the `x` and `y` +/// vectors from a `Transform2D`. #[derive(Debug, Clone, Copy, PartialEq)] #[repr(C)] pub struct Basis2D { + /// The columns of the matrix. pub cols: [Vector2; 2], } -impl Index for Basis2D { - type Output = Vector2; - - fn index(&self, index: usize) -> &Self::Output { - &self.cols[index] - } -} - -impl IndexMut for Basis2D { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.cols[index] - } -} - impl Basis2D { + /// The identity basis, with no rotation or scaling applied. pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0); + + /// The basis that will flip something along the X axis when used in a + /// transformation. pub const FLIP_X: Self = Self::from_diagonal(-1.0, 1.0); + + /// The basis that will flip something along the X axis when used in a + /// transformation. pub const FLIP_Y: Self = Self::from_diagonal(1.0, -1.0); + /// Create a diagonal matrix from the given values. #[must_use] pub const fn from_diagonal(x: f32, y: f32) -> Self { Self { @@ -246,84 +358,61 @@ impl Basis2D { } } - #[must_use] - pub const fn new(cols: [Vector2; 2]) -> Self { - Self { cols } - } - + /// Create a new basis from 2 basis vectors. #[must_use] pub const fn from_cols(x: Vector2, y: Vector2) -> Self { Self { cols: [x, y] } } + /// Create a new basis from 2 row vectors. These are *not* basis vectors. #[must_use] pub const fn from_rows(x: Vector2, y: Vector2) -> Self { - Self::from_cols(x, y).transpose() + Self::from_cols(x, y).transposed() } + /// Create a `Basis2D` from an angle. #[must_use] - pub const fn from_cols_array(cols: &[f32; 4]) -> Self { - let [x0, y0, x1, y1] = cols; - Self { - cols: [Vector2::new(*x0, *y0), Vector2::new(*x1, *y1)], - } - } - - #[must_use] - pub const fn from_cols_array_2d(cols: &[[f32; 2]; 2]) -> Self { - let [[x0, y0], [x1, y1]] = cols; - Self { - cols: [Vector2::new(*x0, *y0), Vector2::new(*x1, *y1)], - } - } - - #[must_use] - pub const fn to_rows_array(&self) -> [f32; 4] { - let [Vector2 { x: x0, y: y0 }, Vector2 { x: x1, y: y1 }] = self.cols; - [x0, y0, x1, y1] - } - - #[must_use] - pub const fn to_rows_array_2d(&self) -> [[f32; 2]; 2] { - let [Vector2 { x: x0, y: y0 }, Vector2 { x: x1, y: y1 }] = self.cols; - [[x0, y0], [x1, y1]] - } - - #[must_use] - pub fn from_scale(scale: Vector2) -> Self { - Mat2::from_scale_angle(scale.to_glam(), 0.0).to_front() + pub fn from_angle(angle: f32) -> Self { + Mat2::from_angle(angle).to_front() } + /// Returns the scale of the matrix. #[must_use] - pub fn get_scale(&self) -> Vector2 { + pub fn scale(&self) -> Vector2 { let det = self.determinant(); let det_sign = if det < 0.0 { -1.0 } else { 1.0 }; Vector2::new(self[0].length(), self[1].length()) * det_sign } - #[must_use] - pub fn from_angle(angle: f32) -> Self { - Mat2::from_angle(angle).to_front() + /// Introduces an additional scaling. + pub fn scaled(&self, scale: Vector2) -> Self { + Self { + cols: [self[0] * scale.x, self[1] * scale.y], + } } + /// Returns the determinant of the matrix. #[must_use] pub fn determinant(&self) -> f32 { self.glam(|mat| mat.determinant()) } + /// Returns the inverse of the matrix. #[must_use] pub fn inverse(self) -> Self { self.glam(|mat| mat.inverse()) } + /// Returns whether each component is finite. #[must_use] pub fn is_finite(&self) -> bool { self.glam(|mat| mat.is_finite()) } + /// Returns the orthonormalized version of the basis. #[must_use] - pub fn orthonormalize(&self) -> Self { + pub fn orthonormalized(&self) -> Self { assert!( is_equal_approx(self.determinant(), 0.0), "Determinant should not be zero." @@ -340,33 +429,20 @@ impl Basis2D { Self::from_cols(x, y) } + /// Introduces an additional rotation. #[must_use] - pub fn rotate(&self, angle: f32) -> Self { - *self * Self::from_angle(angle) - } - - #[must_use] - pub fn tdotx(&self, with: Vector2) -> f32 { - self[0].dot(with) - } - - #[must_use] - pub fn tdoty(&self, with: Vector2) -> f32 { - self[1].dot(with) + pub fn rotated(self, angle: f32) -> Self { + self * Self::from_angle(angle) } + /// Returns the rotation of the matrix #[must_use] pub fn rotation(&self) -> f32 { // Translated from Godot - self[0].y.atan2(self[0].x) - } - - #[must_use] - pub fn scale(&self) -> Vector2 { - let det_sign = self.determinant().signum(); - Vector2::new(self[0].length(), self[1].length() * det_sign) + f32::atan2(self[0].y, self[0].x) } + /// Returns the skew of the matrix #[must_use] pub fn skew(&self) -> f32 { // Translated from Godot @@ -378,11 +454,14 @@ impl Basis2D { - PI * 0.5 } + /// Returns `true` if this basis and `other` are approximately equal, + /// by calling `is_equal_approx` on each column. pub fn is_equal_approx(&self, other: &Self) -> bool { self[0].is_equal_approx(other[0]) && self[1].is_equal_approx(other[1]) } - pub const fn transpose(self) -> Self { + /// Returns the transposed version of the matrix. + pub const fn transposed(self) -> Self { Self { cols: [ Vector2::new(self.cols[0].x, self.cols[1].x), @@ -391,10 +470,22 @@ impl Basis2D { } } - fn scaled(&self, scale: Vector2) -> Self { - Self { - cols: [self[0] * scale.x, self[1] * scale.y], - } + pub fn set_row_x(&mut self, v: Vector2) { + self[0].x = v.x; + self[1].x = v.y; + } + + pub fn row_x(&self) -> Vector2 { + Vector2::new(self[0].x, self[1].x) + } + + pub fn set_row_y(&mut self, v: Vector2) { + self[0].y = v.x; + self[1].y = v.y; + } + + pub fn row_y(&self) -> Vector2 { + Vector2::new(self[0].y, self[1].y) } } @@ -435,6 +526,22 @@ impl Mul for Basis2D { } } +impl Index for Basis2D { + type Output = Vector2; + + #[inline] + fn index(&self, index: usize) -> &Self::Output { + &self.cols[index] + } +} + +impl IndexMut for Basis2D { + #[inline] + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.cols[index] + } +} + impl GlamType for Mat2 { type Mapped = Basis2D; @@ -452,3 +559,193 @@ impl GlamType for Mat2 { impl GlamConv for Basis2D { type Glam = Mat2; } + +#[cfg(test)] +mod test { + use crate::assert_eq_approx; + + use super::*; + + #[test] + fn transform2d_constructors_correct() { + let trans = Transform2D::from_angle(115.0f32.to_radians()); + assert_eq_approx!(trans.rotation(), 115.0f32.to_radians(), is_equal_approx); + + let trans = Transform2D::from_angle_origin((-80.0f32).to_radians(), Vector2::new(1.4, 9.8)); + assert_eq_approx!(trans.rotation(), (-80.0f32).to_radians(), is_equal_approx); + assert_eq_approx!( + trans.origin, + Vector2::new(1.4, 9.8), + Vector2::is_equal_approx + ); + + let trans = Transform2D::from_angle_scale_skew_origin( + 170.0f32.to_radians(), + Vector2::new(3.6, 8.0), + 20.0f32.to_radians(), + Vector2::new(2.4, 6.8), + ); + assert_eq_approx!(trans.rotation(), 170.0f32.to_radians(), is_equal_approx); + assert_eq_approx!( + trans.scale(), + Vector2::new(3.6, 8.0), + Vector2::is_equal_approx + ); + assert_eq_approx!(trans.skew(), 20.0f32.to_radians(), is_equal_approx); + assert_eq_approx!( + trans.origin, + Vector2::new(2.4, 6.8), + Vector2::is_equal_approx + ); + } + + // Tests translated from Godot + + const DUMMY_TRANSFORM: Transform2D = Transform2D::new( + Basis2D::from_cols(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0)), + Vector2::new(5.0, 6.0), + ); + + #[test] + fn translation() { + let offset = Vector2::new(1.0, 2.0); + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform2D::IDENTITY.translated(offset), + Transform2D::IDENTITY.translated_local(offset) + ); + + // Check both versions against left and right multiplications. + let t = Transform2D::IDENTITY.translated(offset); + assert_eq!(DUMMY_TRANSFORM.translated(offset), t * DUMMY_TRANSFORM); + assert_eq!( + DUMMY_TRANSFORM.translated_local(offset), + DUMMY_TRANSFORM * t + ); + } + + #[test] + fn scaling() { + let scaling = Vector2::new(1.0, 2.0); + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform2D::IDENTITY.scaled(scaling), + Transform2D::IDENTITY.scaled_local(scaling) + ); + + // Check both versions against left and right multiplications. + let s: Transform2D = Transform2D::IDENTITY.scaled(scaling); + assert_eq!(DUMMY_TRANSFORM.scaled(scaling), s * DUMMY_TRANSFORM); + assert_eq!(DUMMY_TRANSFORM.scaled_local(scaling), DUMMY_TRANSFORM * s); + } + + #[test] + fn rotation() { + let phi = 1.0; + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform2D::IDENTITY.rotated(phi), + Transform2D::IDENTITY.rotated_local(phi) + ); + + // Check both versions against left and right multiplications. + let r: Transform2D = Transform2D::IDENTITY.rotated(phi); + assert_eq!(DUMMY_TRANSFORM.rotated(phi), r * DUMMY_TRANSFORM); + assert_eq!(DUMMY_TRANSFORM.rotated_local(phi), DUMMY_TRANSFORM * r); + } + + #[test] + fn interpolation() { + let rotate_scale_skew_pos: Transform2D = Transform2D::from_angle_scale_skew_origin( + 170.0f32.to_radians(), + Vector2::new(3.6, 8.0), + 20.0f32.to_radians(), + Vector2::new(2.4, 6.8), + ); + + let rotate_scale_skew_pos_halfway: Transform2D = Transform2D::from_angle_scale_skew_origin( + 85.0f32.to_radians(), + Vector2::new(2.3, 4.5), + 10.0f32.to_radians(), + Vector2::new(1.2, 3.4), + ); + let interpolated: Transform2D = + Transform2D::IDENTITY.interpolate_with(&rotate_scale_skew_pos, 0.5); + assert_eq_approx!( + interpolated.origin, + rotate_scale_skew_pos_halfway.origin, + Vector2::is_equal_approx + ); + assert_eq_approx!( + interpolated.rotation(), + rotate_scale_skew_pos_halfway.rotation() + 1.0, + is_equal_approx + ); + assert_eq_approx!( + interpolated.scale(), + rotate_scale_skew_pos_halfway.scale(), + Vector2::is_equal_approx + ); + assert_eq_approx!( + interpolated.skew(), + rotate_scale_skew_pos_halfway.skew(), + is_equal_approx + ); + assert_eq_approx!( + &interpolated, + &rotate_scale_skew_pos_halfway, + Transform2D::is_equal_approx + ); + let interpolated = rotate_scale_skew_pos.interpolate_with(&Transform2D::IDENTITY, 0.5); + assert_eq_approx!( + &interpolated, + &rotate_scale_skew_pos_halfway, + Transform2D::is_equal_approx + ); + } + + #[test] + fn finite_number_checks() { + let x: Vector2 = Vector2::new(0.0, 1.0); + let infinite: Vector2 = Vector2::new(f32::NAN, f32::NAN); + + assert!( + Transform2D::new(Basis2D::from_cols(x, x), x).is_finite(), + "let with: Transform2D all components finite should be finite", + ); + + assert!( + !Transform2D::new(Basis2D::from_cols(infinite, x), x).is_finite(), + "let with: Transform2D one component infinite should not be finite.", + ); + assert!( + !Transform2D::new(Basis2D::from_cols(x, infinite), x).is_finite(), + "let with: Transform2D one component infinite should not be finite.", + ); + assert!( + !Transform2D::new(Basis2D::from_cols(x, x), infinite).is_finite(), + "let with: Transform2D one component infinite should not be finite.", + ); + + assert!( + !Transform2D::new(Basis2D::from_cols(infinite, infinite), x).is_finite(), + "let with: Transform2D two components infinite should not be finite.", + ); + assert!( + !Transform2D::new(Basis2D::from_cols(infinite, x), infinite).is_finite(), + "let with: Transform2D two components infinite should not be finite.", + ); + assert!( + !Transform2D::new(Basis2D::from_cols(x, infinite), infinite).is_finite(), + "let with: Transform2D two components infinite should not be finite.", + ); + + assert!( + !Transform2D::new(Basis2D::from_cols(infinite, infinite), infinite).is_finite(), + "let with: Transform2D three components infinite should not be finite.", + ); + } +} From bb2c780bc2a3f5473cdb7d7b56bf3fad0efc4efd Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Sat, 18 Feb 2023 20:48:53 +0100 Subject: [PATCH 05/15] Transform3D --- godot-core/src/builtin/basis.rs | 14 +- godot-core/src/builtin/transform2d.rs | 2 +- godot-core/src/builtin/transform3d.rs | 241 ++++++++++++++++++++++++-- 3 files changed, 235 insertions(+), 22 deletions(-) diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index eb6683886..0a9381b74 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -54,8 +54,6 @@ impl Basis { pub const FLIP_Z: Self = Self::from_diagonal(1.0, 1.0, -1.0); /// Create a diagonal matrix from the given values. - /// - /// _Godot equivalent: `Basis.from_scale(Vector3 scale)` #[must_use] pub const fn from_diagonal(x: f32, y: f32, z: f32) -> Self { Self { @@ -67,6 +65,14 @@ impl Basis { } } + /// Create a diagonal matrix from the given values. + /// + /// _Godot equivalent: `Basis.from_scale(Vector3 scale)` + #[must_use] + pub const fn from_scale(scale: Vector3) -> Self { + Self::from_diagonal(scale.x, scale.y, scale.z) + } + /// Create a new basis from 3 row vectors. These are *not* basis vectors. #[must_use] pub const fn from_rows(x: Vector3, y: Vector3, z: Vector3) -> Self { @@ -396,7 +402,7 @@ impl Basis { /// _Godot equivalent: `Basis.scaled()`_ #[must_use] pub fn scaled(self, scale: Vector3) -> Self { - self * Self::from_diagonal(scale.x, scale.y, scale.z) + Self::from_diagonal(scale.x, scale.y, scale.z) * self } /// Returns the inverse of the matrix. @@ -449,7 +455,7 @@ impl Basis { /// _Godot equivalent: `Basis.rotated()`_ #[must_use] pub fn rotated(self, axis: Vector3, angle: f32) -> Self { - self * Self::from_axis_angle(axis, angle) + Self::from_axis_angle(axis, angle) * self } /// Assuming that the matrix is a proper rotation matrix, slerp performs diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs index aca8d1bff..e0c148904 100644 --- a/godot-core/src/builtin/transform2d.rs +++ b/godot-core/src/builtin/transform2d.rs @@ -599,7 +599,7 @@ mod test { ); } - // Tests translated from Godot + // Tests translated from Godot. const DUMMY_TRANSFORM: Transform2D = Transform2D::new( Basis2D::from_cols(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0)), diff --git a/godot-core/src/builtin/transform3d.rs b/godot-core/src/builtin/transform3d.rs index f5d9fe372..b5d4a0ae0 100644 --- a/godot-core/src/builtin/transform3d.rs +++ b/godot-core/src/builtin/transform3d.rs @@ -10,28 +10,66 @@ use sys::{ffi_methods, GodotFfi}; use super::{ glam_helpers::{GlamConv, GlamType}, - Basis, Vector3, + Basis, Projection, Vector3, }; use glam::f32::Affine3A; +/// Affine 3D transform (3x4 matrix). +/// +/// Used for 3D linear transformations. Uses a basis + origin representation. +/// +/// Expressed as a 3x4 matrix, this transform consists of 3 basis (column) +/// vectors `a`, `b`, `c` as well as an origin `o`: +/// ```text +/// [ a.x b.x c.x o.x ] +/// [ a.y b.y c.y o.y ] +/// [ a.z b.z c.z o.z ] +/// ``` #[derive(Debug, Default, Clone, Copy, PartialEq)] #[repr(C)] pub struct Transform3D { + /// The basis is a matrix containing 3 vectors as its columns. They can be + /// interpreted as the basis vectors of the transformed coordinate system. pub basis: Basis, + + /// The new origin of the transformed coordinate system. pub origin: Vector3, } impl Transform3D { + /// The identity transform, with no translation, rotation or scaling + /// applied. When applied to other data structures, `IDENTITY` performs no + /// transformation. + /// + /// _Godot equivalent: `Transform3D.IDENTITY`_ pub const IDENTITY: Self = Self::new(Basis::IDENTITY, Vector3::ZERO); + + /// `Transform3D` with mirroring applied perpendicular to the YZ plane. + /// + /// _Godot equivalent: `Transform3D.FLIP_X`_ pub const FLIP_X: Self = Self::new(Basis::FLIP_X, Vector3::ZERO); + + /// `Transform3D` with mirroring applied perpendicular to the XZ plane. + /// + /// _Godot equivalent: `Transform3D.FLIP_Y`_ pub const FLIP_Y: Self = Self::new(Basis::FLIP_Y, Vector3::ZERO); + + /// `Transform3D` with mirroring applied perpendicular to the XY plane. + /// + /// _Godot equivalent: `Transform3D.FLIP_Z`_ pub const FLIP_Z: Self = Self::new(Basis::FLIP_Z, Vector3::ZERO); + /// Create a new transform from a [`Basis`] and a [`Vector3`]. + /// + /// _Godot equivalent: Transform3D(Basis basis, Vector3 origin)_ pub const fn new(basis: Basis, origin: Vector3) -> Self { Self { basis, origin } } + /// Create a new transform from 4 matrix-columns. + /// + /// _Godot equivalent: Transform3D(Vector3 x_axis, Vector3 y_axis, Vector3 z_axis, Vector3 origin)_ pub const fn from_cols(x: Vector3, y: Vector3, z: Vector3, origin: Vector3) -> Self { Self { basis: Basis::from_cols(x, y, z), @@ -39,7 +77,11 @@ impl Transform3D { } } - pub fn inverted(&self) -> Self { + /// Returns the inverse of the transform, under the assumption that the + /// transformation is composed of rotation, scaling and translation. + /// + /// _Godot equivalent: Transform3D.affine_inverse()_ + pub fn inverse(&self) -> Self { self.glam(|aff| aff.inverse()) } @@ -53,14 +95,28 @@ impl Transform3D { } } + /// Returns `true if this transform and transform are approximately equal, by + /// calling is_equal_approx each basis and origin. + /// + /// _Godot equivalent: Transform3D.is_equal_approx()_ pub fn is_equal_approx(&self, other: &Self) -> bool { self.basis.is_equal_approx(&other.basis) && self.origin.is_equal_approx(other.origin) } + /// Returns `true if this transform is finite by calling `is_finite` on the + /// basis and origin. + /// + /// _Godot equivalent: Transform3D.is_finite()_ pub fn is_finite(&self) -> bool { self.basis.is_finite() && self.origin.is_finite() } + /// Returns a copy of the transform rotated such that the forward axis (-Z) + /// points towards the `target` position. + /// + /// See [`Basis::new_looking_at()`] for more information. + /// + /// _Godot equivalent: Transform3D.looking_at()_ pub fn looking_at(&self, target: Vector3, up: Vector3) -> Self { Self { basis: Basis::new_looking_at(target, up), @@ -68,6 +124,10 @@ impl Transform3D { } } + /// Returns the transform with the basis orthogonal (90 degrees), and + /// normalized axis vectors (scale of 1 or -1). + /// + /// _Godot equivalent: Transform3D.orthonormalized()_ pub fn orthonormalized(&self) -> Self { Self { basis: self.basis.orthonormalized(), @@ -75,36 +135,64 @@ impl Transform3D { } } + /// Returns a copy of the transform rotated by the given `angle` (in radians). + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding rotation transform `R` from the left, i.e., `R * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.rotated()`_ pub fn rotated(&self, axis: Vector3, angle: f32) -> Self { - let basis = self.basis.rotated(axis, angle); + let rotation = Basis::from_axis_angle(axis, angle); Self { - basis, - origin: basis * self.origin, + basis: rotation * self.basis, + origin: rotation * self.origin, } } - + /// Returns a copy of the transform rotated by the given `angle` (in radians). + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding rotation transform `R` from the right, i.e., `X * R`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.rotated_local()`_ pub fn rotated_local(&self, axis: Vector3, angle: f32) -> Self { - let basis = self.basis.rotated(axis, angle); Self { - basis, + basis: self.basis * Basis::from_axis_angle(axis, angle), origin: self.origin, } } + /// Returns a copy of the transform scaled by the given scale factor. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding scaling transform `S` from the left, i.e., `S * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.scaled()`_ pub fn scaled(&self, scale: Vector3) -> Self { Self { - basis: self.basis.scaled(scale), + basis: Basis::from_scale(scale) * self.basis, origin: self.origin * scale, } } + /// Returns a copy of the transform scaled by the given scale factor. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding scaling transform `S` from the right, i.e., `X * S`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.scaled_local()`_ pub fn scaled_local(&self, scale: Vector3) -> Self { Self { - basis: self.basis.scaled(scale), + basis: self.basis * Basis::from_scale(scale), origin: self.origin, } } + /// Returns a copy of the transform translated by the given offset. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding translation transform `T` from the left, i.e., `T * X`. + /// This can be seen as transforming with respect to the global/parent frame. + /// + /// _Godot equivalent: `Transform2D.translated()`_ pub fn translated(&self, offset: Vector3) -> Self { Self { basis: self.basis, @@ -112,24 +200,45 @@ impl Transform3D { } } + /// Returns a copy of the transform translated by the given offset. + /// This method is an optimized version of multiplying the given transform `X` + /// with a corresponding translation transform `T` from the right, i.e., `X * T`. + /// This can be seen as transforming with respect to the local frame. + /// + /// _Godot equivalent: `Transform2D.translated()`_ pub fn translated_local(&self, offset: Vector3) -> Self { Self { basis: self.basis, origin: self.origin + (self.basis * offset), } } +} - pub fn col(&self, index: usize) -> Vector3 { - match index { - 0 => self.basis.col_x(), - 1 => self.basis.col_y(), - 2 => self.basis.col_z(), - 3 => self.origin, - _ => panic!("Index {index} out of bounds"), +impl From for Transform3D { + /// Constructs a let froma Projection by trimming the last row of + /// the projection matrix. + /// + /// _Godot equivalent: Transform3D(Projection from)_ + fn from(proj: Projection) -> Self { + let v1 = Vector3::new(proj[0].x, proj[0].y, proj[0].z); + let v2 = Vector3::new(proj[1].x, proj[1].y, proj[1].z); + let v3 = Vector3::new(proj[2].x, proj[2].y, proj[2].z); + let v4 = Vector3::new(proj[3].x, proj[3].y, proj[3].z); + + Self { + basis: Basis::from_cols(v1, v2, v3), + origin: v4, } } } +impl From for Transform3D { + /// Create a new transform with origin `(0,0,0)` from this basis. + fn from(basis: Basis) -> Self { + Self::new(basis, Vector3::ZERO) + } +} + impl Mul for Transform3D { type Output = Self; @@ -182,3 +291,101 @@ impl GlamConv for Transform3D { impl GodotFfi for Transform3D { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } + +#[cfg(test)] +mod test { + use super::*; + + // Tests translated from Godot. + + const DUMMY_TRANSFORM: Transform3D = Transform3D::new( + Basis::from_cols( + Vector3::new(1.0, 2.0, 3.0), + Vector3::new(4.0, 5.0, 6.0), + Vector3::new(7.0, 8.0, 9.0), + ), + Vector3::new(10.0, 11.0, 12.0), + ); + + #[test] + fn translation() { + let offset = Vector3::new(1.0, 2.0, 3.0); + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform3D::IDENTITY.translated(offset), + Transform3D::IDENTITY.translated_local(offset) + ); + + // Check both versions against left and right multiplications. + let t = Transform3D::IDENTITY.translated(offset); + assert_eq!(DUMMY_TRANSFORM.translated(offset), t * DUMMY_TRANSFORM); + assert_eq!( + DUMMY_TRANSFORM.translated_local(offset), + DUMMY_TRANSFORM * t + ); + } + + #[test] + fn scaling() { + let scaling = Vector3::new(1.0, 2.0, 3.0); + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform3D::IDENTITY.scaled(scaling), + Transform3D::IDENTITY.scaled_local(scaling) + ); + + // Check both versions against left and right multiplications. + let s = Transform3D::IDENTITY.scaled(scaling); + assert_eq!(DUMMY_TRANSFORM.scaled(scaling), s * DUMMY_TRANSFORM); + assert_eq!(DUMMY_TRANSFORM.scaled_local(scaling), DUMMY_TRANSFORM * s); + } + + #[test] + fn rotation() { + let axis = Vector3::new(1.0, 2.0, 3.0).normalized(); + let phi: f32 = 1.0; + + // Both versions should give the same result applied to identity. + assert_eq!( + Transform3D::IDENTITY.rotated(axis, phi), + Transform3D::IDENTITY.rotated_local(axis, phi) + ); + + // Check both versions against left and right multiplications. + let r = Transform3D::IDENTITY.rotated(axis, phi); + assert_eq!(DUMMY_TRANSFORM.rotated(axis, phi), r * DUMMY_TRANSFORM); + assert_eq!( + DUMMY_TRANSFORM.rotated_local(axis, phi), + DUMMY_TRANSFORM * r + ); + } + + #[test] + fn finite_number_checks() { + let y = Vector3::new(0.0, 1.0, 2.0); + let infinite_vec = Vector3::new(f32::NAN, f32::NAN, f32::NAN); + let x = Basis::from_rows(y, y, y); + let infinite_basis = Basis::from_rows(infinite_vec, infinite_vec, infinite_vec); + + assert!( + Transform3D::new(x, y).is_finite(), + "Transform3D with all components finite should be finite", + ); + + assert!( + !Transform3D::new(x, infinite_vec).is_finite(), + "Transform3D with one component infinite should not be finite.", + ); + assert!( + !Transform3D::new(infinite_basis, y).is_finite(), + "Transform3D with one component infinite should not be finite.", + ); + + assert!( + !Transform3D::new(infinite_basis, infinite_vec).is_finite(), + "Transform3D with two components infinite should not be finite.", + ); + } +} From c42a3fdec8d2fb151e9cb2c482c2ceb132217907 Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Sat, 18 Feb 2023 21:25:35 +0100 Subject: [PATCH 06/15] Projection --- godot-core/src/builtin/basis.rs | 6 +- godot-core/src/builtin/projection.rs | 155 +++++++++++++++++++++++++-- 2 files changed, 150 insertions(+), 11 deletions(-) diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index 0a9381b74..e2a6f1e90 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -678,6 +678,8 @@ impl GodotFfi for Basis { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } +/// The ordering used to interpret a set of euler angles as extrinsic +/// rotations. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(C)] pub enum EulerOrder { @@ -689,10 +691,6 @@ pub enum EulerOrder { ZYX = 5, } -impl GodotFfi for EulerOrder { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } -} - #[cfg(test)] mod test { use std::f32::consts::{FRAC_PI_2, PI}; diff --git a/godot-core/src/builtin/projection.rs b/godot-core/src/builtin/projection.rs index b659d4e62..56a011602 100644 --- a/godot-core/src/builtin/projection.rs +++ b/godot-core/src/builtin/projection.rs @@ -11,26 +11,49 @@ use sys::{ffi_methods, GodotFfi}; use super::{ glam_helpers::{GlamConv, GlamType}, inner::InnerProjection, - Transform3D, Vector2, Vector4, + Plane, Transform3D, Vector2, Vector4, }; use glam::f32::Mat4; - +/// A 4x4 matrix used for 3D projective transformations. It can represent +/// transformations such as translation, rotation, scaling, shearing, and +/// perspective division. It consists of four Vector4 columns. +/// +/// For purely linear transformations (translation, rotation, and scale), it is +/// recommended to use Transform3D, as it is more performant and has a lower +/// memory footprint. +/// +/// Used internally as Camera3D's projection matrix. +/// +/// Note: The current implementation largely makes calls to godot for its +/// methods and as such are not as performant as other types. #[derive(Debug, Clone, Copy, PartialEq)] #[repr(C)] pub struct Projection { + /// The columns of the projection matrix. pub cols: [Vector4; 4], } impl Projection { + /// A Projection with no transformation defined. When applied to other data + /// structures, no transformation is performed. + /// + /// _Godot equivalent: Projection.IDENTITY_ pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0, 1.0, 1.0); + + /// A Projection with all values initialized to 0. When applied to other + /// data structures, they will be zeroed. + /// + /// _Godot equivalent: Projection.ZERO pub const ZERO: Self = Self::from_diagonal(0.0, 0.0, 0.0, 0.0); + /// Create a new projection from a list of column vectors. #[must_use] pub const fn new(cols: [Vector4; 4]) -> Self { Self { cols } } + /// Create a diagonal matrix from the given values. #[must_use] pub const fn from_diagonal(x: f32, y: f32, z: f32, w: f32) -> Self { Self { @@ -43,15 +66,28 @@ impl Projection { } } + /// Create a matrix from four column vectors. + /// + /// _Godot equivalent: Projection(Vector4 x_axis, Vector4 y_axis, Vector4 z_axis, Vector4 w_axis)_ #[must_use] pub const fn from_cols(x: Vector4, y: Vector4, z: Vector4, w: Vector4) -> Self { Self { cols: [x, y, z, w] } } + /// Creates a new Projection that projects positions from a depth range of + /// -1 to 1 to one that ranges from 0 to 1, and flips the projected + /// positions vertically, according to flip_y. + /// + /// _Godot equivalent: Projection.create_depth_correction()_ pub fn create_depth_correction(flip_y: bool) -> Self { InnerProjection::create_depth_correction(flip_y) } + /// Creates a new Projection for projecting positions onto a head-mounted + /// display with the given X:Y aspect ratio, distance between eyes, display + /// width, distance to lens, oversampling factor, and depth clipping planes. + /// + /// _Godot equivalent: Projection.create_for_hmd()_ #[allow(clippy::too_many_arguments)] pub fn create_for_hmd( eye: Eye, @@ -75,6 +111,10 @@ impl Projection { ) } + /// Creates a new Projection that projects positions in a frustum with the + /// given clipping planes. + /// + /// _Godot equivalent: Projection.create_frustum()_ pub fn create_frustum( left: f64, right: f64, @@ -86,6 +126,13 @@ impl Projection { InnerProjection::create_frustum(left, right, bottom, top, near, far) } + /// Creates a new Projection that projects positions in a frustum with the + /// given size, X:Y aspect ratio, offset, and clipping planes. + /// + /// `flip_fov` determines whether the projection's field of view is flipped + /// over its diagonal. + /// + /// _Godot equivalent: Projection.create_frustum_aspect()_ pub fn create_frustum_aspect( size: f64, aspect: f64, @@ -97,6 +144,10 @@ impl Projection { InnerProjection::create_frustum_aspect(size, aspect, offset, near, far, flip_fov) } + /// Creates a new Projection that projects positions using an orthogonal + /// projection with the given clipping planes. + /// + /// _Godot equivalent: Projection.create_orthogonal()_ pub fn create_orthogonal( left: f64, right: f64, @@ -108,6 +159,13 @@ impl Projection { InnerProjection::create_orthogonal(left, right, bottom, top, near, far) } + /// Creates a new Projection that projects positions using an orthogonal + /// projection with the given size, X:Y aspect ratio, and clipping planes. + /// + /// `flip_fov` determines whether the projection's field of view is flipped + /// over its diagonal. + /// + /// _Godot equivalent: Projection.create_orthogonal_aspect()_ pub fn create_orthogonal_aspect( size: f64, aspect: f64, @@ -118,6 +176,14 @@ impl Projection { InnerProjection::create_orthogonal_aspect(size, aspect, near, far, flip_fov) } + /// Creates a new Projection that projects positions using a perspective + /// projection with the given Y-axis field of view (in degrees), X:Y aspect + /// ratio, and clipping planes + /// + /// `flip_fov` determines whether the projection's field of view is flipped + /// over its diagonal. + /// + /// _Godot equivalent: Projection.create_perspective()_ pub fn create_perspective( fov_y: f64, aspect: f64, @@ -128,6 +194,16 @@ impl Projection { InnerProjection::create_perspective(fov_y, aspect, near, far, flip_fov) } + /// Creates a new Projection that projects positions using a perspective + /// projection with the given Y-axis field of view (in degrees), X:Y aspect + /// ratio, and clipping distances. The projection is adjusted for a + /// head-mounted display with the given distance between eyes and distance + /// to a point that can be focused on. + /// + /// `flip_fov` determines whether the projection's field of view is flipped + /// over its diagonal. + /// + /// _Godot equivalent: Projection.create_perspective_hmd()_ #[allow(clippy::too_many_arguments)] pub fn create_perspective_hmd( fov_y: f64, @@ -151,64 +227,130 @@ impl Projection { ) } + /// Return the determinant of the matrix. + /// + /// _Godot equivalent: Projection.determinant()_ pub fn determinant(&self) -> f32 { self.glam(|mat| mat.determinant()) } + /// Returns a copy of this Projection with the signs of the values of the Y + /// column flipped. + /// + /// _Godot equivalent: Projection.flipped_y()_ pub fn flipped_y(&self) -> Self { Self { cols: [self[0], -self[1], self[2], self[3]], } } + /// Returns the X:Y aspect ratio of this Projection's viewport. + /// + /// _Godot equivalent: Projection.get_aspect()_ pub fn aspect(&self) -> f64 { self.as_inner().get_aspect() } + /// Returns the dimensions of the far clipping plane of the projection, + /// divided by two. + /// + /// _Godot equivalent: Projection.get_far_plane_half_extents()_ pub fn far_plane_half_extents(&self) -> Vector2 { self.as_inner().get_far_plane_half_extents() } + /// Returns the horizontal field of view of the projection (in degrees). + /// + /// _Godot equivalent: Projection.get_fov()_ pub fn fov(&self) -> f64 { self.as_inner().get_fov() } + /// Returns the vertical field of view of a projection (in degrees) which + /// has the given horizontal field of view (in degrees) and aspect ratio. + /// + /// _Godot equivalent: Projection.get_fovy()_ pub fn fov_y_of(fov_x: f64, aspect: f64) -> f64 { InnerProjection::get_fovy(fov_x, aspect) } + /// Returns the factor by which the visible level of detail is scaled by + /// this Projection. + /// + /// _Godot equivalent: Projection.get_lod_multiplier()_ pub fn lod_multiplier(&self) -> f64 { self.as_inner().get_lod_multiplier() } + /// Returns the number of pixels with the given pixel width displayed per + /// meter, after this Projection is applied. + /// + /// _Godot equivalent: Projection.get_pixels_per_meter()_ pub fn pixels_per_meter(&self, pixel_width: i64) -> i64 { self.as_inner().get_pixels_per_meter(pixel_width) } + /// Returns the clipping plane of this Projection whose index is given by + /// plane. + /// + /// _Godot equivalent: Projection.get_projection_plane()_ + pub fn projection_plane(&self, plane: ProjectionPlane) -> Plane { + self.as_inner().get_projection_plane(plane as i64) + } + + /// Returns the dimensions of the viewport plane that this Projection + /// projects positions onto, divided by two. + /// + /// _Godot equivalent: Projection.get_viewport_half_extents()_ pub fn viewport_half_extents(&self) -> Vector2 { self.as_inner().get_viewport_half_extents() } + /// Returns the distance for this Projection beyond which positions are + /// clipped. + /// + /// _Godot equivalent: Projection.get_z_far()_ pub fn z_far(&self) -> f64 { self.as_inner().get_z_far() } + /// Returns the distance for this Projection before which positions are + /// clipped. + /// + /// _Godot equivalent: Projection.get_z_near()_ pub fn z_near(&self) -> f64 { self.as_inner().get_z_near() } - pub fn inverted(&self) -> Self { + /// Returns a Projection that performs the inverse of this Projection's + /// projective transformation. + /// + /// _Godot equivalent: Projection.inverse()_ + pub fn inverse(&self) -> Self { self.glam(|mat| mat.inverse()) } + /// Returns `true` if this Projection performs an orthogonal projection. + /// + /// _Godot equivalent: Projection.is_orthogonal()_ pub fn is_orthogonal(&self) -> bool { self.as_inner().is_orthogonal() } + /// Returns a Projection with the X and Y values from the given [`Vector2`] + /// added to the first and second values of the final column respectively. + /// + /// _Godot equivalent: Projection.jitter_offseted()_ pub fn jitter_offseted(&self, offset: Vector2) -> Self { self.as_inner().jitter_offseted(offset) } + /// Returns a Projection with the near clipping distance adjusted to be + /// `new_znear`. + /// + /// Note: The original Projection must be a perspective projection. + /// + /// _Godot equivalent: Projection.perspective_znear_adjusted()_ pub fn perspective_znear_adjusted(&self, new_znear: f64) -> Self { self.as_inner().perspective_znear_adjusted(new_znear) } @@ -293,6 +435,7 @@ impl GodotFfi for Projection { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } +/// A projections clipping plane. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[repr(C)] pub enum ProjectionPlane { @@ -304,10 +447,8 @@ pub enum ProjectionPlane { Bottom = 5, } -impl GodotFfi for ProjectionPlane { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } -} - +/// The eye to create a projection for, when creating a projection adjusted +/// for head-mounted displays. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[repr(C)] pub enum Eye { From cd220a48d88e86345d726859c939ff0098027aeb Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Sun, 19 Feb 2023 00:06:27 +0100 Subject: [PATCH 07/15] itests --- .gitignore | 1 + godot-core/src/builtin/basis.rs | 10 +++- godot-core/src/builtin/math.rs | 4 +- godot-core/src/builtin/transform2d.rs | 16 +++--- godot-core/src/builtin/transform3d.rs | 28 ++++++++--- itest/rust/src/basis_test.rs | 54 ++++++++++++++++++++- itest/rust/src/lib.rs | 25 ++++++++++ itest/rust/src/transform2d_test.rs | 68 ++++++++++++++++++++++++++ itest/rust/src/transform3d_test.rs | 70 +++++++++++++++++++++++++++ 9 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 itest/rust/src/transform2d_test.rs create mode 100644 itest/rust/src/transform3d_test.rs diff --git a/.gitignore b/.gitignore index 28bf18c16..3ce4a48b8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .run # Rust +ra_target target Cargo.lock diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index e2a6f1e90..0d2ed62db 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -167,7 +167,7 @@ impl Basis { /// _Godot equivalent: `Basis()`_ #[must_use] pub fn to_quat(&self) -> Quaternion { - glam::Quat::from_mat3(&self.to_glam()).to_front() + glam::Quat::from_mat3(&self.orthonormalized().to_glam()).to_front() } #[must_use] @@ -431,7 +431,7 @@ impl Basis { #[must_use] pub fn orthonormalized(&self) -> Self { assert!( - is_equal_approx(self.determinant(), 0.0), + !is_equal_approx(self.determinant(), 0.0), "Determinant should not be zero." ); @@ -691,6 +691,12 @@ pub enum EulerOrder { ZYX = 5, } +impl From for i64 { + fn from(order: EulerOrder) -> Self { + order as i64 + } +} + #[cfg(test)] mod test { use std::f32::consts::{FRAC_PI_2, PI}; diff --git a/godot-core/src/builtin/math.rs b/godot-core/src/builtin/math.rs index 3f4eb142b..a892e5a2b 100644 --- a/godot-core/src/builtin/math.rs +++ b/godot-core/src/builtin/math.rs @@ -28,14 +28,14 @@ macro_rules! assert_eq_approx { ($a:expr, $b:expr, $func:expr $(,)?) => { match ($a, $b) { (a, b) => { - assert!(($func)(a,b), "\n left: {:?},\n right = {:?}", $a, $b); + assert!(($func)(a,b), "\n left: {:?},\n right: {:?}", $a, $b); } } }; ($a:expr, $b:expr, $func:expr, $($t:tt)+) => { match ($a, $b) { (a, b) => { - assert!(($func)(a,b), "\n left: {:?},\n right = {:?},\n{}", $a, $b, format_args!($($t)+)); + assert!(($func)(a,b), "\n left: {:?},\n right: {:?},\n{}", $a, $b, format_args!($($t)+)); } } }; diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs index e0c148904..44ab88013 100644 --- a/godot-core/src/builtin/transform2d.rs +++ b/godot-core/src/builtin/transform2d.rs @@ -135,7 +135,7 @@ impl Transform2D { /// a given `weight` (on the range of 0.0 to 1.0). /// /// _Godot equivalent: `Transform2D.interpolate_with()`_ - pub fn interpolate_with(&self, other: &Self, weight: f32) -> Self { + pub fn interpolate_with(self, other: Self, weight: f32) -> Self { Self::from_angle_scale_skew_origin( lerp_angle(self.rotation(), other.rotation(), weight), self.scale().lerp(other.scale(), weight), @@ -379,10 +379,8 @@ impl Basis2D { /// Returns the scale of the matrix. #[must_use] pub fn scale(&self) -> Vector2 { - let det = self.determinant(); - let det_sign = if det < 0.0 { -1.0 } else { 1.0 }; - - Vector2::new(self[0].length(), self[1].length()) * det_sign + let det_sign = self.determinant().signum(); + Vector2::new(self[0].length(), det_sign * self[1].length()) } /// Introduces an additional scaling. @@ -414,7 +412,7 @@ impl Basis2D { #[must_use] pub fn orthonormalized(&self) -> Self { assert!( - is_equal_approx(self.determinant(), 0.0), + !is_equal_approx(self.determinant(), 0.0), "Determinant should not be zero." ); @@ -673,7 +671,7 @@ mod test { Vector2::new(1.2, 3.4), ); let interpolated: Transform2D = - Transform2D::IDENTITY.interpolate_with(&rotate_scale_skew_pos, 0.5); + Transform2D::IDENTITY.interpolate_with(rotate_scale_skew_pos, 0.5); assert_eq_approx!( interpolated.origin, rotate_scale_skew_pos_halfway.origin, @@ -681,7 +679,7 @@ mod test { ); assert_eq_approx!( interpolated.rotation(), - rotate_scale_skew_pos_halfway.rotation() + 1.0, + rotate_scale_skew_pos_halfway.rotation(), is_equal_approx ); assert_eq_approx!( @@ -699,7 +697,7 @@ mod test { &rotate_scale_skew_pos_halfway, Transform2D::is_equal_approx ); - let interpolated = rotate_scale_skew_pos.interpolate_with(&Transform2D::IDENTITY, 0.5); + let interpolated = rotate_scale_skew_pos.interpolate_with(Transform2D::IDENTITY, 0.5); assert_eq_approx!( &interpolated, &rotate_scale_skew_pos_halfway, diff --git a/godot-core/src/builtin/transform3d.rs b/godot-core/src/builtin/transform3d.rs index b5d4a0ae0..269a735d5 100644 --- a/godot-core/src/builtin/transform3d.rs +++ b/godot-core/src/builtin/transform3d.rs @@ -81,17 +81,29 @@ impl Transform3D { /// transformation is composed of rotation, scaling and translation. /// /// _Godot equivalent: Transform3D.affine_inverse()_ - pub fn inverse(&self) -> Self { + pub fn affine_inverse(&self) -> Self { self.glam(|aff| aff.inverse()) } - pub fn interpolate_with(&self, other: &Self, weight: f32) -> Self { + /// Returns a transform interpolated between this transform and another by + /// a given weight (on the range of 0.0 to 1.0). + /// + /// _Godot equivalent: Transform3D.interpolate_with()_ + pub fn interpolate_with(self, other: Self, weight: f32) -> Self { + let src_scale = self.basis.scale(); + let src_rot = self.basis.to_quat().normalized(); + let src_loc = self.origin; + + let dst_scale = other.basis.scale(); + let dst_rot = other.basis.to_quat().normalized(); + let dst_loc = other.origin; + + let mut basis = Basis::from_scale(src_scale.lerp(dst_scale, weight)); + basis = Basis::from_quat(src_rot.slerp(dst_rot, weight)) * basis; + Self { - basis: self - .basis - .slerp(other.basis, weight) - .scaled(self.basis.scale().lerp(other.basis.scale(), weight)), - origin: self.origin.lerp(other.origin, weight), + basis, + origin: src_loc.lerp(dst_loc, weight), } } @@ -119,7 +131,7 @@ impl Transform3D { /// _Godot equivalent: Transform3D.looking_at()_ pub fn looking_at(&self, target: Vector3, up: Vector3) -> Self { Self { - basis: Basis::new_looking_at(target, up), + basis: Basis::new_looking_at(target - self.origin, up), origin: self.origin, } } diff --git a/itest/rust/src/basis_test.rs b/itest/rust/src/basis_test.rs index 23c77eac2..addf8e71d 100644 --- a/itest/rust/src/basis_test.rs +++ b/itest/rust/src/basis_test.rs @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::itest; +use crate::{assert_inner_equivalent, itest}; use godot::prelude::{inner::InnerBasis, *}; const TEST_BASIS: Basis = Basis::from_rows( @@ -126,6 +126,58 @@ fn basis_euler_angles_same() { } } +#[itest] +fn basis_equiv() { + use crate::assert_inner_equivalent; + assert_inner_equivalent!(InnerBasis, TEST_BASIS.inverse(), |a, b| { + Basis::is_equal_approx(&a, &b) + }); + assert_inner_equivalent!(InnerBasis, TEST_BASIS.transposed(), |a, b| { + Basis::is_equal_approx(&a, &b) + }); + assert_inner_equivalent!(InnerBasis, TEST_BASIS.orthonormalized(), |a, b| { + Basis::is_equal_approx(&a, &b) + }); + assert_inner_equivalent!( + InnerBasis, + TEST_BASIS.determinant(), + |a, b| is_equal_approx(a, b as f32) + ); + assert_inner_equivalent!( + InnerBasis, + TEST_BASIS.rotated(Vector3::new(0.1, 0.2, 0.3).normalized(), 0.1), + |a, b| Basis::is_equal_approx(&a, &b) + ); + assert_inner_equivalent!( + InnerBasis, + TEST_BASIS.scaled(Vector3::new(1.0, 2.0, 3.0)), + |a, b| { Basis::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!(InnerBasis, TEST_BASIS.(scale, get_scale)(), Vector3::is_equal_approx); + assert_inner_equivalent!(InnerBasis, TEST_BASIS.(euler_angles, get_euler)(EulerOrder::XYZ), Vector3::is_equal_approx); + assert_inner_equivalent!( + InnerBasis, + TEST_BASIS.tdotx(Vector3::new(1.0, 2.0, 3.0)), + |a, b| { is_equal_approx(a, b as f32) } + ); + assert_inner_equivalent!( + InnerBasis, + TEST_BASIS.tdoty(Vector3::new(1.0, 2.0, 3.0)), + |a, b| { is_equal_approx(a, b as f32) } + ); + assert_inner_equivalent!( + InnerBasis, + TEST_BASIS.tdotz(Vector3::new(1.0, 2.0, 3.0)), + |a, b| { is_equal_approx(a, b as f32) } + ); + assert_inner_equivalent!( + InnerBasis, + TEST_BASIS.slerp(Basis::IDENTITY, 0.5), + |a, b| { Basis::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!(InnerBasis, TEST_BASIS.(to_quat, get_rotation_quaternion)(), Quaternion::is_equal_approx); +} + fn deg_to_rad(rotation: Vector3) -> Vector3 { Vector3::new( rotation.x.to_radians(), diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 60d62892a..7cb419f55 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -24,6 +24,8 @@ mod packed_array_test; mod quaternion_test; mod singleton_test; mod string_test; +mod transform2d_test; +mod transform3d_test; mod utilities_test; mod variant_test; mod virtual_methods_test; @@ -45,6 +47,8 @@ fn run_tests() -> bool { ok &= quaternion_test::run(); ok &= singleton_test::run(); ok &= string_test::run(); + ok &= transform2d_test::run(); + ok &= transform3d_test::run(); ok &= utilities_test::run(); ok &= variant_test::run(); ok &= virtual_methods_test::run(); @@ -83,3 +87,24 @@ pub(crate) fn expect_panic(context: &str, code: impl FnOnce() + UnwindSafe) { "code should have panicked but did not: {context}", ); } + +#[macro_export] +macro_rules! assert_inner_equivalent { + ($inner_ty:ty, $self:ident.($func_outer:ident, $func_inner:ident)($($args:expr),*), $equal_fn:expr) => { + { + let outer = $self.$func_outer($($args.into()),*); + let inner = <$inner_ty>::from_outer(&$self).$func_inner($($args.into()),*); + godot::private::class_macros::assert_eq_approx!( + outer, + inner, + $equal_fn, + "using functions: outer = {}, inner = {}", + stringify!($func_outer), + stringify!($func_inner) + ); + } + }; + ($inner_ty:ty, $self:ident.$func:ident($($args:expr),*), $equal_fn:expr) => { + assert_inner_equivalent!($inner_ty, $self.($func, $func)($($args),*), $equal_fn); + }; +} diff --git a/itest/rust/src/transform2d_test.rs b/itest/rust/src/transform2d_test.rs new file mode 100644 index 000000000..f6132b2db --- /dev/null +++ b/itest/rust/src/transform2d_test.rs @@ -0,0 +1,68 @@ +use crate::{assert_inner_equivalent, itest}; + +use godot::prelude::{inner::InnerTransform2D, *}; + +const TEST_TRANSFORM: Transform2D = Transform2D::new( + Basis2D::from_cols(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0)), + Vector2::new(5.0, 6.0), +); + +pub(crate) fn run() -> bool { + let mut ok = true; + ok &= transform2d_equiv(); + ok +} + +#[itest] +fn transform2d_equiv() { + assert_inner_equivalent!(InnerTransform2D, TEST_TRANSFORM.affine_inverse(), |a, b| { + Transform2D::is_equal_approx(&a, &b) + }); + assert_inner_equivalent!(InnerTransform2D, TEST_TRANSFORM.(rotation,get_rotation)(), |a, b| { + is_equal_approx(a, b as f32) + }); + assert_inner_equivalent!(InnerTransform2D, TEST_TRANSFORM.(scale,get_scale)(), |a, b| { + Vector2::is_equal_approx(a, b) + }); + assert_inner_equivalent!(InnerTransform2D, TEST_TRANSFORM.(skew,get_skew)(), |a, b| { + is_equal_approx(a, b as f32) + }); + assert_inner_equivalent!( + InnerTransform2D, + TEST_TRANSFORM.orthonormalized(), + |a, b| { Transform2D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!(InnerTransform2D, TEST_TRANSFORM.rotated(1.0), |a, b| { + Transform2D::is_equal_approx(&a, &b) + }); + assert_inner_equivalent!( + InnerTransform2D, + TEST_TRANSFORM.rotated_local(1.0), + |a, b| { Transform2D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform2D, + TEST_TRANSFORM.scaled(Vector2::new(1.0, 2.0)), + |a, b| { Transform2D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform2D, + TEST_TRANSFORM.scaled_local(Vector2::new(1.0, 2.0)), + |a, b| { Transform2D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform2D, + TEST_TRANSFORM.translated_local(Vector2::new(1.0, 2.0)), + |a, b| { Transform2D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform2D, + TEST_TRANSFORM.translated_local(Vector2::new(1.0, 2.0)), + |a, b| { Transform2D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform2D, + TEST_TRANSFORM.interpolate_with(Transform2D::IDENTITY, 0.5), + |a, b| { Transform2D::is_equal_approx(&a, &b) } + ); +} diff --git a/itest/rust/src/transform3d_test.rs b/itest/rust/src/transform3d_test.rs new file mode 100644 index 000000000..932c4a64a --- /dev/null +++ b/itest/rust/src/transform3d_test.rs @@ -0,0 +1,70 @@ +use crate::{assert_inner_equivalent, itest}; + +use godot::prelude::{inner::InnerTransform3D, *}; + +const TEST_TRANSFORM: Transform3D = Transform3D::new( + Basis::from_cols( + Vector3::new(1.0, 2.0, 3.0), + Vector3::new(4.0, 5.0, 6.0), + Vector3::new(7.0, 8.0, -9.0), + ), + Vector3::new(10.0, 11.0, 12.0), +); + +pub(crate) fn run() -> bool { + let mut ok = true; + ok &= transform3d_equiv(); + ok +} + +#[itest] +fn transform3d_equiv() { + assert_inner_equivalent!(InnerTransform3D, TEST_TRANSFORM.affine_inverse(), |a, b| { + Transform3D::is_equal_approx(&a, &b) + }); + assert_inner_equivalent!( + InnerTransform3D, + TEST_TRANSFORM.orthonormalized(), + |a, b| { Transform3D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform3D, + TEST_TRANSFORM.rotated(Vector3::new(1.0, 2.0, 3.0).normalized(), 1.0), + |a, b| { Transform3D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform3D, + TEST_TRANSFORM.rotated_local(Vector3::new(1.0, 2.0, 3.0).normalized(), 1.0), + |a, b| { Transform3D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform3D, + TEST_TRANSFORM.scaled(Vector3::new(1.0, 2.0, 3.0)), + |a, b| { Transform3D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform3D, + TEST_TRANSFORM.scaled_local(Vector3::new(1.0, 2.0, 3.0)), + |a, b| { Transform3D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform3D, + TEST_TRANSFORM.translated_local(Vector3::new(1.0, 2.0, 3.0)), + |a, b| { Transform3D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform3D, + TEST_TRANSFORM.translated_local(Vector3::new(1.0, 2.0, 3.0)), + |a, b| { Transform3D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform3D, + TEST_TRANSFORM.looking_at(Vector3::new(1.0, 2.0, 3.0), Vector3::UP), + |a, b| { Transform3D::is_equal_approx(&a, &b) } + ); + assert_inner_equivalent!( + InnerTransform3D, + TEST_TRANSFORM.interpolate_with(Transform3D::IDENTITY, 0.5), + |a, b| { Transform3D::is_equal_approx(&a, &b) } + ); +} From 13900d1f574735673dbcadb6abbdcb9ead61ca22 Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Sun, 19 Feb 2023 00:10:21 +0100 Subject: [PATCH 08/15] license guards clippy --- godot-core/src/builtin/math.rs | 5 ++++- itest/rust/src/transform2d_test.rs | 5 +++++ itest/rust/src/transform3d_test.rs | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/godot-core/src/builtin/math.rs b/godot-core/src/builtin/math.rs index a892e5a2b..1919ed7f2 100644 --- a/godot-core/src/builtin/math.rs +++ b/godot-core/src/builtin/math.rs @@ -44,7 +44,10 @@ macro_rules! assert_eq_approx { #[macro_export] macro_rules! assert_ne_approx { ($a:expr, $b:expr, $func:expr $(, $($t:tt)*)?) => { - assert_eq_approx!($a, $b, |a,b| !($func)(a,b) $(, $($t)*)?) + #[allow(clippy::redundant_closure_call)] + { + assert_eq_approx!($a, $b, |a,b| !($func)(a,b) $(, $($t)*)?) + } }; } diff --git a/itest/rust/src/transform2d_test.rs b/itest/rust/src/transform2d_test.rs index f6132b2db..c4f7211bc 100644 --- a/itest/rust/src/transform2d_test.rs +++ b/itest/rust/src/transform2d_test.rs @@ -1,3 +1,8 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ use crate::{assert_inner_equivalent, itest}; use godot::prelude::{inner::InnerTransform2D, *}; diff --git a/itest/rust/src/transform3d_test.rs b/itest/rust/src/transform3d_test.rs index 932c4a64a..fa9d49ad0 100644 --- a/itest/rust/src/transform3d_test.rs +++ b/itest/rust/src/transform3d_test.rs @@ -1,3 +1,8 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ use crate::{assert_inner_equivalent, itest}; use godot::prelude::{inner::InnerTransform3D, *}; From daa45c6f4be13d7e02cacf9f6541ecd7cb0fb4a5 Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Sun, 19 Feb 2023 18:00:04 +0100 Subject: [PATCH 09/15] mlem --- godot-core/src/builtin/math.rs | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/godot-core/src/builtin/math.rs b/godot-core/src/builtin/math.rs index 1919ed7f2..880c0cb78 100644 --- a/godot-core/src/builtin/math.rs +++ b/godot-core/src/builtin/math.rs @@ -23,34 +23,6 @@ pub fn is_equal_approx(a: f32, b: f32) -> bool { (a - b).abs() < tolerance } -#[macro_export] -macro_rules! assert_eq_approx { - ($a:expr, $b:expr, $func:expr $(,)?) => { - match ($a, $b) { - (a, b) => { - assert!(($func)(a,b), "\n left: {:?},\n right: {:?}", $a, $b); - } - } - }; - ($a:expr, $b:expr, $func:expr, $($t:tt)+) => { - match ($a, $b) { - (a, b) => { - assert!(($func)(a,b), "\n left: {:?},\n right: {:?},\n{}", $a, $b, format_args!($($t)+)); - } - } - }; -} - -#[macro_export] -macro_rules! assert_ne_approx { - ($a:expr, $b:expr, $func:expr $(, $($t:tt)*)?) => { - #[allow(clippy::redundant_closure_call)] - { - assert_eq_approx!($a, $b, |a,b| !($func)(a,b) $(, $($t)*)?) - } - }; -} - pub fn is_zero_approx(s: f32) -> bool { s.abs() < CMP_EPSILON } From 3b88a0a457fb99bf17031d85832847c24c43197a Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Sun, 19 Feb 2023 18:17:31 +0100 Subject: [PATCH 10/15] Final tweaks for draft --- godot-core/src/builtin/basis.rs | 14 +++++--- godot-core/src/builtin/projection.rs | 4 +-- godot-core/src/builtin/transform2d.rs | 33 +++++++++++++++--- godot-core/src/builtin/transform3d.rs | 48 +++++++++++++++++++++------ 4 files changed, 78 insertions(+), 21 deletions(-) diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index 0d2ed62db..43722c279 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -166,7 +166,7 @@ impl Basis { /// /// _Godot equivalent: `Basis()`_ #[must_use] - pub fn to_quat(&self) -> Quaternion { + pub fn to_quat(self) -> Quaternion { glam::Quat::from_mat3(&self.orthonormalized().to_glam()).to_front() } @@ -409,7 +409,7 @@ impl Basis { /// /// _Godot equivalent: `Basis.inverse()`_ #[must_use] - pub fn inverse(&self) -> Basis { + pub fn inverse(self) -> Basis { self.glam(|mat| mat.inverse()) } @@ -429,7 +429,7 @@ impl Basis { /// /// _Godot equivalent: `Basis.orthonormalized()`_ #[must_use] - pub fn orthonormalized(&self) -> Self { + pub fn orthonormalized(self) -> Self { assert!( !is_equal_approx(self.determinant(), 0.0), "Determinant should not be zero." @@ -506,7 +506,7 @@ impl Basis { /// _Godot equivalent: `Basis.is_finite()`_ #[must_use] pub fn is_finite(&self) -> bool { - self.to_glam().is_finite() + self[0].is_finite() && self[1].is_finite() && self[2].is_finite() } /// Returns `true` if this basis and `other` are approximately equal, @@ -590,6 +590,12 @@ impl Display for Basis { } } +impl From for Basis { + fn from(quat: Quaternion) -> Self { + Basis::from_quat(quat) + } +} + impl GlamConv for Basis { type Glam = glam::f32::Mat3; } diff --git a/godot-core/src/builtin/projection.rs b/godot-core/src/builtin/projection.rs index 56a011602..3247fe102 100644 --- a/godot-core/src/builtin/projection.rs +++ b/godot-core/src/builtin/projection.rs @@ -238,7 +238,7 @@ impl Projection { /// column flipped. /// /// _Godot equivalent: Projection.flipped_y()_ - pub fn flipped_y(&self) -> Self { + pub fn flipped_y(self) -> Self { Self { cols: [self[0], -self[1], self[2], self[3]], } @@ -326,7 +326,7 @@ impl Projection { /// projective transformation. /// /// _Godot equivalent: Projection.inverse()_ - pub fn inverse(&self) -> Self { + pub fn inverse(self) -> Self { self.glam(|mat| mat.inverse()) } diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs index 44ab88013..7622d88ca 100644 --- a/godot-core/src/builtin/transform2d.rs +++ b/godot-core/src/builtin/transform2d.rs @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::{f32::consts::PI, ops::*}; +use std::{f32::consts::PI, fmt::Display, ops::*}; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; @@ -106,7 +106,7 @@ impl Transform2D { /// transformation is composed of rotation, scaling and translation. /// /// _Godot equivalent: `Transform2D.affine_inverse()`_ - pub fn affine_inverse(&self) -> Self { + pub fn affine_inverse(self) -> Self { self.glam(|aff| aff.inverse()) } @@ -164,7 +164,7 @@ impl Transform2D { /// normalized axis vectors (scale of 1 or -1). /// /// _Godot equivalent: `Transform2D.orthonormalized()`_ - pub fn orthonormalized(&self) -> Self { + pub fn orthonormalized(self) -> Self { Self { basis: self.basis.orthonormalized(), origin: self.origin, @@ -247,6 +247,21 @@ impl Transform2D { } } +impl Display for Transform2D { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Godot output: + // [X: (1, 2), Y: (3, 4), O: (5, 6)] + // Where X,Y,O are the columns + + let [Vector2 { x: x1, y: x2 }, Vector2 { x: y1, y: y2 }] = self.basis.cols; + let Vector2 { x: o1, y: o2 } = self.origin; + + f.write_fmt(format_args!( + "[X: ({x1}, {x2}), Y: ({y1}, {y2}), O: ({o1}, {o2})]" + )) + } +} + impl Index for Transform2D { type Output = Vector2; @@ -384,7 +399,7 @@ impl Basis2D { } /// Introduces an additional scaling. - pub fn scaled(&self, scale: Vector2) -> Self { + pub fn scaled(self, scale: Vector2) -> Self { Self { cols: [self[0] * scale.x, self[1] * scale.y], } @@ -410,7 +425,7 @@ impl Basis2D { /// Returns the orthonormalized version of the basis. #[must_use] - pub fn orthonormalized(&self) -> Self { + pub fn orthonormalized(self) -> Self { assert!( !is_equal_approx(self.determinant(), 0.0), "Determinant should not be zero." @@ -493,6 +508,14 @@ impl Default for Basis2D { } } +impl Display for Basis2D { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let [Vector2 { x: x1, y: x2 }, Vector2 { x: y1, y: y2 }] = self.cols; + + f.write_fmt(format_args!("[X: ({x1}, {x2}), Y: ({y1}, {y2}))]")) + } +} + impl Mul for Basis2D { type Output = Self; diff --git a/godot-core/src/builtin/transform3d.rs b/godot-core/src/builtin/transform3d.rs index 269a735d5..f1e2c4af7 100644 --- a/godot-core/src/builtin/transform3d.rs +++ b/godot-core/src/builtin/transform3d.rs @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::ops::*; +use std::{fmt::Display, ops::*}; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; @@ -81,7 +81,7 @@ impl Transform3D { /// transformation is composed of rotation, scaling and translation. /// /// _Godot equivalent: Transform3D.affine_inverse()_ - pub fn affine_inverse(&self) -> Self { + pub fn affine_inverse(self) -> Self { self.glam(|aff| aff.inverse()) } @@ -129,7 +129,7 @@ impl Transform3D { /// See [`Basis::new_looking_at()`] for more information. /// /// _Godot equivalent: Transform3D.looking_at()_ - pub fn looking_at(&self, target: Vector3, up: Vector3) -> Self { + pub fn looking_at(self, target: Vector3, up: Vector3) -> Self { Self { basis: Basis::new_looking_at(target - self.origin, up), origin: self.origin, @@ -140,7 +140,7 @@ impl Transform3D { /// normalized axis vectors (scale of 1 or -1). /// /// _Godot equivalent: Transform3D.orthonormalized()_ - pub fn orthonormalized(&self) -> Self { + pub fn orthonormalized(self) -> Self { Self { basis: self.basis.orthonormalized(), origin: self.origin, @@ -153,7 +153,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the global/parent frame. /// /// _Godot equivalent: `Transform2D.rotated()`_ - pub fn rotated(&self, axis: Vector3, angle: f32) -> Self { + pub fn rotated(self, axis: Vector3, angle: f32) -> Self { let rotation = Basis::from_axis_angle(axis, angle); Self { basis: rotation * self.basis, @@ -166,7 +166,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the local frame. /// /// _Godot equivalent: `Transform2D.rotated_local()`_ - pub fn rotated_local(&self, axis: Vector3, angle: f32) -> Self { + pub fn rotated_local(self, axis: Vector3, angle: f32) -> Self { Self { basis: self.basis * Basis::from_axis_angle(axis, angle), origin: self.origin, @@ -179,7 +179,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the global/parent frame. /// /// _Godot equivalent: `Transform2D.scaled()`_ - pub fn scaled(&self, scale: Vector3) -> Self { + pub fn scaled(self, scale: Vector3) -> Self { Self { basis: Basis::from_scale(scale) * self.basis, origin: self.origin * scale, @@ -192,7 +192,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the local frame. /// /// _Godot equivalent: `Transform2D.scaled_local()`_ - pub fn scaled_local(&self, scale: Vector3) -> Self { + pub fn scaled_local(self, scale: Vector3) -> Self { Self { basis: self.basis * Basis::from_scale(scale), origin: self.origin, @@ -205,7 +205,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the global/parent frame. /// /// _Godot equivalent: `Transform2D.translated()`_ - pub fn translated(&self, offset: Vector3) -> Self { + pub fn translated(self, offset: Vector3) -> Self { Self { basis: self.basis, origin: self.origin + offset, @@ -218,7 +218,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the local frame. /// /// _Godot equivalent: `Transform2D.translated()`_ - pub fn translated_local(&self, offset: Vector3) -> Self { + pub fn translated_local(self, offset: Vector3) -> Self { Self { basis: self.basis, origin: self.origin + (self.basis * offset), @@ -226,6 +226,34 @@ impl Transform3D { } } +impl Display for Transform3D { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Godot output: + // [X: (1, 2, 3), Y: (4, 5, 6), Z: (7, 8, 9), O: (10, 11, 12)] + // Where X,Y,O are the columns + let [Vector3 { + x: x1, + y: x2, + z: x3, + }, Vector3 { + x: y1, + y: y2, + z: y3, + }, Vector3 { + x: z1, + y: z2, + z: z3, + }] = self.basis.to_cols(); + let Vector3 { + x: o1, + y: o2, + z: o3, + } = self.origin; + + f.write_fmt(format_args!("[X: ({x1}, {x2}, {x3}), Y: ({y1}, {y2}, {y3}), Z: ({z1}, {z2}, {z3}), O: ({o1}, {o2}, {o3})]")) + } +} + impl From for Transform3D { /// Constructs a let froma Projection by trimming the last row of /// the projection matrix. From 02148742bca56d2d97eff43a5a365a2bcda9aafe Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Sun, 19 Feb 2023 23:39:18 +0100 Subject: [PATCH 11/15] Changes from review --- godot-core/src/builtin/basis.rs | 568 +++++++++----------------- godot-core/src/builtin/quaternion.rs | 3 +- godot-core/src/builtin/transform2d.rs | 43 +- godot-core/src/builtin/transform3d.rs | 66 ++- itest/rust/src/basis_test.rs | 29 +- itest/rust/src/transform2d_test.rs | 16 +- itest/rust/src/transform3d_test.rs | 20 +- 7 files changed, 279 insertions(+), 466 deletions(-) diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index 43722c279..fa0bb49e0 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -1,3 +1,4 @@ +use std::cmp::Ordering; /* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -5,6 +6,7 @@ */ use std::{f32::consts::FRAC_PI_2, fmt::Display, ops::*}; +use glam::{Vec2, Vec3}; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; @@ -26,9 +28,9 @@ use glam::f32::Mat3; pub struct Basis { /// The rows of the matrix. These are *not* the basis vectors. /// - /// To access the basis vectors see [`col_x()`](Self::col_x), [`set_col_x()`](Self::set_col_x), - /// [`col_y()`](Self::col_y), [`set_col_y()`](Self::set_col_y), [`col_x`](Self::col_z()), - /// [`set_col_z()`](Self::set_col_z). + /// To access the basis vectors see [`col_a()`](Self::col_a), [`set_col_a()`](Self::set_col_a), + /// [`col_b()`](Self::col_b), [`set_col_b()`](Self::set_col_b), [`col_c`](Self::col_c()), + /// [`set_col_c()`](Self::set_col_c). pub rows: [Vector3; 3], } @@ -53,8 +55,24 @@ impl Basis { /// _Godot equivalent: `Basis.FLIP_Z`_ pub const FLIP_Z: Self = Self::from_diagonal(1.0, 1.0, -1.0); + /// Create a new basis from 3 row vectors. These are *not* basis vectors. + pub const fn from_rows(x: Vector3, y: Vector3, z: Vector3) -> Self { + Self { rows: [x, y, z] } + } + + /// Create a new basis from 3 column vectors. + pub const fn from_cols(x: Vector3, y: Vector3, z: Vector3) -> Self { + Self::from_rows_array(&[x.x, y.x, z.x, x.y, y.y, z.y, x.z, y.z, z.z]) + } + + /// Create a `Basis` from an axis and angle. + /// + /// _Godot equivalent: `Basis(Vector3 axis, float angle)`_ + pub fn from_axis_angle(axis: Vector3, angle: f32) -> Self { + Mat3::from_axis_angle(axis.to_glam(), angle).to_front() + } + /// Create a diagonal matrix from the given values. - #[must_use] pub const fn from_diagonal(x: f32, y: f32, z: f32) -> Self { Self { rows: [ @@ -68,47 +86,22 @@ impl Basis { /// Create a diagonal matrix from the given values. /// /// _Godot equivalent: `Basis.from_scale(Vector3 scale)` - #[must_use] pub const fn from_scale(scale: Vector3) -> Self { Self::from_diagonal(scale.x, scale.y, scale.z) } - /// Create a new basis from 3 row vectors. These are *not* basis vectors. - #[must_use] - pub const fn from_rows(x: Vector3, y: Vector3, z: Vector3) -> Self { - Self { rows: [x, y, z] } - } - - /// Create a new basis from 3 column vectors. - #[must_use] - pub const fn from_cols(x: Vector3, y: Vector3, z: Vector3) -> Self { - Self::from_rows_array(&[x.x, y.x, z.x, x.y, y.y, z.y, x.z, y.z, z.z]) - } - - #[must_use] const fn from_rows_array(rows: &[f32; 9]) -> Self { - let [x0, y0, z0, x1, y1, z1, x2, y2, z2] = rows; - Self { - rows: [ - Vector3::new(*x0, *y0, *z0), - Vector3::new(*x1, *y1, *z1), - Vector3::new(*x2, *y2, *z2), - ], - } - } - - /// Create a `Basis` from an axis and angle. - /// - /// _Godot equivalent: `Basis(Vector3 axis, float angle)`_ - #[must_use] - pub fn from_axis_angle(axis: Vector3, angle: f32) -> Self { - Mat3::from_axis_angle(axis.to_glam(), angle).to_front() + 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), + ) } /// Create a `Basis` from a `Quaternion`. /// /// _Godot equivalent: `Basis(Quaternion from)`_ - #[must_use] pub fn from_quat(quat: Quaternion) -> Self { Mat3::from_quat(quat.to_glam()).to_front() } @@ -117,14 +110,14 @@ impl Basis { /// as Euler angles according to the given `EulerOrder`. /// /// _Godot equivalent: `Basis.from_euler(Vector3 euler, int order)`_ - #[must_use] - pub fn from_euler(order: EulerOrder, a: f32, b: f32, c: f32) -> Self { + pub fn from_euler(order: EulerOrder, angles: Vector3) -> Self { // Translated from "Basis::from_euler" in // https://github.com/godotengine/godot/blob/master/core/math/basis.cpp // We can't use glam to do these conversions since glam uses intrinsic rotations // whereas godot uses extrinsic rotations. // see https://github.com/bitshifter/glam-rs/issues/337 + 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 = @@ -133,7 +126,7 @@ impl Basis { 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::XYZ => xmat * ymat * zmat, EulerOrder::XZY => xmat * zmat * ymat, EulerOrder::YXZ => ymat * xmat * zmat, EulerOrder::YZX => ymat * zmat * xmat, @@ -151,13 +144,11 @@ impl Basis { /// cannot be parallel to each other. /// /// _Godot equivalent: `Basis.looking_at()`_ - #[must_use] pub fn new_looking_at(target: Vector3, up: Vector3) -> Self { super::inner::InnerBasis::looking_at(target, up) } /// Creates a `[Vector3; 3]` with the columns of the `Basis`. - #[must_use] pub fn to_cols(self) -> [Vector3; 3] { self.transposed().rows } @@ -165,27 +156,25 @@ impl Basis { /// Creates a [`Quaternion`] representing the same rotation as this basis. /// /// _Godot equivalent: `Basis()`_ - #[must_use] pub fn to_quat(self) -> Quaternion { glam::Quat::from_mat3(&self.orthonormalized().to_glam()).to_front() } - #[must_use] const fn to_rows_array(self) -> [f32; 9] { let [Vector3 { - x: x0, - y: y0, - z: z0, + x: ax, + y: bx, + z: cx, }, Vector3 { - x: x1, - y: y1, - z: z1, + x: ay, + y: by, + z: cy, }, Vector3 { - x: x2, - y: y2, - z: z2, + x: az, + y: bz, + z: cz, }] = self.rows; - [x0, y0, z0, x1, y1, z1, x2, y2, z2] + [ax, bx, cx, ay, by, cy, az, bz, cz] } /// Returns the scale of the matrix. @@ -197,9 +186,9 @@ impl Basis { let det_sign = if det < 0.0 { -1.0 } else { 1.0 }; Vector3::new( - self.col_x().length(), - self.col_y().length(), - self.col_z().length(), + self.col_a().length(), + self.col_b().length(), + self.col_c().length(), ) * det_sign } @@ -208,191 +197,90 @@ impl Basis { /// The order of the angles are given by `order`. /// /// _Godot equivalent: `Basis.get_euler()`_ - #[must_use] - pub fn euler_angles(&self, order: EulerOrder) -> Vector3 { - // Translated from "Basis::get_euler" in - // https://github.com/godotengine/godot/blob/master/core/math/basis.cpp + pub fn to_euler(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(); - // We can't use glam to do these conversions since glam uses intrinsic rotations - // whereas godot uses extrinsic rotations. - // see https://github.com/bitshifter/glam-rs/issues/337 match order { EulerOrder::XYZ => { - // Euler angles in XYZ convention. - // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix - // - // rot = cy*cz -cy*sz sy - // cz*sx*sy+cx*sz cx*cz-sx*sy*sz -cy*sx - // -cx*cz*sy+sx*sz cz*sx+cx*sy*sz cx*cy - - let sy: f32 = self[0].z; - if sy < 1.0 - CMP_EPSILON { - if sy > -(1.0 - CMP_EPSILON) { - // is this a pure Y rotation? - if self[1].x == 0.0 - && self[0].y == 0.0 - && self[1].z == 0.0 - && self[2].y == 0.0 - && self[1].y == 1.0 - { - // return the simplest form (human friendlier in editor and scripts) - Vector3::new(0.0, f32::atan2(self[0].z, self[0].x), 0.0) - } else { - Vector3::new( - f32::atan2(-self[1].z, self[2].z), - f32::asin(sy), - f32::atan2(-self[0].y, self[0].x), - ) - } - } else { - Vector3::new(f32::atan2(self[2].y, self[1].y), -FRAC_PI_2, 0.0) + let major = self.rows[1].z; + match Self::is_between_neg1_1(major) { + // Is it a pure Y rotation? + Ordering::Equal if row_b == Vec3::Y && col_b == Vec3::Y => { + // return the simplest form (human friendlier in editor and scripts) + Vec3::new(0.0, f32::atan2(self.rows[0].z, self.rows[0].x), 0.0) } - } else { - Vector3::new(f32::atan2(self[2].y, self[1].y), FRAC_PI_2, 0.0) + _ => -Self::to_euler_inner(self.rows[0].z, row_a.yx(), col_c.yz(), col_b.zy()) + .yxz(), } } EulerOrder::XZY => { - // Euler angles in XZY convention. - // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix - // - // rot = cz*cy -sz cz*sy - // sx*sy+cx*cy*sz cx*cz cx*sz*sy-cy*sx - // cy*sx*sz cz*sx cx*cy+sx*sz*sy - - let sz: f32 = self[0].y; - if sz < (1.0 - CMP_EPSILON) { - if sz > -(1.0 - CMP_EPSILON) { - Vector3::new( - f32::atan2(self[2].y, self[1].y), - f32::atan2(self[0].z, self[0].x), - f32::asin(-sz), - ) - } else { - // It's -1 - Vector3::new(-f32::atan2(self[1].z, self[2].z), 0.0, FRAC_PI_2) - } - } else { - // It's 1 - Vector3::new(-f32::atan2(self[1].z, self[2].z), 0.0, -FRAC_PI_2) - } + Self::to_euler_inner(self.rows[0].y, row_a.zx(), col_b.zy(), col_c.yz()).yzx() } EulerOrder::YXZ => { - // Euler angles in YXZ convention. - // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix - // - // rot = cy*cz+sy*sx*sz cz*sy*sx-cy*sz cx*sy - // cx*sz cx*cz -sx - // cy*sx*sz-cz*sy cy*cz*sx+sy*sz cy*cx - - let m12: f32 = self[1].z; - - if m12 < (1.0 - CMP_EPSILON) { - if m12 > -(1.0 - CMP_EPSILON) { - // is this a pure X rotation? - if self[1].x == 0.0 - && self[0].y == 0.0 - && self[0].z == 0.0 - && self[2].x == 0.0 - && self[0].x == 1.0 - { - // return the simplest form (human friendlier in editor and scripts) - Vector3::new(f32::atan2(-m12, self[1].y), 0.0, 0.0) - } else { - Vector3::new( - f32::asin(-m12), - f32::atan2(self[0].z, self[2].z), - f32::atan2(self[1].x, self[1].y), - ) - } - } else { - // m12 == -1 - Vector3::new(FRAC_PI_2, f32::atan2(self[0].y, self[0].x), 0.0) + let major = self.rows[1].z; + let fst = row_b.xy(); + let snd = col_c.xz(); + let rest = row_a.yx(); + match Self::is_between_neg1_1(major) { + // Is it a pure X rotation? + Ordering::Equal if row_a == Vec3::X && col_a == Vec3::X => { + // return the simplest form (human friendlier in editor and scripts) + Vec3::new(f32::atan2(-major, self.rows[1].y), 0.0, 0.0) } - } else { - // m12 == 1 - Vector3::new(-FRAC_PI_2, -f32::atan2(self[0].y, self[0].x), 0.0) + Ordering::Less => { + Self::to_euler_inner(major, fst, snd, rest) * Vec3::new(1.0, -1.0, 1.0) + } + _ => Self::to_euler_inner(major, fst, snd, rest), } } EulerOrder::YZX => { - // Euler angles in YZX convention. - // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix - // - // rot = cy*cz sy*sx-cy*cx*sz cx*sy+cy*sz*sx - // sz cz*cx -cz*sx - // -cz*sy cy*sx+cx*sy*sz cy*cx-sy*sz*sx - - let sz: f32 = self[1].x; - if sz < (1.0 - CMP_EPSILON) { - if sz > -(1.0 - CMP_EPSILON) { - Vector3::new( - f32::atan2(-self[1].z, self[1].y), - f32::atan2(-self[2].x, self[0].x), - f32::asin(sz), - ) - } else { - // It's -1 - Vector3::new(f32::atan2(self[2].y, self[2].z), 0.0, -FRAC_PI_2) - } - } else { - // It's 1 - Vector3::new(f32::atan2(self[2].y, self[2].z), 0.0, FRAC_PI_2) - } + -Self::to_euler_inner(self.rows[1].x, col_a.zx(), row_b.zy(), row_c.yz()).yzx() } EulerOrder::ZXY => { - // Euler angles in ZXY convention. - // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix - // - // rot = cz*cy-sz*sx*sy -cx*sz cz*sy+cy*sz*sx - // cy*sz+cz*sx*sy cz*cx sz*sy-cz*cy*sx - // -cx*sy sx cx*cy - let sx: f32 = self[2].y; - if sx < (1.0 - CMP_EPSILON) { - if sx > -(1.0 - CMP_EPSILON) { - Vector3::new( - f32::asin(sx), - f32::atan2(-self[2].x, self[2].z), - f32::atan2(-self[0].y, self[1].y), - ) - } else { - // It's -1 - Vector3::new(-FRAC_PI_2, f32::atan2(self[0].z, self[0].x), 0.0) - } - } else { - // It's 1 - Vector3::new(FRAC_PI_2, f32::atan2(self[0].z, self[0].x), 0.0) - } + -Self::to_euler_inner(self.rows[2].y, col_b.xy(), row_c.xz(), row_a.zx()) } EulerOrder::ZYX => { - // Euler angles in ZYX convention. - // See https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix - // - // rot = cz*cy cz*sy*sx-cx*sz sz*sx+cz*cx*cy - // cy*sz cz*cx+sz*sy*sx cx*sz*sy-cz*sx - // -sy cy*sx cy*cx - let sy: f32 = self[2].x; - if sy < (1.0 - CMP_EPSILON) { - if sy > -(1.0 - CMP_EPSILON) { - Vector3::new( - f32::atan2(self[2].y, self[2].z), - f32::asin(-sy), - f32::atan2(self[1].x, self[0].x), - ) - } else { - // It's -1 - Vector3::new(0.0, FRAC_PI_2, -f32::atan2(self[0].y, self[1].y)) - } - } else { - // It's 1 - Vector3::new(0.0, -FRAC_PI_2, -f32::atan2(self[0].y, self[1].y)) - } + Self::to_euler_inner(self.rows[2].x, row_c.yz(), col_a.yx(), col_b.xy()).zxy() } } + .to_front() + } + + fn is_between_neg1_1(f: f32) -> Ordering { + if f >= (1.0 - CMP_EPSILON) { + Ordering::Greater + } else if f <= -(1.0 - CMP_EPSILON) { + Ordering::Less + } else { + Ordering::Equal + } + } + + fn to_euler_inner(major: f32, fst: Vec2, snd: Vec2, rest: Vec2) -> Vec3 { + match Self::is_between_neg1_1(major) { + // It's -1 + Ordering::Less => Vec3::new(FRAC_PI_2, -f32::atan2(rest.x, rest.y), 0.0), + Ordering::Equal => Vec3::new( + f32::asin(-major), + f32::atan2(snd.x, snd.y), + f32::atan2(fst.x, fst.y), + ), + // It's 1 + Ordering::Greater => Vec3::new(-FRAC_PI_2, -f32::atan2(rest.x, rest.y), 0.0), + } } /// Returns the determinant of the matrix. /// /// _Godot equivalent: `Basis.determinant()`_ - #[must_use] pub fn determinant(&self) -> f32 { self.to_glam().determinant() } @@ -418,9 +306,7 @@ impl Basis { /// _Godot equivalent: `Basis.transposed()`_ #[must_use] pub fn transposed(self) -> Self { - Self { - rows: [self.col_x(), self.col_y(), self.col_z()], - } + Self::from_cols(self.rows[0], self.rows[1], self.rows[2]) } /// Returns the orthonormalized version of the matrix (useful to call from @@ -436,14 +322,14 @@ impl Basis { ); // Gram-Schmidt Process - let mut x = self.col_x(); - let mut y = self.col_y(); - let mut z = self.col_z(); + 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 - x * x.dot(y); y = y.normalized(); - z = z - x * (x.dot(z)) - y * (y.dot(z)); + z = z - x * x.dot(z) - y * y.dot(z); z = z.normalized(); Self::from_cols(x, y, z) @@ -470,7 +356,7 @@ impl Basis { let mut result = Self::from_quat(from.slerp(to, weight)); for i in 0..3 { - result[i] *= lerp(self[i].length(), other[i].length(), weight); + result.rows[i] *= lerp(self.rows[i].length(), other.rows[i].length(), weight); } result @@ -481,7 +367,7 @@ impl Basis { /// _Godot equivalent: `Basis.tdotx()`_ #[must_use] pub fn tdotx(&self, with: Vector3) -> f32 { - self.col_x().dot(with) + self.col_a().dot(with) } /// Transposed dot product with the Y axis (column) of the matrix. @@ -489,7 +375,7 @@ impl Basis { /// _Godot equivalent: `Basis.tdoty()`_ #[must_use] pub fn tdoty(&self, with: Vector3) -> f32 { - self.col_y().dot(with) + self.col_b().dot(with) } /// Transposed dot product with the Z axis (column) of the matrix. @@ -497,16 +383,15 @@ impl Basis { /// _Godot equivalent: `Basis.tdotz()`_ #[must_use] pub fn tdotz(&self, with: Vector3) -> f32 { - self.col_z().dot(with) + self.col_c().dot(with) } /// Returns `true` if this basis is finite. Meaning each element of the /// matrix is not `NaN`, positive infinity, or negative infinity. /// /// _Godot equivalent: `Basis.is_finite()`_ - #[must_use] pub fn is_finite(&self) -> bool { - self[0].is_finite() && self[1].is_finite() && self[2].is_finite() + self.rows[0].is_finite() && self.rows[1].is_finite() && self.rows[2].is_finite() } /// Returns `true` if this basis and `other` are approximately equal, @@ -514,54 +399,54 @@ impl Basis { /// /// _Godot equivalent: `Basis.is_equal_approx(Basis b)`_ pub fn is_equal_approx(&self, other: &Self) -> bool { - self[0].is_equal_approx(other[0]) - && self[1].is_equal_approx(other[1]) - && self[2].is_equal_approx(other[2]) + self.rows[0].is_equal_approx(other.rows[0]) + && self.rows[1].is_equal_approx(other.rows[1]) + && self.rows[2].is_equal_approx(other.rows[2]) } /// Returns the first column of the matrix, /// /// _Godot equivalent: `Basis.x`_ #[must_use] - pub fn col_x(&self) -> Vector3 { - Vector3::new(self[0].x, self[1].x, self[2].x) + pub fn col_a(&self) -> Vector3 { + Vector3::new(self.rows[0].x, self.rows[1].x, self.rows[2].x) } /// Set the values of the first column of the matrix. - pub fn set_col_x(&mut self, col: Vector3) { - self[0].x = col.x; - self[1].x = col.y; - self[2].x = col.z; + 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; } /// Returns the second column of the matrix, /// /// _Godot equivalent: `Basis.y`_ #[must_use] - pub fn col_y(&self) -> Vector3 { - Vector3::new(self[0].y, self[1].y, self[2].y) + pub fn col_b(&self) -> Vector3 { + Vector3::new(self.rows[0].y, self.rows[1].y, self.rows[2].y) } /// Set the values of the second column of the matrix. - pub fn set_col_y(&mut self, col: Vector3) { - self[0].y = col.x; - self[1].y = col.y; - self[2].y = col.z; + 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; } /// Returns the third column of the matrix, /// /// _Godot equivalent: `Basis.z`_ #[must_use] - pub fn col_z(&self) -> Vector3 { - Vector3::new(self[0].z, self[1].z, self[2].z) + pub fn col_c(&self) -> Vector3 { + Vector3::new(self.rows[0].z, self.rows[1].z, self.rows[2].z) } /// Set the values of the third column of the matrix. - pub fn set_col_z(&mut self, col: Vector3) { - self[0].z = col.x; - self[1].z = col.y; - self[2].z = col.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; } } @@ -570,29 +455,9 @@ impl Display for Basis { // Godot output: // [X: (1, 0, 0), Y: (0, 1, 0), Z: (0, 0, 1)] // Where X,Y,Z are the columns - let [Vector3 { - x: x1, - y: x2, - z: x3, - }, Vector3 { - x: y1, - y: y2, - z: y3, - }, Vector3 { - x: z1, - y: z2, - z: z3, - }] = self.to_cols(); + let [a, b, c] = self.to_cols(); - f.write_fmt(format_args!( - "[X: ({x1}, {x2}, {x3}), Y: ({y1}, {y2}, {y3}), Z: ({z1}, {z2}, {z3})]" - )) - } -} - -impl From for Basis { - fn from(quat: Quaternion) -> Self { - Basis::from_quat(quat) + write!(f, "[a: {a}, b: {b}, c: {c}]") } } @@ -624,24 +489,6 @@ impl GlamType for glam::f32::Mat3A { } } -impl Index for Basis { - type Output = Vector3; - - /// Indexes into the basis by rows. - #[inline] - fn index(&self, index: usize) -> &Self::Output { - &self.rows[index] - } -} - -impl IndexMut for Basis { - /// Indexes into the basis by rows. - #[inline] - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.rows[index] - } -} - impl Default for Basis { fn default() -> Self { Self::IDENTITY @@ -659,16 +506,17 @@ impl Mul for Basis { impl Mul for Basis { type Output = Self; - fn mul(self, rhs: f32) -> Self::Output { - (self.to_glam() * rhs).to_front() + fn mul(mut self, rhs: f32) -> Self::Output { + self *= rhs; + self } } impl MulAssign for Basis { fn mul_assign(&mut self, rhs: f32) { - self[0] *= rhs; - self[1] *= rhs; - self[2] *= rhs; + self.rows[0] *= rhs; + self.rows[1] *= rhs; + self.rows[2] *= rhs; } } @@ -697,6 +545,8 @@ pub enum EulerOrder { ZYX = 5, } +// TODO: remove when we improve/replace `assert_eq_approx` +#[doc(hidden)] impl From for i64 { fn from(order: EulerOrder) -> Self { order as i64 @@ -707,6 +557,8 @@ impl From for i64 { mod test { use std::f32::consts::{FRAC_PI_2, PI}; + use crate::assert_eq_approx; + use super::*; fn deg_to_rad(rotation: Vector3) -> Vector3 { @@ -738,65 +590,50 @@ mod test { // Euler to rotation let original_euler: Vector3 = deg_to_rad(deg_original_euler); - let to_rotation: Basis = Basis::from_euler( - rot_order, - original_euler.x, - original_euler.y, - original_euler.z, - ); + let to_rotation: Basis = Basis::from_euler(rot_order, original_euler); // Euler from rotation - let euler_from_rotation: Vector3 = to_rotation.euler_angles(rot_order); - let rotation_from_computed_euler: Basis = Basis::from_euler( - rot_order, - euler_from_rotation.x, - euler_from_rotation.y, - euler_from_rotation.z, - ); + let euler_from_rotation: Vector3 = to_rotation.to_euler(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_x() - Vector3::new(1.0, 0.0, 0.0)).length() <= 0.1, + (res.col_a() - Vector3::RIGHT).length() <= 0.1, "Fail due to X {} with {deg_original_euler} using {rot_order:?}", - res.col_x() + res.col_a() ); assert!( - (res.col_y() - Vector3::new(0.0, 1.0, 0.0)).length() <= 0.1, + (res.col_b() - Vector3::UP).length() <= 0.1, "Fail due to Y {} with {deg_original_euler} using {rot_order:?}", - res.col_y() + res.col_b() ); assert!( - (res.col_z() - Vector3::new(0.0, 0.0, 1.0)).length() <= 0.1, + (res.col_c() - Vector3::BACK).length() <= 0.1, "Fail due to Z {} with {deg_original_euler} using {rot_order:?}", - res.col_z() + res.col_c() ); // Double check `to_rotation` decomposing with XYZ rotation order. - let euler_xyz_from_rotation: Vector3 = to_rotation.euler_angles(EulerOrder::XYZ); - let rotation_from_xyz_computed_euler: Basis = Basis::from_euler( - EulerOrder::XYZ, - euler_xyz_from_rotation.x, - euler_xyz_from_rotation.y, - euler_xyz_from_rotation.z, - ); + let euler_xyz_from_rotation: Vector3 = to_rotation.to_euler(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; assert!( - (res.col_x() - Vector3::new(1.0, 0.0, 0.0)).length() <= 0.1, + (res.col_a() - Vector3::new(1.0, 0.0, 0.0)).length() <= 0.1, "Double check with XYZ rot order failed, due to X {} with {deg_original_euler} using {rot_order:?}", - res.col_x(), + res.col_a(), ); assert!( - (res.col_y() - Vector3::new(0.0, 1.0, 0.0)).length() <= 0.1, + (res.col_b() - Vector3::new(0.0, 1.0, 0.0)).length() <= 0.1, "Double check with XYZ rot order failed, due to Y {} with {deg_original_euler} using {rot_order:?}", - res.col_y(), + res.col_b(), ); assert!( - (res.col_z() - Vector3::new(0.0, 0.0, 1.0)).length() <= 0.1, + (res.col_c() - Vector3::new(0.0, 0.0, 1.0)).length() <= 0.1, "Double check with XYZ rot order failed, due to Z {} with {deg_original_euler} using {rot_order:?}", - res.col_z(), + res.col_c(), ); } @@ -804,60 +641,45 @@ mod test { fn consts_behavior_correct() { let v = Vector3::new(1.0, 2.0, 3.0); - assert!( - (Basis::IDENTITY * v).is_equal_approx(v), - "got {got}, expected {v}", - got = Basis::IDENTITY * v + assert_eq_approx!(Basis::IDENTITY * v, v, Vector3::is_equal_approx); + assert_eq_approx!( + Basis::FLIP_X * v, + Vector3::new(-v.x, v.y, v.z), + Vector3::is_equal_approx ); - let u = Vector3::new(-v.x, v.y, v.z); - assert!( - (Basis::FLIP_X * v).is_equal_approx(u), - "got {got}, expected {u}", - got = Basis::FLIP_X * v + assert_eq_approx!( + Basis::FLIP_Y * v, + Vector3::new(v.x, -v.y, v.z), + Vector3::is_equal_approx ); - let u = Vector3::new(v.x, -v.y, v.z); - assert!( - (Basis::FLIP_Y * v).is_equal_approx(u), - "got {got}, expected {u}", - got = Basis::FLIP_X * v - ); - let u = Vector3::new(v.x, v.y, -v.z); - assert!( - (Basis::FLIP_Z * v).is_equal_approx(u), - "got {got}, expected {u}", - got = Basis::FLIP_X * v + assert_eq_approx!( + Basis::FLIP_Z * v, + Vector3::new(v.x, v.y, -v.z), + Vector3::is_equal_approx ); } #[test] fn basic_rotation_correct() { - assert!( - (Basis::from_axis_angle(Vector3::FORWARD, 0.0) * Vector3::RIGHT) - .is_equal_approx(Vector3::RIGHT), - "got {got}, expected {expected}", - got = Basis::from_axis_angle(Vector3::FORWARD, 0.0) * Vector3::RIGHT, - expected = Vector3::RIGHT + assert_eq_approx!( + Basis::from_axis_angle(Vector3::FORWARD, 0.0) * Vector3::RIGHT, + Vector3::RIGHT, + Vector3::is_equal_approx, ); - assert!( - (Basis::from_axis_angle(Vector3::FORWARD, FRAC_PI_2) * Vector3::RIGHT) - .is_equal_approx(Vector3::DOWN), - "got {got}, expected {expected}", - got = Basis::from_axis_angle(Vector3::FORWARD, FRAC_PI_2) * Vector3::RIGHT, - expected = Vector3::DOWN + assert_eq_approx!( + Basis::from_axis_angle(Vector3::FORWARD, FRAC_PI_2) * Vector3::RIGHT, + Vector3::DOWN, + Vector3::is_equal_approx, ); - assert!( - (Basis::from_axis_angle(Vector3::FORWARD, PI) * Vector3::RIGHT) - .is_equal_approx(Vector3::LEFT), - "got {got}, expected {expected}", - got = Basis::from_axis_angle(Vector3::FORWARD, PI) * Vector3::RIGHT, - expected = Vector3::LEFT + assert_eq_approx!( + Basis::from_axis_angle(Vector3::FORWARD, PI) * Vector3::RIGHT, + Vector3::LEFT, + Vector3::is_equal_approx, ); - assert!( - (Basis::from_axis_angle(Vector3::FORWARD, PI + FRAC_PI_2) * Vector3::RIGHT) - .is_equal_approx(Vector3::UP), - "got {got}, expected {expected}", - got = Basis::from_axis_angle(Vector3::FORWARD, PI + FRAC_PI_2) * Vector3::RIGHT, - expected = Vector3::UP + assert_eq_approx!( + Basis::from_axis_angle(Vector3::FORWARD, PI + FRAC_PI_2) * Vector3::RIGHT, + Vector3::UP, + Vector3::is_equal_approx, ); } diff --git a/godot-core/src/builtin/quaternion.rs b/godot-core/src/builtin/quaternion.rs index c1580dd1f..10d80ab86 100644 --- a/godot-core/src/builtin/quaternion.rs +++ b/godot-core/src/builtin/quaternion.rs @@ -98,9 +98,8 @@ impl Quaternion { } } - // TODO: Figure out how godot actually treats "order", then make a match/if chain pub fn get_euler(self, order: EulerOrder) -> Vector3 { - Basis::from_quat(self).euler_angles(order) + Basis::from_quat(self).to_euler(order) } pub fn inverse(self) -> Self { diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs index 7622d88ca..4f3ffd05b 100644 --- a/godot-core/src/builtin/transform2d.rs +++ b/godot-core/src/builtin/transform2d.rs @@ -106,6 +106,7 @@ impl Transform2D { /// transformation is composed of rotation, scaling and translation. /// /// _Godot equivalent: `Transform2D.affine_inverse()`_ + #[must_use] pub fn affine_inverse(self) -> Self { self.glam(|aff| aff.inverse()) } @@ -120,6 +121,7 @@ impl Transform2D { /// Returns the transform's scale. /// /// _Godot equivalent: `Transform2D.get_scale()`_ + #[must_use] pub fn scale(&self) -> Vector2 { self.basis.scale() } @@ -127,6 +129,7 @@ impl Transform2D { /// Returns the transform's skew (in radians). /// /// _Godot equivalent: `Transform2D.get_skew()`_ + #[must_use] pub fn skew(&self) -> f32 { self.basis.skew() } @@ -135,6 +138,7 @@ impl Transform2D { /// a given `weight` (on the range of 0.0 to 1.0). /// /// _Godot equivalent: `Transform2D.interpolate_with()`_ + #[must_use] pub fn interpolate_with(self, other: Self, weight: f32) -> Self { Self::from_angle_scale_skew_origin( lerp_angle(self.rotation(), other.rotation(), weight), @@ -164,6 +168,7 @@ impl Transform2D { /// normalized axis vectors (scale of 1 or -1). /// /// _Godot equivalent: `Transform2D.orthonormalized()`_ + #[must_use] pub fn orthonormalized(self) -> Self { Self { basis: self.basis.orthonormalized(), @@ -177,6 +182,7 @@ impl Transform2D { /// This can be seen as transforming with respect to the global/parent frame. /// /// _Godot equivalent: `Transform2D.rotated()`_ + #[must_use] pub fn rotated(self, angle: f32) -> Self { Self::from_angle(angle) * self } @@ -187,6 +193,7 @@ impl Transform2D { /// This can be seen as transforming with respect to the local frame. /// /// _Godot equivalent: `Transform2D.rotated_local()`_ + #[must_use] pub fn rotated_local(self, angle: f32) -> Self { self * Self::from_angle(angle) } @@ -197,10 +204,11 @@ impl Transform2D { /// This can be seen as transforming with respect to the global/parent frame. /// /// _Godot equivalent: `Transform2D.scaled()`_ + #[must_use] pub fn scaled(self, scale: Vector2) -> Self { let mut basis = self.basis; - basis.set_row_x(basis.row_x() * scale.x); - basis.set_row_y(basis.row_y() * scale.y); + basis.set_row_a(basis.row_a() * scale.x); + basis.set_row_b(basis.row_b() * scale.y); Self { basis, origin: self.origin * scale, @@ -213,6 +221,7 @@ impl Transform2D { /// This can be seen as transforming with respect to the local frame. /// /// _Godot equivalent: `Transform2D.scaled_local()`_ + #[must_use] pub fn scaled_local(self, scale: Vector2) -> Self { Self { basis: self.basis.scaled(scale), @@ -226,6 +235,7 @@ impl Transform2D { /// This can be seen as transforming with respect to the global/parent frame. /// /// _Godot equivalent: `Transform2D.translated()`_ + #[must_use] pub fn translated(self, offset: Vector2) -> Self { Self { basis: self.basis, @@ -239,6 +249,7 @@ impl Transform2D { /// This can be seen as transforming with respect to the local frame. /// /// _Godot equivalent: `Transform2D.translated()`_ + #[must_use] pub fn translated_local(self, offset: Vector2) -> Self { Self { basis: self.basis, @@ -253,12 +264,10 @@ impl Display for Transform2D { // [X: (1, 2), Y: (3, 4), O: (5, 6)] // Where X,Y,O are the columns - let [Vector2 { x: x1, y: x2 }, Vector2 { x: y1, y: y2 }] = self.basis.cols; - let Vector2 { x: o1, y: o2 } = self.origin; + let [a, b] = self.basis.cols; + let o = self.origin; - f.write_fmt(format_args!( - "[X: ({x1}, {x2}), Y: ({y1}, {y2}), O: ({o1}, {o2})]" - )) + write!(f, "[a: {a}, b: {b}, o: {o}]") } } @@ -366,7 +375,6 @@ impl Basis2D { pub const FLIP_Y: Self = Self::from_diagonal(1.0, -1.0); /// Create a diagonal matrix from the given values. - #[must_use] pub const fn from_diagonal(x: f32, y: f32) -> Self { Self { cols: [Vector2::new(x, 0.0), Vector2::new(0.0, y)], @@ -374,19 +382,16 @@ impl Basis2D { } /// Create a new basis from 2 basis vectors. - #[must_use] pub const fn from_cols(x: Vector2, y: Vector2) -> Self { Self { cols: [x, y] } } /// Create a new basis from 2 row vectors. These are *not* basis vectors. - #[must_use] pub const fn from_rows(x: Vector2, y: Vector2) -> Self { Self::from_cols(x, y).transposed() } /// Create a `Basis2D` from an angle. - #[must_use] pub fn from_angle(angle: f32) -> Self { Mat2::from_angle(angle).to_front() } @@ -399,6 +404,7 @@ impl Basis2D { } /// Introduces an additional scaling. + #[must_use] pub fn scaled(self, scale: Vector2) -> Self { Self { cols: [self[0] * scale.x, self[1] * scale.y], @@ -406,7 +412,6 @@ impl Basis2D { } /// Returns the determinant of the matrix. - #[must_use] pub fn determinant(&self) -> f32 { self.glam(|mat| mat.determinant()) } @@ -418,7 +423,6 @@ impl Basis2D { } /// Returns whether each component is finite. - #[must_use] pub fn is_finite(&self) -> bool { self.glam(|mat| mat.is_finite()) } @@ -449,7 +453,6 @@ impl Basis2D { } /// Returns the rotation of the matrix - #[must_use] pub fn rotation(&self) -> f32 { // Translated from Godot f32::atan2(self[0].y, self[0].x) @@ -483,21 +486,21 @@ impl Basis2D { } } - pub fn set_row_x(&mut self, v: Vector2) { + pub fn set_row_a(&mut self, v: Vector2) { self[0].x = v.x; self[1].x = v.y; } - pub fn row_x(&self) -> Vector2 { + pub fn row_a(&self) -> Vector2 { Vector2::new(self[0].x, self[1].x) } - pub fn set_row_y(&mut self, v: Vector2) { + pub fn set_row_b(&mut self, v: Vector2) { self[0].y = v.x; self[1].y = v.y; } - pub fn row_y(&self) -> Vector2 { + pub fn row_b(&self) -> Vector2 { Vector2::new(self[0].y, self[1].y) } } @@ -510,9 +513,9 @@ impl Default for Basis2D { impl Display for Basis2D { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let [Vector2 { x: x1, y: x2 }, Vector2 { x: y1, y: y2 }] = self.cols; + let [a, b] = self.cols; - f.write_fmt(format_args!("[X: ({x1}, {x2}), Y: ({y1}, {y2}))]")) + write!(f, "[a: {a}, b: {b})]") } } diff --git a/godot-core/src/builtin/transform3d.rs b/godot-core/src/builtin/transform3d.rs index f1e2c4af7..9004bb2c9 100644 --- a/godot-core/src/builtin/transform3d.rs +++ b/godot-core/src/builtin/transform3d.rs @@ -77,10 +77,27 @@ impl Transform3D { } } + /// Constructs a let froma Projection by trimming the last row of + /// the projection matrix. + /// + /// _Godot equivalent: Transform3D(Projection from)_ + pub fn from_projection(proj: Projection) -> Self { + let a = Vector3::new(proj[0].x, proj[0].y, proj[0].z); + let b = Vector3::new(proj[1].x, proj[1].y, proj[1].z); + let c = Vector3::new(proj[2].x, proj[2].y, proj[2].z); + let o = Vector3::new(proj[3].x, proj[3].y, proj[3].z); + + Self { + basis: Basis::from_cols(a, b, c), + origin: o, + } + } + /// Returns the inverse of the transform, under the assumption that the /// transformation is composed of rotation, scaling and translation. /// /// _Godot equivalent: Transform3D.affine_inverse()_ + #[must_use] pub fn affine_inverse(self) -> Self { self.glam(|aff| aff.inverse()) } @@ -89,6 +106,7 @@ impl Transform3D { /// a given weight (on the range of 0.0 to 1.0). /// /// _Godot equivalent: Transform3D.interpolate_with()_ + #[must_use] pub fn interpolate_with(self, other: Self, weight: f32) -> Self { let src_scale = self.basis.scale(); let src_rot = self.basis.to_quat().normalized(); @@ -129,6 +147,7 @@ impl Transform3D { /// See [`Basis::new_looking_at()`] for more information. /// /// _Godot equivalent: Transform3D.looking_at()_ + #[must_use] pub fn looking_at(self, target: Vector3, up: Vector3) -> Self { Self { basis: Basis::new_looking_at(target - self.origin, up), @@ -140,6 +159,7 @@ impl Transform3D { /// normalized axis vectors (scale of 1 or -1). /// /// _Godot equivalent: Transform3D.orthonormalized()_ + #[must_use] pub fn orthonormalized(self) -> Self { Self { basis: self.basis.orthonormalized(), @@ -153,6 +173,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the global/parent frame. /// /// _Godot equivalent: `Transform2D.rotated()`_ + #[must_use] pub fn rotated(self, axis: Vector3, angle: f32) -> Self { let rotation = Basis::from_axis_angle(axis, angle); Self { @@ -166,6 +187,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the local frame. /// /// _Godot equivalent: `Transform2D.rotated_local()`_ + #[must_use] pub fn rotated_local(self, axis: Vector3, angle: f32) -> Self { Self { basis: self.basis * Basis::from_axis_angle(axis, angle), @@ -179,6 +201,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the global/parent frame. /// /// _Godot equivalent: `Transform2D.scaled()`_ + #[must_use] pub fn scaled(self, scale: Vector3) -> Self { Self { basis: Basis::from_scale(scale) * self.basis, @@ -192,6 +215,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the local frame. /// /// _Godot equivalent: `Transform2D.scaled_local()`_ + #[must_use] pub fn scaled_local(self, scale: Vector3) -> Self { Self { basis: self.basis * Basis::from_scale(scale), @@ -205,6 +229,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the global/parent frame. /// /// _Godot equivalent: `Transform2D.translated()`_ + #[must_use] pub fn translated(self, offset: Vector3) -> Self { Self { basis: self.basis, @@ -218,6 +243,7 @@ impl Transform3D { /// This can be seen as transforming with respect to the local frame. /// /// _Godot equivalent: `Transform2D.translated()`_ + #[must_use] pub fn translated_local(self, offset: Vector3) -> Self { Self { basis: self.basis, @@ -231,44 +257,10 @@ impl Display for Transform3D { // Godot output: // [X: (1, 2, 3), Y: (4, 5, 6), Z: (7, 8, 9), O: (10, 11, 12)] // Where X,Y,O are the columns - let [Vector3 { - x: x1, - y: x2, - z: x3, - }, Vector3 { - x: y1, - y: y2, - z: y3, - }, Vector3 { - x: z1, - y: z2, - z: z3, - }] = self.basis.to_cols(); - let Vector3 { - x: o1, - y: o2, - z: o3, - } = self.origin; - - f.write_fmt(format_args!("[X: ({x1}, {x2}, {x3}), Y: ({y1}, {y2}, {y3}), Z: ({z1}, {z2}, {z3}), O: ({o1}, {o2}, {o3})]")) - } -} + let [a, b, c] = self.basis.to_cols(); + let o = self.origin; -impl From for Transform3D { - /// Constructs a let froma Projection by trimming the last row of - /// the projection matrix. - /// - /// _Godot equivalent: Transform3D(Projection from)_ - fn from(proj: Projection) -> Self { - let v1 = Vector3::new(proj[0].x, proj[0].y, proj[0].z); - let v2 = Vector3::new(proj[1].x, proj[1].y, proj[1].z); - let v3 = Vector3::new(proj[2].x, proj[2].y, proj[2].z); - let v4 = Vector3::new(proj[3].x, proj[3].y, proj[3].z); - - Self { - basis: Basis::from_cols(v1, v2, v3), - origin: v4, - } + write!(f, "[a: {a}, b: {b}, c: {c}, o: {o}]") } } diff --git a/itest/rust/src/basis_test.rs b/itest/rust/src/basis_test.rs index addf8e71d..41e86453c 100644 --- a/itest/rust/src/basis_test.rs +++ b/itest/rust/src/basis_test.rs @@ -5,7 +5,10 @@ */ use crate::{assert_inner_equivalent, itest}; -use godot::prelude::{inner::InnerBasis, *}; +use godot::{ + prelude::{inner::InnerBasis, *}, + private::class_macros::assert_eq_approx, +}; const TEST_BASIS: Basis = Basis::from_rows( Vector3::new(0.942155, -0.270682, 0.197677), @@ -29,10 +32,7 @@ fn basis_multiply_same() { .evaluate(&Basis::IDENTITY.to_variant(), VariantOperator::Multiply) .unwrap() .to::(); - assert!( - (rust_res).is_equal_approx(&godot_res), - "got = {rust_res}, expected = {godot_res}" - ); + assert_eq_approx!(rust_res, godot_res, |a, b| Basis::is_equal_approx(&a, &b)); let rhs = Basis::from_axis_angle(Vector3::new(1.0, 2.0, 3.0).normalized(), 0.5); let rust_res = TEST_BASIS * rhs; @@ -41,10 +41,7 @@ fn basis_multiply_same() { .evaluate(&rhs.to_variant(), VariantOperator::Multiply) .unwrap() .to::(); - assert!( - (rust_res).is_equal_approx(&godot_res), - "got = {rust_res}, expected = {godot_res}" - ); + assert_eq_approx!(rust_res, godot_res, |a, b| Basis::is_equal_approx(&a, &b)); } #[itest] @@ -116,7 +113,7 @@ fn basis_euler_angles_same() { for order in euler_order_to_test.into_iter() { for vector in vectors_to_test.iter() { let vector = deg_to_rad(*vector); - let rust_basis = Basis::from_euler(order, vector.x, vector.y, vector.z); + let rust_basis = Basis::from_euler(order, vector); let godot_basis = InnerBasis::from_euler(vector, order as i64); assert!( (rust_basis).is_equal_approx(&godot_basis), @@ -151,29 +148,29 @@ fn basis_equiv() { assert_inner_equivalent!( InnerBasis, TEST_BASIS.scaled(Vector3::new(1.0, 2.0, 3.0)), - |a, b| { Basis::is_equal_approx(&a, &b) } + |a, b| Basis::is_equal_approx(&a, &b) ); assert_inner_equivalent!(InnerBasis, TEST_BASIS.(scale, get_scale)(), Vector3::is_equal_approx); - assert_inner_equivalent!(InnerBasis, TEST_BASIS.(euler_angles, get_euler)(EulerOrder::XYZ), Vector3::is_equal_approx); + assert_inner_equivalent!(InnerBasis, TEST_BASIS.(to_euler, get_euler)(EulerOrder::XYZ), Vector3::is_equal_approx); assert_inner_equivalent!( InnerBasis, TEST_BASIS.tdotx(Vector3::new(1.0, 2.0, 3.0)), - |a, b| { is_equal_approx(a, b as f32) } + |a, b| is_equal_approx(a, b as f32) ); assert_inner_equivalent!( InnerBasis, TEST_BASIS.tdoty(Vector3::new(1.0, 2.0, 3.0)), - |a, b| { is_equal_approx(a, b as f32) } + |a, b| is_equal_approx(a, b as f32) ); assert_inner_equivalent!( InnerBasis, TEST_BASIS.tdotz(Vector3::new(1.0, 2.0, 3.0)), - |a, b| { is_equal_approx(a, b as f32) } + |a, b| is_equal_approx(a, b as f32) ); assert_inner_equivalent!( InnerBasis, TEST_BASIS.slerp(Basis::IDENTITY, 0.5), - |a, b| { Basis::is_equal_approx(&a, &b) } + |a, b| Basis::is_equal_approx(&a, &b) ); assert_inner_equivalent!(InnerBasis, TEST_BASIS.(to_quat, get_rotation_quaternion)(), Quaternion::is_equal_approx); } diff --git a/itest/rust/src/transform2d_test.rs b/itest/rust/src/transform2d_test.rs index c4f7211bc..a132e6629 100644 --- a/itest/rust/src/transform2d_test.rs +++ b/itest/rust/src/transform2d_test.rs @@ -35,7 +35,7 @@ fn transform2d_equiv() { assert_inner_equivalent!( InnerTransform2D, TEST_TRANSFORM.orthonormalized(), - |a, b| { Transform2D::is_equal_approx(&a, &b) } + |a, b| Transform2D::is_equal_approx(&a, &b) ); assert_inner_equivalent!(InnerTransform2D, TEST_TRANSFORM.rotated(1.0), |a, b| { Transform2D::is_equal_approx(&a, &b) @@ -43,31 +43,31 @@ fn transform2d_equiv() { assert_inner_equivalent!( InnerTransform2D, TEST_TRANSFORM.rotated_local(1.0), - |a, b| { Transform2D::is_equal_approx(&a, &b) } + |a, b| Transform2D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform2D, TEST_TRANSFORM.scaled(Vector2::new(1.0, 2.0)), - |a, b| { Transform2D::is_equal_approx(&a, &b) } + |a, b| Transform2D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform2D, TEST_TRANSFORM.scaled_local(Vector2::new(1.0, 2.0)), - |a, b| { Transform2D::is_equal_approx(&a, &b) } + |a, b| Transform2D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform2D, - TEST_TRANSFORM.translated_local(Vector2::new(1.0, 2.0)), - |a, b| { Transform2D::is_equal_approx(&a, &b) } + TEST_TRANSFORM.translated(Vector2::new(1.0, 2.0)), + |a, b| Transform2D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform2D, TEST_TRANSFORM.translated_local(Vector2::new(1.0, 2.0)), - |a, b| { Transform2D::is_equal_approx(&a, &b) } + |a, b| Transform2D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform2D, TEST_TRANSFORM.interpolate_with(Transform2D::IDENTITY, 0.5), - |a, b| { Transform2D::is_equal_approx(&a, &b) } + |a, b| Transform2D::is_equal_approx(&a, &b) ); } diff --git a/itest/rust/src/transform3d_test.rs b/itest/rust/src/transform3d_test.rs index fa9d49ad0..25c31c445 100644 --- a/itest/rust/src/transform3d_test.rs +++ b/itest/rust/src/transform3d_test.rs @@ -30,46 +30,46 @@ fn transform3d_equiv() { assert_inner_equivalent!( InnerTransform3D, TEST_TRANSFORM.orthonormalized(), - |a, b| { Transform3D::is_equal_approx(&a, &b) } + |a, b| Transform3D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform3D, TEST_TRANSFORM.rotated(Vector3::new(1.0, 2.0, 3.0).normalized(), 1.0), - |a, b| { Transform3D::is_equal_approx(&a, &b) } + |a, b| Transform3D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform3D, TEST_TRANSFORM.rotated_local(Vector3::new(1.0, 2.0, 3.0).normalized(), 1.0), - |a, b| { Transform3D::is_equal_approx(&a, &b) } + |a, b| Transform3D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform3D, TEST_TRANSFORM.scaled(Vector3::new(1.0, 2.0, 3.0)), - |a, b| { Transform3D::is_equal_approx(&a, &b) } + |a, b| Transform3D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform3D, TEST_TRANSFORM.scaled_local(Vector3::new(1.0, 2.0, 3.0)), - |a, b| { Transform3D::is_equal_approx(&a, &b) } + |a, b| Transform3D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform3D, - TEST_TRANSFORM.translated_local(Vector3::new(1.0, 2.0, 3.0)), - |a, b| { Transform3D::is_equal_approx(&a, &b) } + TEST_TRANSFORM.translated(Vector3::new(1.0, 2.0, 3.0)), + |a, b| Transform3D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform3D, TEST_TRANSFORM.translated_local(Vector3::new(1.0, 2.0, 3.0)), - |a, b| { Transform3D::is_equal_approx(&a, &b) } + |a, b| Transform3D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform3D, TEST_TRANSFORM.looking_at(Vector3::new(1.0, 2.0, 3.0), Vector3::UP), - |a, b| { Transform3D::is_equal_approx(&a, &b) } + |a, b| Transform3D::is_equal_approx(&a, &b) ); assert_inner_equivalent!( InnerTransform3D, TEST_TRANSFORM.interpolate_with(Transform3D::IDENTITY, 0.5), - |a, b| { Transform3D::is_equal_approx(&a, &b) } + |a, b| Transform3D::is_equal_approx(&a, &b) ); } From c79b0b8986df92dc99ffebd6544d28aacaa563f7 Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Sun, 19 Feb 2023 23:45:40 +0100 Subject: [PATCH 12/15] Remove more indices --- godot-core/src/builtin/projection.rs | 24 ++------ godot-core/src/builtin/transform2d.rs | 82 +++++++-------------------- godot-core/src/builtin/transform3d.rs | 10 ++-- 3 files changed, 31 insertions(+), 85 deletions(-) diff --git a/godot-core/src/builtin/projection.rs b/godot-core/src/builtin/projection.rs index 3247fe102..f3f889552 100644 --- a/godot-core/src/builtin/projection.rs +++ b/godot-core/src/builtin/projection.rs @@ -240,7 +240,7 @@ impl Projection { /// _Godot equivalent: Projection.flipped_y()_ pub fn flipped_y(self) -> Self { Self { - cols: [self[0], -self[1], self[2], self[3]], + cols: [self.cols[0], -self.cols[1], self.cols[2], self.cols[3]], } } @@ -389,20 +389,6 @@ impl Mul for Projection { } } -impl Index for Projection { - type Output = Vector4; - - fn index(&self, index: usize) -> &Self::Output { - &self.cols[index] - } -} - -impl IndexMut for Projection { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.cols[index] - } -} - impl GlamType for Mat4 { type Mapped = Projection; @@ -419,10 +405,10 @@ impl GlamType for Mat4 { fn from_front(mapped: &Self::Mapped) -> Self { Self { - x_axis: mapped[0].to_glam(), - y_axis: mapped[1].to_glam(), - z_axis: mapped[2].to_glam(), - w_axis: mapped[3].to_glam(), + x_axis: mapped.cols[0].to_glam(), + y_axis: mapped.cols[1].to_glam(), + z_axis: mapped.cols[2].to_glam(), + w_axis: mapped.cols[3].to_glam(), } } } diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs index 4f3ffd05b..37ae49fa2 100644 --- a/godot-core/src/builtin/transform2d.rs +++ b/godot-core/src/builtin/transform2d.rs @@ -95,10 +95,10 @@ impl Transform2D { ) -> Self { // Translated from Godot's implementation let mut basis = Basis2D::IDENTITY; - basis[0].x = angle.cos() * scale.x; - basis[1].y = (angle + skew).cos() * scale.y; - basis[1].x = -(angle + skew).sin() * scale.y; - basis[0].y = angle.sin() * scale.x; + basis.cols[0].x = angle.cos() * scale.x; + basis.cols[1].y = (angle + skew).cos() * scale.y; + basis.cols[1].x = -(angle + skew).sin() * scale.y; + basis.cols[0].y = angle.sin() * scale.x; Self { basis, origin } } @@ -271,30 +271,6 @@ impl Display for Transform2D { } } -impl Index for Transform2D { - type Output = Vector2; - - #[inline] - fn index(&self, index: usize) -> &Self::Output { - match index { - 0 | 1 => &self.basis[index], - 2 => &self.origin, - _ => panic!("Index {index} out of bounds"), - } - } -} - -impl IndexMut for Transform2D { - #[inline] - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - match index { - 0 | 1 => &mut self.basis[index], - 2 => &mut self.origin, - _ => panic!("Index {index} out of bounds"), - } - } -} - impl Mul for Transform2D { type Output = Self; @@ -400,14 +376,14 @@ impl Basis2D { #[must_use] pub fn scale(&self) -> Vector2 { let det_sign = self.determinant().signum(); - Vector2::new(self[0].length(), det_sign * self[1].length()) + Vector2::new(self.cols[0].length(), det_sign * self.cols[1].length()) } /// Introduces an additional scaling. #[must_use] pub fn scaled(self, scale: Vector2) -> Self { Self { - cols: [self[0] * scale.x, self[1] * scale.y], + cols: [self.cols[0] * scale.x, self.cols[1] * scale.y], } } @@ -436,8 +412,8 @@ impl Basis2D { ); // Gram-Schmidt Process - let mut x = self[0]; - let mut y = self[1]; + let mut x = self.cols[0]; + let mut y = self.cols[1]; x = x.normalized(); y = y - x * (x.dot(y)); @@ -455,7 +431,7 @@ impl Basis2D { /// Returns the rotation of the matrix pub fn rotation(&self) -> f32 { // Translated from Godot - f32::atan2(self[0].y, self[0].x) + f32::atan2(self.cols[0].y, self.cols[0].x) } /// Returns the skew of the matrix @@ -463,9 +439,9 @@ impl Basis2D { pub fn skew(&self) -> f32 { // Translated from Godot let det_sign = self.determinant().signum(); - self[0] + self.cols[0] .normalized() - .dot(det_sign * self[1].normalized()) + .dot(det_sign * self.cols[1].normalized()) .acos() - PI * 0.5 } @@ -473,7 +449,7 @@ impl Basis2D { /// Returns `true` if this basis and `other` are approximately equal, /// by calling `is_equal_approx` on each column. pub fn is_equal_approx(&self, other: &Self) -> bool { - self[0].is_equal_approx(other[0]) && self[1].is_equal_approx(other[1]) + self.cols[0].is_equal_approx(other.cols[0]) && self.cols[1].is_equal_approx(other.cols[1]) } /// Returns the transposed version of the matrix. @@ -487,21 +463,21 @@ impl Basis2D { } pub fn set_row_a(&mut self, v: Vector2) { - self[0].x = v.x; - self[1].x = v.y; + self.cols[0].x = v.x; + self.cols[1].x = v.y; } pub fn row_a(&self) -> Vector2 { - Vector2::new(self[0].x, self[1].x) + Vector2::new(self.cols[0].x, self.cols[1].x) } pub fn set_row_b(&mut self, v: Vector2) { - self[0].y = v.x; - self[1].y = v.y; + self.cols[0].y = v.x; + self.cols[1].y = v.y; } pub fn row_b(&self) -> Vector2 { - Vector2::new(self[0].y, self[1].y) + Vector2::new(self.cols[0].y, self.cols[1].y) } } @@ -537,8 +513,8 @@ impl Mul for Basis2D { impl MulAssign for Basis2D { fn mul_assign(&mut self, rhs: f32) { - self[0] *= rhs; - self[1] *= rhs; + self.cols[0] *= rhs; + self.cols[1] *= rhs; } } @@ -550,22 +526,6 @@ impl Mul for Basis2D { } } -impl Index for Basis2D { - type Output = Vector2; - - #[inline] - fn index(&self, index: usize) -> &Self::Output { - &self.cols[index] - } -} - -impl IndexMut for Basis2D { - #[inline] - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.cols[index] - } -} - impl GlamType for Mat2 { type Mapped = Basis2D; @@ -576,7 +536,7 @@ impl GlamType for Mat2 { } fn from_front(mapped: &Self::Mapped) -> Self { - Self::from_cols(mapped[0].to_glam(), mapped[1].to_glam()) + Self::from_cols(mapped.cols[0].to_glam(), mapped.cols[1].to_glam()) } } diff --git a/godot-core/src/builtin/transform3d.rs b/godot-core/src/builtin/transform3d.rs index 9004bb2c9..a6078f38d 100644 --- a/godot-core/src/builtin/transform3d.rs +++ b/godot-core/src/builtin/transform3d.rs @@ -77,15 +77,15 @@ impl Transform3D { } } - /// Constructs a let froma Projection by trimming the last row of + /// Constructs a let from a Projection by trimming the last row of /// the projection matrix. /// /// _Godot equivalent: Transform3D(Projection from)_ pub fn from_projection(proj: Projection) -> Self { - let a = Vector3::new(proj[0].x, proj[0].y, proj[0].z); - let b = Vector3::new(proj[1].x, proj[1].y, proj[1].z); - let c = Vector3::new(proj[2].x, proj[2].y, proj[2].z); - let o = Vector3::new(proj[3].x, proj[3].y, proj[3].z); + let a = Vector3::new(proj.cols[0].x, proj.cols[0].y, proj.cols[0].z); + let b = Vector3::new(proj.cols[1].x, proj.cols[1].y, proj.cols[1].z); + let c = Vector3::new(proj.cols[2].x, proj.cols[2].y, proj.cols[2].z); + let o = Vector3::new(proj.cols[3].x, proj.cols[3].y, proj.cols[3].z); Self { basis: Basis::from_cols(a, b, c), From bf768942ccc6e090e9deed74097be4cc8c385194 Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Thu, 2 Mar 2023 02:44:52 +0100 Subject: [PATCH 13/15] More changes from review --- .gitignore | 1 - godot-core/src/builtin/basis.rs | 41 ++--- godot-core/src/builtin/math.rs | 14 +- godot-core/src/builtin/projection.rs | 242 ++++++++++++++------------ godot-core/src/builtin/quaternion.rs | 4 +- godot-core/src/builtin/transform2d.rs | 58 +++--- godot-core/src/builtin/transform3d.rs | 25 ++- itest/rust/src/basis_test.rs | 103 +++++------ itest/rust/src/lib.rs | 21 --- itest/rust/src/transform2d_test.rs | 90 +++++----- itest/rust/src/transform3d_test.rs | 75 +++----- 11 files changed, 316 insertions(+), 358 deletions(-) diff --git a/.gitignore b/.gitignore index 3ce4a48b8..28bf18c16 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ .run # Rust -ra_target target Cargo.lock diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index fa0bb49e0..cd65357bb 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -6,14 +6,13 @@ use std::cmp::Ordering; */ use std::{f32::consts::FRAC_PI_2, fmt::Display, ops::*}; -use glam::{Vec2, Vec3}; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; use super::glam_helpers::{GlamConv, GlamType}; use super::{math::*, Quaternion, Vector3}; -use glam::f32::Mat3; +use glam; /// A 3x3 matrix, typically used as an orthogonal basis for [`Transform3D`](crate::builtin::Transform3D). /// @@ -23,7 +22,7 @@ use glam::f32::Mat3; /// /// The basis vectors are the columns of the matrix, whereas the [`rows`](Self::rows) field represents /// the row vectors. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Copy, Clone, PartialEq, Debug)] #[repr(C)] pub struct Basis { /// The rows of the matrix. These are *not* the basis vectors. @@ -61,15 +60,15 @@ impl Basis { } /// Create a new basis from 3 column vectors. - pub const fn from_cols(x: Vector3, y: Vector3, z: Vector3) -> Self { - Self::from_rows_array(&[x.x, y.x, z.x, x.y, y.y, z.y, x.z, y.z, z.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]) } /// Create a `Basis` from an axis and angle. /// /// _Godot equivalent: `Basis(Vector3 axis, float angle)`_ pub fn from_axis_angle(axis: Vector3, angle: f32) -> Self { - Mat3::from_axis_angle(axis.to_glam(), angle).to_front() + glam::Mat3::from_axis_angle(axis.to_glam(), angle).to_front() } /// Create a diagonal matrix from the given values. @@ -103,7 +102,7 @@ impl Basis { /// /// _Godot equivalent: `Basis(Quaternion from)`_ pub fn from_quat(quat: Quaternion) -> Self { - Mat3::from_quat(quat.to_glam()).to_front() + glam::Mat3::from_quat(quat.to_glam()).to_front() } /// Create a `Basis` from three angles `a`, `b`, and `c` interpreted @@ -199,6 +198,7 @@ impl Basis { /// _Godot equivalent: `Basis.get_euler()`_ pub fn to_euler(self, order: EulerOrder) -> Vector3 { use glam::swizzles::Vec3Swizzles as _; + use glam::Vec3; let col_a = self.col_a().to_glam(); let col_b = self.col_b().to_glam(); @@ -264,17 +264,22 @@ impl Basis { } } - fn to_euler_inner(major: f32, fst: Vec2, snd: Vec2, rest: Vec2) -> Vec3 { + fn to_euler_inner( + major: f32, + fst: glam::Vec2, + snd: glam::Vec2, + rest: glam::Vec2, + ) -> glam::Vec3 { match Self::is_between_neg1_1(major) { // It's -1 - Ordering::Less => Vec3::new(FRAC_PI_2, -f32::atan2(rest.x, rest.y), 0.0), - Ordering::Equal => Vec3::new( + Ordering::Less => glam::Vec3::new(FRAC_PI_2, -f32::atan2(rest.x, rest.y), 0.0), + Ordering::Equal => glam::Vec3::new( f32::asin(-major), f32::atan2(snd.x, snd.y), f32::atan2(fst.x, fst.y), ), // It's 1 - Ordering::Greater => Vec3::new(-FRAC_PI_2, -f32::atan2(rest.x, rest.y), 0.0), + Ordering::Greater => glam::Vec3::new(-FRAC_PI_2, -f32::atan2(rest.x, rest.y), 0.0), } } @@ -462,10 +467,10 @@ impl Display for Basis { } impl GlamConv for Basis { - type Glam = glam::f32::Mat3; + type Glam = glam::Mat3; } -impl GlamType for glam::f32::Mat3 { +impl GlamType for glam::Mat3 { type Mapped = Basis; fn to_front(&self) -> Self::Mapped { @@ -477,7 +482,7 @@ impl GlamType for glam::f32::Mat3 { } } -impl GlamType for glam::f32::Mat3A { +impl GlamType for glam::Mat3A { type Mapped = Basis; fn to_front(&self) -> Self::Mapped { @@ -545,14 +550,6 @@ pub enum EulerOrder { ZYX = 5, } -// TODO: remove when we improve/replace `assert_eq_approx` -#[doc(hidden)] -impl From for i64 { - fn from(order: EulerOrder) -> Self { - order as i64 - } -} - #[cfg(test)] mod test { use std::f32::consts::{FRAC_PI_2, PI}; diff --git a/godot-core/src/builtin/math.rs b/godot-core/src/builtin/math.rs index 880c0cb78..c5efc9059 100644 --- a/godot-core/src/builtin/math.rs +++ b/godot-core/src/builtin/math.rs @@ -205,15 +205,9 @@ mod test { is_equal_approx ); let angle = PI * 2.0 / 3.0; - assert_eq_approx!( - lerp_angle(-5.0 * TAU, angle + 3.0 * TAU, 0.5).sin(), - (angle / 2.0).sin(), - is_equal_approx - ); - assert_eq_approx!( - lerp_angle(-5.0 * TAU, angle + 3.0 * TAU, 0.5).cos(), - (angle / 2.0).cos(), - is_equal_approx - ); + let (s1, c1) = lerp_angle(-5.0 * TAU, angle + 3.0 * TAU, 0.5).sin_cos(); + let (s2, c2) = (angle / 2.0).sin_cos(); + assert_eq_approx!(s1, s2, is_equal_approx); + assert_eq_approx!(c1, c2, is_equal_approx); } } diff --git a/godot-core/src/builtin/projection.rs b/godot-core/src/builtin/projection.rs index f3f889552..473d0f95d 100644 --- a/godot-core/src/builtin/projection.rs +++ b/godot-core/src/builtin/projection.rs @@ -8,13 +8,10 @@ use std::ops::*; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; -use super::{ - glam_helpers::{GlamConv, GlamType}, - inner::InnerProjection, - Plane, Transform3D, Vector2, Vector4, -}; +use super::glam_helpers::{GlamConv, GlamType}; +use super::{inner::InnerProjection, Plane, Transform3D, Vector2, Vector4}; -use glam::f32::Mat4; +use glam; /// A 4x4 matrix used for 3D projective transformations. It can represent /// transformations such as translation, rotation, scaling, shearing, and /// perspective division. It consists of four Vector4 columns. @@ -27,7 +24,7 @@ use glam::f32::Mat4; /// /// Note: The current implementation largely makes calls to godot for its /// methods and as such are not as performant as other types. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Copy, Clone, PartialEq, Debug)] #[repr(C)] pub struct Projection { /// The columns of the projection matrix. @@ -44,32 +41,27 @@ impl Projection { /// A Projection with all values initialized to 0. When applied to other /// data structures, they will be zeroed. /// - /// _Godot equivalent: Projection.ZERO + /// _Godot equivalent: Projection.ZERO_ pub const ZERO: Self = Self::from_diagonal(0.0, 0.0, 0.0, 0.0); /// Create a new projection from a list of column vectors. - #[must_use] pub const fn new(cols: [Vector4; 4]) -> Self { Self { cols } } /// Create a diagonal matrix from the given values. - #[must_use] pub const fn from_diagonal(x: f32, y: f32, z: f32, w: f32) -> Self { - Self { - cols: [ - Vector4::new(x, 0.0, 0.0, 0.0), - Vector4::new(0.0, y, 0.0, 0.0), - Vector4::new(0.0, 0.0, z, 0.0), - Vector4::new(0.0, 0.0, 0.0, w), - ], - } + Self::from_cols( + Vector4::new(x, 0.0, 0.0, 0.0), + Vector4::new(0.0, y, 0.0, 0.0), + Vector4::new(0.0, 0.0, z, 0.0), + Vector4::new(0.0, 0.0, 0.0, w), + ) } /// Create a matrix from four column vectors. /// /// _Godot equivalent: Projection(Vector4 x_axis, Vector4 y_axis, Vector4 z_axis, Vector4 w_axis)_ - #[must_use] pub const fn from_cols(x: Vector4, y: Vector4, z: Vector4, w: Vector4) -> Self { Self { cols: [x, y, z, w] } } @@ -90,24 +82,24 @@ impl Projection { /// _Godot equivalent: Projection.create_for_hmd()_ #[allow(clippy::too_many_arguments)] pub fn create_for_hmd( - eye: Eye, - aspect: f64, - intraocular_dist: f64, - display_width: f64, - display_to_lens: f64, - oversample: f64, - near: f64, - far: f64, + eye: ProjectionEye, + aspect: f32, + intraocular_dist: f32, + display_width: f32, + display_to_lens: f32, + oversample: f32, + near: f32, + far: f32, ) -> Self { InnerProjection::create_for_hmd( eye as i64, - aspect, - intraocular_dist, - display_width, - display_to_lens, - oversample, - near, - far, + aspect as f64, + intraocular_dist as f64, + display_width as f64, + display_to_lens as f64, + oversample as f64, + near as f64, + far as f64, ) } @@ -116,14 +108,21 @@ impl Projection { /// /// _Godot equivalent: Projection.create_frustum()_ pub fn create_frustum( - left: f64, - right: f64, - bottom: f64, - top: f64, - near: f64, - far: f64, + left: f32, + right: f32, + bottom: f32, + top: f32, + near: f32, + far: f32, ) -> Self { - InnerProjection::create_frustum(left, right, bottom, top, near, far) + InnerProjection::create_frustum( + left as f64, + right as f64, + bottom as f64, + top as f64, + near as f64, + far as f64, + ) } /// Creates a new Projection that projects positions in a frustum with the @@ -134,14 +133,21 @@ impl Projection { /// /// _Godot equivalent: Projection.create_frustum_aspect()_ pub fn create_frustum_aspect( - size: f64, - aspect: f64, + size: f32, + aspect: f32, offset: Vector2, - near: f64, - far: f64, + near: f32, + far: f32, flip_fov: bool, ) -> Self { - InnerProjection::create_frustum_aspect(size, aspect, offset, near, far, flip_fov) + InnerProjection::create_frustum_aspect( + size as f64, + aspect as f64, + offset, + near as f64, + far as f64, + flip_fov, + ) } /// Creates a new Projection that projects positions using an orthogonal @@ -149,14 +155,21 @@ impl Projection { /// /// _Godot equivalent: Projection.create_orthogonal()_ pub fn create_orthogonal( - left: f64, - right: f64, - bottom: f64, - top: f64, - near: f64, - far: f64, + left: f32, + right: f32, + bottom: f32, + top: f32, + near: f32, + far: f32, ) -> Self { - InnerProjection::create_orthogonal(left, right, bottom, top, near, far) + InnerProjection::create_orthogonal( + left as f64, + right as f64, + bottom as f64, + top as f64, + near as f64, + far as f64, + ) } /// Creates a new Projection that projects positions using an orthogonal @@ -167,13 +180,19 @@ impl Projection { /// /// _Godot equivalent: Projection.create_orthogonal_aspect()_ pub fn create_orthogonal_aspect( - size: f64, - aspect: f64, - near: f64, - far: f64, + size: f32, + aspect: f32, + near: f32, + far: f32, flip_fov: bool, ) -> Self { - InnerProjection::create_orthogonal_aspect(size, aspect, near, far, flip_fov) + InnerProjection::create_orthogonal_aspect( + size as f64, + aspect as f64, + near as f64, + far as f64, + flip_fov, + ) } /// Creates a new Projection that projects positions using a perspective @@ -185,13 +204,19 @@ impl Projection { /// /// _Godot equivalent: Projection.create_perspective()_ pub fn create_perspective( - fov_y: f64, - aspect: f64, - near: f64, - far: f64, + fov_y: f32, + aspect: f32, + near: f32, + far: f32, flip_fov: bool, ) -> Self { - InnerProjection::create_perspective(fov_y, aspect, near, far, flip_fov) + InnerProjection::create_perspective( + fov_y as f64, + aspect as f64, + near as f64, + far as f64, + flip_fov, + ) } /// Creates a new Projection that projects positions using a perspective @@ -206,24 +231,24 @@ impl Projection { /// _Godot equivalent: Projection.create_perspective_hmd()_ #[allow(clippy::too_many_arguments)] pub fn create_perspective_hmd( - fov_y: f64, - aspect: f64, - near: f64, - far: f64, + fov_y: f32, + aspect: f32, + near: f32, + far: f32, flip_fov: bool, - eye: Eye, - intraocular_dist: f64, - convergence_dist: f64, + eye: ProjectionEye, + intraocular_dist: f32, + convergence_dist: f32, ) -> Self { InnerProjection::create_perspective_hmd( - fov_y, - aspect, - near, - far, + fov_y as f64, + aspect as f64, + near as f64, + far as f64, flip_fov, eye as i64, - intraocular_dist, - convergence_dist, + intraocular_dist as f64, + convergence_dist as f64, ) } @@ -239,16 +264,15 @@ impl Projection { /// /// _Godot equivalent: Projection.flipped_y()_ pub fn flipped_y(self) -> Self { - Self { - cols: [self.cols[0], -self.cols[1], self.cols[2], self.cols[3]], - } + let [x, y, z, w] = self.cols; + Self::from_cols(x, -y, z, w) } /// Returns the X:Y aspect ratio of this Projection's viewport. /// /// _Godot equivalent: Projection.get_aspect()_ - pub fn aspect(&self) -> f64 { - self.as_inner().get_aspect() + pub fn aspect(&self) -> f32 { + self.as_inner().get_aspect() as f32 } /// Returns the dimensions of the far clipping plane of the projection, @@ -262,24 +286,24 @@ impl Projection { /// Returns the horizontal field of view of the projection (in degrees). /// /// _Godot equivalent: Projection.get_fov()_ - pub fn fov(&self) -> f64 { - self.as_inner().get_fov() + pub fn fov(&self) -> f32 { + self.as_inner().get_fov() as f32 } /// Returns the vertical field of view of a projection (in degrees) which /// has the given horizontal field of view (in degrees) and aspect ratio. /// /// _Godot equivalent: Projection.get_fovy()_ - pub fn fov_y_of(fov_x: f64, aspect: f64) -> f64 { - InnerProjection::get_fovy(fov_x, aspect) + pub fn fovy_of(fov_x: f32, aspect: f32) -> f32 { + InnerProjection::get_fovy(fov_x as f64, aspect as f64) as f32 } /// Returns the factor by which the visible level of detail is scaled by /// this Projection. /// /// _Godot equivalent: Projection.get_lod_multiplier()_ - pub fn lod_multiplier(&self) -> f64 { - self.as_inner().get_lod_multiplier() + pub fn lod_multiplier(&self) -> f32 { + self.as_inner().get_lod_multiplier() as f32 } /// Returns the number of pixels with the given pixel width displayed per @@ -310,16 +334,16 @@ impl Projection { /// clipped. /// /// _Godot equivalent: Projection.get_z_far()_ - pub fn z_far(&self) -> f64 { - self.as_inner().get_z_far() + pub fn z_far(&self) -> f32 { + self.as_inner().get_z_far() as f32 } /// Returns the distance for this Projection before which positions are /// clipped. /// /// _Godot equivalent: Projection.get_z_near()_ - pub fn z_near(&self) -> f64 { - self.as_inner().get_z_near() + pub fn z_near(&self) -> f32 { + self.as_inner().get_z_near() as f32 } /// Returns a Projection that performs the inverse of this Projection's @@ -341,7 +365,8 @@ impl Projection { /// added to the first and second values of the final column respectively. /// /// _Godot equivalent: Projection.jitter_offseted()_ - pub fn jitter_offseted(&self, offset: Vector2) -> Self { + #[must_use] + pub fn jitter_offset(&self, offset: Vector2) -> Self { self.as_inner().jitter_offseted(offset) } @@ -351,8 +376,8 @@ impl Projection { /// Note: The original Projection must be a perspective projection. /// /// _Godot equivalent: Projection.perspective_znear_adjusted()_ - pub fn perspective_znear_adjusted(&self, new_znear: f64) -> Self { - self.as_inner().perspective_znear_adjusted(new_znear) + pub fn perspective_znear_adjusted(&self, new_znear: f32) -> Self { + self.as_inner().perspective_znear_adjusted(new_znear as f64) } #[doc(hidden)] @@ -363,7 +388,7 @@ impl Projection { impl From for Projection { fn from(trans: Transform3D) -> Self { - trans.glam(Mat4::from) + trans.glam(glam::Mat4::from) } } @@ -389,32 +414,25 @@ impl Mul for Projection { } } -impl GlamType for Mat4 { +impl GlamType for glam::Mat4 { type Mapped = Projection; fn to_front(&self) -> Self::Mapped { - Projection { - cols: [ - self.col(0).to_front(), - self.col(1).to_front(), - self.col(2).to_front(), - self.col(3).to_front(), - ], - } + Projection::from_cols( + self.x_axis.to_front(), + self.y_axis.to_front(), + self.z_axis.to_front(), + self.w_axis.to_front(), + ) } fn from_front(mapped: &Self::Mapped) -> Self { - Self { - x_axis: mapped.cols[0].to_glam(), - y_axis: mapped.cols[1].to_glam(), - z_axis: mapped.cols[2].to_glam(), - w_axis: mapped.cols[3].to_glam(), - } + Self::from_cols_array_2d(&mapped.cols.map(|v| v.to_glam().to_array())) } } impl GlamConv for Projection { - type Glam = Mat4; + type Glam = glam::Mat4; } impl GodotFfi for Projection { @@ -437,7 +455,7 @@ pub enum ProjectionPlane { /// for head-mounted displays. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[repr(C)] -pub enum Eye { +pub enum ProjectionEye { Left = 1, Right = 2, } diff --git a/godot-core/src/builtin/quaternion.rs b/godot-core/src/builtin/quaternion.rs index 10d80ab86..598a875e2 100644 --- a/godot-core/src/builtin/quaternion.rs +++ b/godot-core/src/builtin/quaternion.rs @@ -13,7 +13,7 @@ use crate::builtin::{inner, math::*, vector3::*}; use super::{Basis, EulerOrder}; -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, PartialEq, Debug)] #[repr(C)] pub struct Quaternion { pub x: f32, @@ -98,7 +98,7 @@ impl Quaternion { } } - pub fn get_euler(self, order: EulerOrder) -> Vector3 { + pub fn to_euler(self, order: EulerOrder) -> Vector3 { Basis::from_quat(self).to_euler(order) } diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs index 37ae49fa2..3c6f6757e 100644 --- a/godot-core/src/builtin/transform2d.rs +++ b/godot-core/src/builtin/transform2d.rs @@ -8,14 +8,10 @@ use std::{f32::consts::PI, fmt::Display, ops::*}; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; -use super::{ - glam_helpers::{GlamConv, GlamType}, - math::*, - Vector2, -}; +use super::glam_helpers::{GlamConv, GlamType}; +use super::{math::*, Vector2}; -use glam::f32::Affine2; -use glam::f32::Mat2; +use glam; /// Affine 2D transform (2x3 matrix). /// @@ -29,7 +25,7 @@ use glam::f32::Mat2; /// ``` /// /// For methods that don't take translation into account, see [`Basis2D`]. -#[derive(Debug, Default, Clone, Copy, PartialEq)] +#[derive(Default, Copy, Clone, PartialEq, Debug)] #[repr(C)] pub struct Transform2D { /// The basis of the transform. @@ -67,6 +63,16 @@ impl Transform2D { Self { basis, origin } } + /// Create a new `Transform2D` with the given column vectors. + /// + /// _Godot equivalent: `Transform2D(Vector2 x_axis, Vector2 y_axis, Vector2 origin)_ + pub const fn from_cols(a: Vector2, b: Vector2, origin: Vector2) -> Self { + Self { + basis: Basis2D::from_cols(a, b), + origin, + } + } + /// Create a new `Transform2D` which will rotate by the given angle. pub fn from_angle(angle: f32) -> Self { Self::from_angle_origin(angle, Vector2::ZERO) @@ -94,12 +100,15 @@ impl Transform2D { origin: Vector2, ) -> Self { // Translated from Godot's implementation - let mut basis = Basis2D::IDENTITY; - basis.cols[0].x = angle.cos() * scale.x; - basis.cols[1].y = (angle + skew).cos() * scale.y; - basis.cols[1].x = -(angle + skew).sin() * scale.y; - basis.cols[0].y = angle.sin() * scale.x; - Self { basis, origin } + + Self { + basis: Basis2D::from_cols( + Vector2::new(angle.cos(), angle.sin()), + Vector2::new(-(angle + skew).sin(), (angle + skew).cos()), + ) + .scaled(scale), + origin, + } } /// Returns the inverse of the transform, under the assumption that the @@ -298,14 +307,11 @@ impl Mul for Transform2D { } } -impl GlamType for Affine2 { +impl GlamType for glam::Affine2 { type Mapped = Transform2D; fn to_front(&self) -> Self::Mapped { - Transform2D { - basis: self.matrix2.to_front(), - origin: self.translation.to_front(), - } + Transform2D::new(self.matrix2.to_front(), self.translation.to_front()) } fn from_front(mapped: &Self::Mapped) -> Self { @@ -317,7 +323,7 @@ impl GlamType for Affine2 { } impl GlamConv for Transform2D { - type Glam = Affine2; + type Glam = glam::Affine2; } impl GodotFfi for Transform2D { @@ -331,7 +337,7 @@ impl GodotFfi for Transform2D { /// /// This has no direct equivalent in Godot, but is the same as the `x` and `y` /// vectors from a `Transform2D`. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Copy, Clone, PartialEq, Debug)] #[repr(C)] pub struct Basis2D { /// The columns of the matrix. @@ -352,9 +358,7 @@ impl Basis2D { /// Create a diagonal matrix from the given values. pub const fn from_diagonal(x: f32, y: f32) -> Self { - Self { - cols: [Vector2::new(x, 0.0), Vector2::new(0.0, y)], - } + Self::from_cols(Vector2::new(x, 0.0), Vector2::new(0.0, y)) } /// Create a new basis from 2 basis vectors. @@ -369,7 +373,7 @@ impl Basis2D { /// Create a `Basis2D` from an angle. pub fn from_angle(angle: f32) -> Self { - Mat2::from_angle(angle).to_front() + glam::Mat2::from_angle(angle).to_front() } /// Returns the scale of the matrix. @@ -526,7 +530,7 @@ impl Mul for Basis2D { } } -impl GlamType for Mat2 { +impl GlamType for glam::Mat2 { type Mapped = Basis2D; fn to_front(&self) -> Self::Mapped { @@ -541,7 +545,7 @@ impl GlamType for Mat2 { } impl GlamConv for Basis2D { - type Glam = Mat2; + type Glam = glam::Mat2; } #[cfg(test)] diff --git a/godot-core/src/builtin/transform3d.rs b/godot-core/src/builtin/transform3d.rs index a6078f38d..660813e31 100644 --- a/godot-core/src/builtin/transform3d.rs +++ b/godot-core/src/builtin/transform3d.rs @@ -8,12 +8,10 @@ use std::{fmt::Display, ops::*}; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; -use super::{ - glam_helpers::{GlamConv, GlamType}, - Basis, Projection, Vector3, -}; +use super::glam_helpers::{GlamConv, GlamType}; +use super::{Basis, Projection, Vector3}; -use glam::f32::Affine3A; +use glam; /// Affine 3D transform (3x4 matrix). /// @@ -26,7 +24,7 @@ use glam::f32::Affine3A; /// [ a.y b.y c.y o.y ] /// [ a.z b.z c.z o.z ] /// ``` -#[derive(Debug, Default, Clone, Copy, PartialEq)] +#[derive(Default, Copy, Clone, PartialEq, Debug)] #[repr(C)] pub struct Transform3D { /// The basis is a matrix containing 3 vectors as its columns. They can be @@ -70,14 +68,14 @@ impl Transform3D { /// Create a new transform from 4 matrix-columns. /// /// _Godot equivalent: Transform3D(Vector3 x_axis, Vector3 y_axis, Vector3 z_axis, Vector3 origin)_ - pub const fn from_cols(x: Vector3, y: Vector3, z: Vector3, origin: Vector3) -> Self { + pub const fn from_cols(a: Vector3, b: Vector3, c: Vector3, origin: Vector3) -> Self { Self { - basis: Basis::from_cols(x, y, z), + basis: Basis::from_cols(a, b, c), origin, } } - /// Constructs a let from a Projection by trimming the last row of + /// Constructs a Transform3d from a Projection by trimming the last row of /// the projection matrix. /// /// _Godot equivalent: Transform3D(Projection from)_ @@ -298,14 +296,11 @@ impl Mul for Transform3D { } } -impl GlamType for Affine3A { +impl GlamType for glam::Affine3A { type Mapped = Transform3D; fn to_front(&self) -> Self::Mapped { - Transform3D { - basis: self.matrix3.to_front(), - origin: self.translation.to_front(), - } + Transform3D::new(self.matrix3.to_front(), self.translation.to_front()) } fn from_front(mapped: &Self::Mapped) -> Self { @@ -317,7 +312,7 @@ impl GlamType for Affine3A { } impl GlamConv for Transform3D { - type Glam = Affine3A; + type Glam = glam::Affine3A; } impl GodotFfi for Transform3D { diff --git a/itest/rust/src/basis_test.rs b/itest/rust/src/basis_test.rs index 41e86453c..7de930843 100644 --- a/itest/rust/src/basis_test.rs +++ b/itest/rust/src/basis_test.rs @@ -4,11 +4,9 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::{assert_inner_equivalent, itest}; -use godot::{ - prelude::{inner::InnerBasis, *}, - private::class_macros::assert_eq_approx, -}; +use crate::itest; +use godot::prelude::{inner::InnerBasis, *}; +use godot::private::class_macros::assert_eq_approx; const TEST_BASIS: Basis = Basis::from_rows( Vector3::new(0.942155, -0.270682, 0.197677), @@ -125,54 +123,57 @@ fn basis_euler_angles_same() { #[itest] fn basis_equiv() { - use crate::assert_inner_equivalent; - assert_inner_equivalent!(InnerBasis, TEST_BASIS.inverse(), |a, b| { - Basis::is_equal_approx(&a, &b) - }); - assert_inner_equivalent!(InnerBasis, TEST_BASIS.transposed(), |a, b| { - Basis::is_equal_approx(&a, &b) - }); - assert_inner_equivalent!(InnerBasis, TEST_BASIS.orthonormalized(), |a, b| { - Basis::is_equal_approx(&a, &b) - }); - assert_inner_equivalent!( - InnerBasis, - TEST_BASIS.determinant(), - |a, b| is_equal_approx(a, b as f32) - ); - assert_inner_equivalent!( - InnerBasis, - TEST_BASIS.rotated(Vector3::new(0.1, 0.2, 0.3).normalized(), 0.1), - |a, b| Basis::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerBasis, - TEST_BASIS.scaled(Vector3::new(1.0, 2.0, 3.0)), - |a, b| Basis::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!(InnerBasis, TEST_BASIS.(scale, get_scale)(), Vector3::is_equal_approx); - assert_inner_equivalent!(InnerBasis, TEST_BASIS.(to_euler, get_euler)(EulerOrder::XYZ), Vector3::is_equal_approx); - assert_inner_equivalent!( - InnerBasis, - TEST_BASIS.tdotx(Vector3::new(1.0, 2.0, 3.0)), - |a, b| is_equal_approx(a, b as f32) - ); - assert_inner_equivalent!( - InnerBasis, - TEST_BASIS.tdoty(Vector3::new(1.0, 2.0, 3.0)), - |a, b| is_equal_approx(a, b as f32) - ); - assert_inner_equivalent!( - InnerBasis, - TEST_BASIS.tdotz(Vector3::new(1.0, 2.0, 3.0)), - |a, b| is_equal_approx(a, b as f32) + let inner = InnerBasis::from_outer(&TEST_BASIS); + let outer = TEST_BASIS; + let vec = Vector3::new(1.0, 2.0, 3.0); + + #[rustfmt::skip] + let mappings_basis = [ + ("inverse", inner.inverse(), outer.inverse() ), + ("transposed", inner.transposed(), outer.transposed() ), + ("orthonormalized", inner.orthonormalized(), outer.orthonormalized() ), + ("rotated", inner.rotated(vec.normalized(), 0.1), outer.rotated(vec.normalized(), 0.1)), + ("scaled", inner.scaled(vec), outer.scaled(vec) ), + ("slerp", inner.slerp(Basis::IDENTITY, 0.5), outer.slerp(Basis::IDENTITY, 0.5) ), + ]; + for (name, inner, outer) in mappings_basis { + assert_eq_approx!(&inner, &outer, Basis::is_equal_approx, "function: {name}\n"); + } + + #[rustfmt::skip] + let mappings_float = [ + ("determinant", inner.determinant(), outer.determinant()), + ("tdotx", inner.tdotx(vec), outer.tdotx(vec) ), + ("tdoty", inner.tdoty(vec), outer.tdoty(vec) ), + ("tdotz", inner.tdotz(vec), outer.tdotz(vec) ), + ]; + for (name, inner, outer) in mappings_float { + assert_eq_approx!( + inner, + outer, + |a, b| is_equal_approx(a as f32, b), + "function: {name}\n" + ); + } + + assert_eq_approx!( + inner.get_scale(), + outer.scale(), + Vector3::is_equal_approx, + "function: get_scale\n" ); - assert_inner_equivalent!( - InnerBasis, - TEST_BASIS.slerp(Basis::IDENTITY, 0.5), - |a, b| Basis::is_equal_approx(&a, &b) + assert_eq_approx!( + inner.get_euler(EulerOrder::XYZ as i64), + outer.to_euler(EulerOrder::XYZ), + Vector3::is_equal_approx, + "function: get_euler\n" ); - assert_inner_equivalent!(InnerBasis, TEST_BASIS.(to_quat, get_rotation_quaternion)(), Quaternion::is_equal_approx); + assert_eq_approx!( + inner.get_rotation_quaternion(), + outer.to_quat(), + Quaternion::is_equal_approx, + "function: get_rotation_quaternion\n" + ) } fn deg_to_rad(rotation: Vector3) -> Vector3 { diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 7cb419f55..f0ed9282a 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -87,24 +87,3 @@ pub(crate) fn expect_panic(context: &str, code: impl FnOnce() + UnwindSafe) { "code should have panicked but did not: {context}", ); } - -#[macro_export] -macro_rules! assert_inner_equivalent { - ($inner_ty:ty, $self:ident.($func_outer:ident, $func_inner:ident)($($args:expr),*), $equal_fn:expr) => { - { - let outer = $self.$func_outer($($args.into()),*); - let inner = <$inner_ty>::from_outer(&$self).$func_inner($($args.into()),*); - godot::private::class_macros::assert_eq_approx!( - outer, - inner, - $equal_fn, - "using functions: outer = {}, inner = {}", - stringify!($func_outer), - stringify!($func_inner) - ); - } - }; - ($inner_ty:ty, $self:ident.$func:ident($($args:expr),*), $equal_fn:expr) => { - assert_inner_equivalent!($inner_ty, $self.($func, $func)($($args),*), $equal_fn); - }; -} diff --git a/itest/rust/src/transform2d_test.rs b/itest/rust/src/transform2d_test.rs index a132e6629..f9c783395 100644 --- a/itest/rust/src/transform2d_test.rs +++ b/itest/rust/src/transform2d_test.rs @@ -3,9 +3,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::{assert_inner_equivalent, itest}; +use crate::itest; use godot::prelude::{inner::InnerTransform2D, *}; +use godot::private::class_macros::assert_eq_approx; const TEST_TRANSFORM: Transform2D = Transform2D::new( Basis2D::from_cols(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0)), @@ -20,54 +21,47 @@ pub(crate) fn run() -> bool { #[itest] fn transform2d_equiv() { - assert_inner_equivalent!(InnerTransform2D, TEST_TRANSFORM.affine_inverse(), |a, b| { - Transform2D::is_equal_approx(&a, &b) - }); - assert_inner_equivalent!(InnerTransform2D, TEST_TRANSFORM.(rotation,get_rotation)(), |a, b| { - is_equal_approx(a, b as f32) - }); - assert_inner_equivalent!(InnerTransform2D, TEST_TRANSFORM.(scale,get_scale)(), |a, b| { - Vector2::is_equal_approx(a, b) - }); - assert_inner_equivalent!(InnerTransform2D, TEST_TRANSFORM.(skew,get_skew)(), |a, b| { - is_equal_approx(a, b as f32) - }); - assert_inner_equivalent!( - InnerTransform2D, - TEST_TRANSFORM.orthonormalized(), - |a, b| Transform2D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!(InnerTransform2D, TEST_TRANSFORM.rotated(1.0), |a, b| { - Transform2D::is_equal_approx(&a, &b) - }); - assert_inner_equivalent!( - InnerTransform2D, - TEST_TRANSFORM.rotated_local(1.0), - |a, b| Transform2D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerTransform2D, - TEST_TRANSFORM.scaled(Vector2::new(1.0, 2.0)), - |a, b| Transform2D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerTransform2D, - TEST_TRANSFORM.scaled_local(Vector2::new(1.0, 2.0)), - |a, b| Transform2D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerTransform2D, - TEST_TRANSFORM.translated(Vector2::new(1.0, 2.0)), - |a, b| Transform2D::is_equal_approx(&a, &b) + let inner = InnerTransform2D::from_outer(&TEST_TRANSFORM); + let outer = TEST_TRANSFORM; + let vec = Vector2::new(1.0, 2.0); + + #[rustfmt::skip] + let mappings_transform = [ + ("affine_inverse", inner.affine_inverse(), outer.affine_inverse() ), + ("orthonormalized", inner.orthonormalized(), outer.orthonormalized() ), + ("rotated", inner.rotated(1.0), outer.rotated(1.0) ), + ("rotated_local", inner.rotated_local(1.0), outer.rotated_local(1.0) ), + ("scaled", inner.scaled(vec), outer.scaled(vec) ), + ("scaled_local", inner.scaled_local(vec), outer.scaled_local(vec) ), + ("translated", inner.translated(vec), outer.translated(vec) ), + ("translated_local", inner.translated_local(vec), outer.translated_local(vec) ), + ("interpolate_with", inner.interpolate_with(Transform2D::IDENTITY, 0.5), outer.interpolate_with(Transform2D::IDENTITY, 0.5)) + ]; + for (name, inner, outer) in mappings_transform { + assert_eq_approx!( + &inner, + &outer, + Transform2D::is_equal_approx, + "function: {name}\n" + ); + } + + assert_eq_approx!( + inner.get_rotation(), + outer.rotation(), + |a, b| is_equal_approx(a as f32, b), + "function = get_rotation\n" ); - assert_inner_equivalent!( - InnerTransform2D, - TEST_TRANSFORM.translated_local(Vector2::new(1.0, 2.0)), - |a, b| Transform2D::is_equal_approx(&a, &b) + assert_eq_approx!( + inner.get_rotation(), + outer.rotation(), + |a, b| is_equal_approx(a as f32, b), + "function = get_rotation\n" ); - assert_inner_equivalent!( - InnerTransform2D, - TEST_TRANSFORM.interpolate_with(Transform2D::IDENTITY, 0.5), - |a, b| Transform2D::is_equal_approx(&a, &b) + assert_eq_approx!( + inner.get_skew(), + outer.skew(), + |a, b| is_equal_approx(a as f32, b), + "function = get_scale\n" ); } diff --git a/itest/rust/src/transform3d_test.rs b/itest/rust/src/transform3d_test.rs index 25c31c445..b7a54b0b4 100644 --- a/itest/rust/src/transform3d_test.rs +++ b/itest/rust/src/transform3d_test.rs @@ -3,9 +3,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::{assert_inner_equivalent, itest}; +use crate::itest; use godot::prelude::{inner::InnerTransform3D, *}; +use godot::private::class_macros::assert_eq_approx; const TEST_TRANSFORM: Transform3D = Transform3D::new( Basis::from_cols( @@ -24,52 +25,28 @@ pub(crate) fn run() -> bool { #[itest] fn transform3d_equiv() { - assert_inner_equivalent!(InnerTransform3D, TEST_TRANSFORM.affine_inverse(), |a, b| { - Transform3D::is_equal_approx(&a, &b) - }); - assert_inner_equivalent!( - InnerTransform3D, - TEST_TRANSFORM.orthonormalized(), - |a, b| Transform3D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerTransform3D, - TEST_TRANSFORM.rotated(Vector3::new(1.0, 2.0, 3.0).normalized(), 1.0), - |a, b| Transform3D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerTransform3D, - TEST_TRANSFORM.rotated_local(Vector3::new(1.0, 2.0, 3.0).normalized(), 1.0), - |a, b| Transform3D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerTransform3D, - TEST_TRANSFORM.scaled(Vector3::new(1.0, 2.0, 3.0)), - |a, b| Transform3D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerTransform3D, - TEST_TRANSFORM.scaled_local(Vector3::new(1.0, 2.0, 3.0)), - |a, b| Transform3D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerTransform3D, - TEST_TRANSFORM.translated(Vector3::new(1.0, 2.0, 3.0)), - |a, b| Transform3D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerTransform3D, - TEST_TRANSFORM.translated_local(Vector3::new(1.0, 2.0, 3.0)), - |a, b| Transform3D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerTransform3D, - TEST_TRANSFORM.looking_at(Vector3::new(1.0, 2.0, 3.0), Vector3::UP), - |a, b| Transform3D::is_equal_approx(&a, &b) - ); - assert_inner_equivalent!( - InnerTransform3D, - TEST_TRANSFORM.interpolate_with(Transform3D::IDENTITY, 0.5), - |a, b| Transform3D::is_equal_approx(&a, &b) - ); + let inner = InnerTransform3D::from_outer(&TEST_TRANSFORM); + let outer = TEST_TRANSFORM; + let vec = Vector3::new(1.0, 2.0, 3.0); + + #[rustfmt::skip] + let mappings_transform = [ + ("affine_inverse", inner.affine_inverse(), outer.affine_inverse() ), + ("orthonormalized", inner.orthonormalized(), outer.orthonormalized() ), + ("rotated", inner.rotated(vec.normalized(), 1.0), outer.rotated(vec.normalized(), 1.0) ), + ("rotated_local", inner.rotated_local(vec.normalized(), 1.0), outer.rotated_local(vec.normalized(), 1.0) ), + ("scaled", inner.scaled(vec), outer.scaled(vec) ), + ("scaled_local", inner.scaled_local(vec), outer.scaled_local(vec) ), + ("translated", inner.translated(vec), outer.translated(vec) ), + ("translated_local", inner.translated_local(vec), outer.translated_local(vec) ), + ("interpolate_with", inner.interpolate_with(Transform3D::IDENTITY, 0.5), outer.interpolate_with(Transform3D::IDENTITY, 0.5)) + ]; + for (name, inner, outer) in mappings_transform { + assert_eq_approx!( + &inner, + &outer, + Transform3D::is_equal_approx, + "function: {name}\n" + ); + } } From 1114ae8bb5026a2ffb757bc3d4a997e5b19030f9 Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Thu, 2 Mar 2023 13:25:35 +0100 Subject: [PATCH 14/15] Add `is_angle_equal_approx` --- godot-core/src/builtin/math.rs | 36 ++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/godot-core/src/builtin/math.rs b/godot-core/src/builtin/math.rs index c5efc9059..8e52185ab 100644 --- a/godot-core/src/builtin/math.rs +++ b/godot-core/src/builtin/math.rs @@ -6,6 +6,8 @@ use std::f32::consts::TAU; +use super::Vector2; + pub const CMP_EPSILON: f32 = 0.00001; pub fn lerp(a: f32, b: f32, t: f32) -> f32 { @@ -23,6 +25,18 @@ pub fn is_equal_approx(a: f32, b: f32) -> bool { (a - b).abs() < tolerance } +/// Check if two angles are approximately equal, by comparing the distance +/// between the points on the unit circle with 0 using [`is_equal_approx`]. +pub fn is_angle_equal_approx(a: f32, b: f32) -> bool { + let (x1, y1) = a.sin_cos(); + let (x2, y2) = b.sin_cos(); + + is_equal_approx( + Vector2::distance_to(Vector2::new(x1, y1), Vector2::new(x2, y2)), + 0.0, + ) +} + pub fn is_zero_approx(s: f32) -> bool { s.abs() < CMP_EPSILON } @@ -190,6 +204,15 @@ mod test { assert_ne_approx!(1.0, 2.0, is_equal_approx, "Message {}", "formatted"); } + #[test] + fn angle_equal_approx() { + assert_eq_approx!(1.0, 1.000001, is_angle_equal_approx); + assert_eq_approx!(0.0, TAU, is_angle_equal_approx); + assert_eq_approx!(PI, -PI, is_angle_equal_approx); + assert_eq_approx!(4.45783, -(TAU - 4.45783), is_angle_equal_approx); + assert_eq_approx!(31.0 * PI, -13.0 * PI, is_angle_equal_approx); + } + #[test] #[should_panic(expected = "I am inside format")] fn eq_approx_fail_with_message() { @@ -198,16 +221,17 @@ mod test { #[test] fn lerp_angle_test() { - assert_eq_approx!(lerp_angle(0.0, PI, 0.5), -FRAC_PI_2, is_equal_approx); + assert_eq_approx!(lerp_angle(0.0, PI, 0.5), -FRAC_PI_2, is_angle_equal_approx); assert_eq_approx!( lerp_angle(0.0, PI + 3.0 * TAU, 0.5), FRAC_PI_2, - is_equal_approx + is_angle_equal_approx ); let angle = PI * 2.0 / 3.0; - let (s1, c1) = lerp_angle(-5.0 * TAU, angle + 3.0 * TAU, 0.5).sin_cos(); - let (s2, c2) = (angle / 2.0).sin_cos(); - assert_eq_approx!(s1, s2, is_equal_approx); - assert_eq_approx!(c1, c2, is_equal_approx); + assert_eq_approx!( + lerp_angle(-5.0 * TAU, angle + 3.0 * TAU, 0.5), + (angle / 2.0), + is_angle_equal_approx + ); } } From e163f0fd43d39ad42a600d5bfba549aa84b92414 Mon Sep 17 00:00:00 2001 From: Lili Zoey Date: Fri, 3 Mar 2023 16:09:14 +0100 Subject: [PATCH 15/15] Fix naming in `Basis::to_euler_inner` Make `Projection` use `f64`s Make `Basis2D` crate-public instead of public --- godot-core/src/builtin/basis.rs | 16 +- godot-core/src/builtin/projection.rs | 175 +++++++++----------- godot-core/src/builtin/transform2d.rs | 219 ++++++++++++-------------- itest/rust/src/transform2d_test.rs | 11 +- 4 files changed, 185 insertions(+), 236 deletions(-) diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index cd65357bb..66cb3e396 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -217,17 +217,17 @@ impl Basis { // return the simplest form (human friendlier in editor and scripts) Vec3::new(0.0, f32::atan2(self.rows[0].z, self.rows[0].x), 0.0) } - _ => -Self::to_euler_inner(self.rows[0].z, row_a.yx(), col_c.yz(), col_b.zy()) + _ => -Self::to_euler_inner(self.rows[0].z, col_c.yz(), row_a.yx(), col_b.zy()) .yxz(), } } EulerOrder::XZY => { - Self::to_euler_inner(self.rows[0].y, row_a.zx(), col_b.zy(), col_c.yz()).yzx() + Self::to_euler_inner(self.rows[0].y, col_b.zy(), row_a.zx(), col_c.yz()).yzx() } EulerOrder::YXZ => { let major = self.rows[1].z; - let fst = row_b.xy(); - let snd = col_c.xz(); + let fst = col_c.xz(); + let snd = row_b.xy(); let rest = row_a.yx(); match Self::is_between_neg1_1(major) { // Is it a pure X rotation? @@ -242,13 +242,13 @@ impl Basis { } } EulerOrder::YZX => { - -Self::to_euler_inner(self.rows[1].x, col_a.zx(), row_b.zy(), row_c.yz()).yzx() + -Self::to_euler_inner(self.rows[1].x, row_b.zy(), col_a.zx(), row_c.yz()).yzx() } EulerOrder::ZXY => { - -Self::to_euler_inner(self.rows[2].y, col_b.xy(), row_c.xz(), row_a.zx()) + -Self::to_euler_inner(self.rows[2].y, row_c.xz(), col_b.xy(), row_a.zx()) } EulerOrder::ZYX => { - Self::to_euler_inner(self.rows[2].x, row_c.yz(), col_a.yx(), col_b.xy()).zxy() + Self::to_euler_inner(self.rows[2].x, col_a.yx(), row_c.yz(), col_b.xy()).zxy() } } .to_front() @@ -275,8 +275,8 @@ impl Basis { Ordering::Less => glam::Vec3::new(FRAC_PI_2, -f32::atan2(rest.x, rest.y), 0.0), Ordering::Equal => glam::Vec3::new( f32::asin(-major), - f32::atan2(snd.x, snd.y), f32::atan2(fst.x, fst.y), + f32::atan2(snd.x, snd.y), ), // It's 1 Ordering::Greater => glam::Vec3::new(-FRAC_PI_2, -f32::atan2(rest.x, rest.y), 0.0), diff --git a/godot-core/src/builtin/projection.rs b/godot-core/src/builtin/projection.rs index 473d0f95d..92ee2d5f4 100644 --- a/godot-core/src/builtin/projection.rs +++ b/godot-core/src/builtin/projection.rs @@ -83,23 +83,23 @@ impl Projection { #[allow(clippy::too_many_arguments)] pub fn create_for_hmd( eye: ProjectionEye, - aspect: f32, - intraocular_dist: f32, - display_width: f32, - display_to_lens: f32, - oversample: f32, - near: f32, - far: f32, + aspect: f64, + intraocular_dist: f64, + display_width: f64, + display_to_lens: f64, + oversample: f64, + near: f64, + far: f64, ) -> Self { InnerProjection::create_for_hmd( eye as i64, - aspect as f64, - intraocular_dist as f64, - display_width as f64, - display_to_lens as f64, - oversample as f64, - near as f64, - far as f64, + aspect, + intraocular_dist, + display_width, + display_to_lens, + oversample, + near, + far, ) } @@ -108,21 +108,14 @@ impl Projection { /// /// _Godot equivalent: Projection.create_frustum()_ pub fn create_frustum( - left: f32, - right: f32, - bottom: f32, - top: f32, - near: f32, - far: f32, + left: f64, + right: f64, + bottom: f64, + top: f64, + near: f64, + far: f64, ) -> Self { - InnerProjection::create_frustum( - left as f64, - right as f64, - bottom as f64, - top as f64, - near as f64, - far as f64, - ) + InnerProjection::create_frustum(left, right, bottom, top, near, far) } /// Creates a new Projection that projects positions in a frustum with the @@ -133,21 +126,14 @@ impl Projection { /// /// _Godot equivalent: Projection.create_frustum_aspect()_ pub fn create_frustum_aspect( - size: f32, - aspect: f32, + size: f64, + aspect: f64, offset: Vector2, - near: f32, - far: f32, + near: f64, + far: f64, flip_fov: bool, ) -> Self { - InnerProjection::create_frustum_aspect( - size as f64, - aspect as f64, - offset, - near as f64, - far as f64, - flip_fov, - ) + InnerProjection::create_frustum_aspect(size, aspect, offset, near, far, flip_fov) } /// Creates a new Projection that projects positions using an orthogonal @@ -155,21 +141,14 @@ impl Projection { /// /// _Godot equivalent: Projection.create_orthogonal()_ pub fn create_orthogonal( - left: f32, - right: f32, - bottom: f32, - top: f32, - near: f32, - far: f32, + left: f64, + right: f64, + bottom: f64, + top: f64, + near: f64, + far: f64, ) -> Self { - InnerProjection::create_orthogonal( - left as f64, - right as f64, - bottom as f64, - top as f64, - near as f64, - far as f64, - ) + InnerProjection::create_orthogonal(left, right, bottom, top, near, far) } /// Creates a new Projection that projects positions using an orthogonal @@ -180,19 +159,13 @@ impl Projection { /// /// _Godot equivalent: Projection.create_orthogonal_aspect()_ pub fn create_orthogonal_aspect( - size: f32, - aspect: f32, - near: f32, - far: f32, + size: f64, + aspect: f64, + near: f64, + far: f64, flip_fov: bool, ) -> Self { - InnerProjection::create_orthogonal_aspect( - size as f64, - aspect as f64, - near as f64, - far as f64, - flip_fov, - ) + InnerProjection::create_orthogonal_aspect(size, aspect, near, far, flip_fov) } /// Creates a new Projection that projects positions using a perspective @@ -204,19 +177,13 @@ impl Projection { /// /// _Godot equivalent: Projection.create_perspective()_ pub fn create_perspective( - fov_y: f32, - aspect: f32, - near: f32, - far: f32, + fov_y: f64, + aspect: f64, + near: f64, + far: f64, flip_fov: bool, ) -> Self { - InnerProjection::create_perspective( - fov_y as f64, - aspect as f64, - near as f64, - far as f64, - flip_fov, - ) + InnerProjection::create_perspective(fov_y, aspect, near, far, flip_fov) } /// Creates a new Projection that projects positions using a perspective @@ -231,32 +198,32 @@ impl Projection { /// _Godot equivalent: Projection.create_perspective_hmd()_ #[allow(clippy::too_many_arguments)] pub fn create_perspective_hmd( - fov_y: f32, - aspect: f32, - near: f32, - far: f32, + fov_y: f64, + aspect: f64, + near: f64, + far: f64, flip_fov: bool, eye: ProjectionEye, - intraocular_dist: f32, - convergence_dist: f32, + intraocular_dist: f64, + convergence_dist: f64, ) -> Self { InnerProjection::create_perspective_hmd( - fov_y as f64, - aspect as f64, - near as f64, - far as f64, + fov_y, + aspect, + near, + far, flip_fov, eye as i64, - intraocular_dist as f64, - convergence_dist as f64, + intraocular_dist, + convergence_dist, ) } /// Return the determinant of the matrix. /// /// _Godot equivalent: Projection.determinant()_ - pub fn determinant(&self) -> f32 { - self.glam(|mat| mat.determinant()) + pub fn determinant(&self) -> f64 { + self.glam(|mat| mat.determinant()) as f64 } /// Returns a copy of this Projection with the signs of the values of the Y @@ -271,8 +238,8 @@ impl Projection { /// Returns the X:Y aspect ratio of this Projection's viewport. /// /// _Godot equivalent: Projection.get_aspect()_ - pub fn aspect(&self) -> f32 { - self.as_inner().get_aspect() as f32 + pub fn aspect(&self) -> f64 { + self.as_inner().get_aspect() } /// Returns the dimensions of the far clipping plane of the projection, @@ -286,24 +253,24 @@ impl Projection { /// Returns the horizontal field of view of the projection (in degrees). /// /// _Godot equivalent: Projection.get_fov()_ - pub fn fov(&self) -> f32 { - self.as_inner().get_fov() as f32 + pub fn fov(&self) -> f64 { + self.as_inner().get_fov() } /// Returns the vertical field of view of a projection (in degrees) which /// has the given horizontal field of view (in degrees) and aspect ratio. /// /// _Godot equivalent: Projection.get_fovy()_ - pub fn fovy_of(fov_x: f32, aspect: f32) -> f32 { - InnerProjection::get_fovy(fov_x as f64, aspect as f64) as f32 + pub fn fovy_of(fov_x: f64, aspect: f64) -> f64 { + InnerProjection::get_fovy(fov_x, aspect) } /// Returns the factor by which the visible level of detail is scaled by /// this Projection. /// /// _Godot equivalent: Projection.get_lod_multiplier()_ - pub fn lod_multiplier(&self) -> f32 { - self.as_inner().get_lod_multiplier() as f32 + pub fn lod_multiplier(&self) -> f64 { + self.as_inner().get_lod_multiplier() } /// Returns the number of pixels with the given pixel width displayed per @@ -334,16 +301,16 @@ impl Projection { /// clipped. /// /// _Godot equivalent: Projection.get_z_far()_ - pub fn z_far(&self) -> f32 { - self.as_inner().get_z_far() as f32 + pub fn z_far(&self) -> f64 { + self.as_inner().get_z_far() } /// Returns the distance for this Projection before which positions are /// clipped. /// /// _Godot equivalent: Projection.get_z_near()_ - pub fn z_near(&self) -> f32 { - self.as_inner().get_z_near() as f32 + pub fn z_near(&self) -> f64 { + self.as_inner().get_z_near() } /// Returns a Projection that performs the inverse of this Projection's @@ -376,8 +343,8 @@ impl Projection { /// Note: The original Projection must be a perspective projection. /// /// _Godot equivalent: Projection.perspective_znear_adjusted()_ - pub fn perspective_znear_adjusted(&self, new_znear: f32) -> Self { - self.as_inner().perspective_znear_adjusted(new_znear as f64) + pub fn perspective_znear_adjusted(&self, new_znear: f64) -> Self { + self.as_inner().perspective_znear_adjusted(new_znear) } #[doc(hidden)] diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs index 3c6f6757e..9161ffea4 100644 --- a/godot-core/src/builtin/transform2d.rs +++ b/godot-core/src/builtin/transform2d.rs @@ -17,21 +17,26 @@ use glam; /// /// Represents transformations such as translation, rotation, or scaling. /// -/// Expressed as a 2x3 matrix, this transform consists of a 2d rotation matrix -/// `basis` containing two (column) vectors, as well as an origin `origin`: +/// Expressed as a 2x3 matrix, this transform consists of a two column vectors +/// `a` and `b` representing the basis of the transform, as well as the origin: /// ```text -/// [ basis[0].x basis[1].x origin.x ] -/// [ basis[0].y basis[1].y origin.y ] +/// [ a.x b.x origin.x ] +/// [ a.y b.y origin.y ] /// ``` /// /// For methods that don't take translation into account, see [`Basis2D`]. #[derive(Default, Copy, Clone, PartialEq, Debug)] #[repr(C)] pub struct Transform2D { - /// The basis of the transform. + /// The first basis vector. /// - /// This is equivalent to the `x` and `y` fields from godot. - pub basis: Basis2D, + /// This is equivalent to the `x` field from godot. + pub a: Vector2, + + /// The second basis vector. + /// + /// This is equivalent to the `y` field from godot. + pub b: Vector2, /// The origin of the transform. The coordinate space defined by this transform /// starts at this point. @@ -46,31 +51,28 @@ impl Transform2D { /// transformation. /// /// _Godot equivalent: `Transform2D.IDENTITY`_ - pub const IDENTITY: Self = Self::new(Basis2D::IDENTITY, Vector2::ZERO); + pub const IDENTITY: Self = Self::from_basis_origin(Basis2D::IDENTITY, Vector2::ZERO); /// The `Transform2D` that will flip something along its x axis. /// /// _Godot equivalent: `Transform2D.FLIP_X`_ - pub const FLIP_X: Self = Self::new(Basis2D::FLIP_X, Vector2::ZERO); + pub const FLIP_X: Self = Self::from_basis_origin(Basis2D::FLIP_X, Vector2::ZERO); /// The `Transform2D` that will flip something along its y axis. /// /// _Godot equivalent: `Transform2D.FLIP_Y`_ - pub const FLIP_Y: Self = Self::new(Basis2D::FLIP_Y, Vector2::ZERO); + pub const FLIP_Y: Self = Self::from_basis_origin(Basis2D::FLIP_Y, Vector2::ZERO); - /// Create a new `Transform2D` from the given basis and origin. - pub const fn new(basis: Basis2D, origin: Vector2) -> Self { - Self { basis, origin } + const fn from_basis_origin(basis: Basis2D, origin: Vector2) -> Self { + let [a, b] = basis.cols; + Self { a, b, origin } } /// Create a new `Transform2D` with the given column vectors. /// /// _Godot equivalent: `Transform2D(Vector2 x_axis, Vector2 y_axis, Vector2 origin)_ pub const fn from_cols(a: Vector2, b: Vector2, origin: Vector2) -> Self { - Self { - basis: Basis2D::from_cols(a, b), - origin, - } + Self { a, b, origin } } /// Create a new `Transform2D` which will rotate by the given angle. @@ -83,10 +85,7 @@ impl Transform2D { /// /// _Godot equivalent: `Transform2D(float rotation, Vector2 position)`_ pub fn from_angle_origin(angle: f32, origin: Vector2) -> Self { - Self { - basis: Basis2D::from_angle(angle), - origin, - } + Self::from_basis_origin(Basis2D::from_angle(angle), origin) } /// Create a new `Transform2D` which will rotate by `angle`, scale by @@ -101,14 +100,28 @@ impl Transform2D { ) -> Self { // Translated from Godot's implementation - Self { - basis: Basis2D::from_cols( + Self::from_basis_origin( + Basis2D::from_cols( Vector2::new(angle.cos(), angle.sin()), Vector2::new(-(angle + skew).sin(), (angle + skew).cos()), ) .scaled(scale), origin, - } + ) + } + + /// Create a reference to the first two columns of the transform + /// interpreted as a [`Basis2D`]. + fn basis<'a>(&'a self) -> &'a Basis2D { + // SAFETY: Both `Basis2D` and `Transform2D` are `repr(C)`, and the + // layout of `Basis2D` is a prefix of `Transform2D` + + unsafe { std::mem::transmute::<&'a Transform2D, &'a Basis2D>(self) } + } + + /// Create a [`Basis2D`] from the first two columns of the transform. + fn to_basis(self) -> Basis2D { + Basis2D::from_cols(self.a, self.b) } /// Returns the inverse of the transform, under the assumption that the @@ -124,7 +137,7 @@ impl Transform2D { /// /// _Godot equivalent: `Transform2D.get_rotation()`_ pub fn rotation(&self) -> f32 { - self.basis.rotation() + self.basis().rotation() } /// Returns the transform's scale. @@ -132,7 +145,7 @@ impl Transform2D { /// _Godot equivalent: `Transform2D.get_scale()`_ #[must_use] pub fn scale(&self) -> Vector2 { - self.basis.scale() + self.basis().scale() } /// Returns the transform's skew (in radians). @@ -140,7 +153,7 @@ impl Transform2D { /// _Godot equivalent: `Transform2D.get_skew()`_ #[must_use] pub fn skew(&self) -> f32 { - self.basis.skew() + self.basis().skew() } /// Returns a transform interpolated between this transform and another by @@ -162,15 +175,17 @@ impl Transform2D { /// /// _Godot equivalent: `Transform2D.is_equal_approx()`_ pub fn is_equal_approx(&self, other: &Self) -> bool { - self.basis.is_equal_approx(&other.basis) && self.origin.is_equal_approx(other.origin) + self.a.is_equal_approx(other.a) + && self.b.is_equal_approx(other.b) + && self.origin.is_equal_approx(other.origin) } /// Returns `true` if this transform is finite, by calling - /// [`Basis2D::is_finite()`] and [`Vector2::is_finite()`]. + /// [`Vector2::is_finite()`] on each component. /// /// _Godot equivalent: `Transform2D.is_finite()`_ pub fn is_finite(&self) -> bool { - self.basis.is_finite() && self.origin.is_finite() + self.a.is_finite() && self.b.is_finite() && self.origin.is_finite() } /// Returns the transform with the basis orthogonal (90 degrees), and @@ -179,10 +194,7 @@ impl Transform2D { /// _Godot equivalent: `Transform2D.orthonormalized()`_ #[must_use] pub fn orthonormalized(self) -> Self { - Self { - basis: self.basis.orthonormalized(), - origin: self.origin, - } + Self::from_basis_origin(self.basis().orthonormalized(), self.origin) } /// Returns a copy of the transform rotated by the given `angle` (in radians). @@ -215,13 +227,10 @@ impl Transform2D { /// _Godot equivalent: `Transform2D.scaled()`_ #[must_use] pub fn scaled(self, scale: Vector2) -> Self { - let mut basis = self.basis; + let mut basis = self.to_basis(); basis.set_row_a(basis.row_a() * scale.x); basis.set_row_b(basis.row_b() * scale.y); - Self { - basis, - origin: self.origin * scale, - } + Self::from_basis_origin(basis, self.origin * scale) } /// Returns a copy of the transform scaled by the given scale factor. @@ -232,10 +241,7 @@ impl Transform2D { /// _Godot equivalent: `Transform2D.scaled_local()`_ #[must_use] pub fn scaled_local(self, scale: Vector2) -> Self { - Self { - basis: self.basis.scaled(scale), - origin: self.origin, - } + Self::from_basis_origin(self.basis().scaled(scale), self.origin) } /// Returns a copy of the transform translated by the given offset. @@ -246,10 +252,7 @@ impl Transform2D { /// _Godot equivalent: `Transform2D.translated()`_ #[must_use] pub fn translated(self, offset: Vector2) -> Self { - Self { - basis: self.basis, - origin: self.origin + offset, - } + Self::from_cols(self.a, self.b, self.origin + offset) } /// Returns a copy of the transform translated by the given offset. @@ -260,10 +263,23 @@ impl Transform2D { /// _Godot equivalent: `Transform2D.translated()`_ #[must_use] pub fn translated_local(self, offset: Vector2) -> Self { - Self { - basis: self.basis, - origin: self.origin + (self.basis * offset), - } + Self::from_cols(self.a, self.b, self.origin + (self.to_basis() * offset)) + } + + /// Returns a vector transformed (multiplied) by the basis matrix. + /// This method does not account for translation (the origin vector). + /// + /// _Godot equivalent: `Transform2D.basis_xform()`_ + pub fn basis_xform(&self, v: Vector2) -> Vector2 { + self.to_basis() * v + } + + /// Returns a vector transformed (multiplied) by the inverse basis matrix. + /// This method does not account for translation (the origin vector). + /// + /// _Godot equivalent: `Transform2D.basis_xform_inv()`_ + pub fn basis_xform_inv(&self, v: Vector2) -> Vector2 { + self.basis().inverse() * v } } @@ -273,10 +289,9 @@ impl Display for Transform2D { // [X: (1, 2), Y: (3, 4), O: (5, 6)] // Where X,Y,O are the columns - let [a, b] = self.basis.cols; - let o = self.origin; + let Transform2D { a, b, origin } = self; - write!(f, "[a: {a}, b: {b}, o: {o}]") + write!(f, "[a: {a}, b: {b}, o: {origin}]") } } @@ -300,10 +315,7 @@ impl Mul for Transform2D { type Output = Self; fn mul(self, rhs: f32) -> Self::Output { - Self { - basis: self.basis * rhs, - origin: self.origin * rhs, - } + Self::from_cols(self.a * rhs, self.b * rhs, self.origin * rhs) } } @@ -311,12 +323,12 @@ impl GlamType for glam::Affine2 { type Mapped = Transform2D; fn to_front(&self) -> Self::Mapped { - Transform2D::new(self.matrix2.to_front(), self.translation.to_front()) + Transform2D::from_basis_origin(self.matrix2.to_front(), self.translation.to_front()) } fn from_front(mapped: &Self::Mapped) -> Self { Self { - matrix2: mapped.basis.to_glam(), + matrix2: mapped.basis().to_glam(), translation: mapped.origin.to_glam(), } } @@ -339,60 +351,55 @@ impl GodotFfi for Transform2D { /// vectors from a `Transform2D`. #[derive(Copy, Clone, PartialEq, Debug)] #[repr(C)] -pub struct Basis2D { +pub(crate) struct Basis2D { /// The columns of the matrix. - pub cols: [Vector2; 2], + cols: [Vector2; 2], } impl Basis2D { /// The identity basis, with no rotation or scaling applied. - pub const IDENTITY: Self = Self::from_diagonal(1.0, 1.0); + pub(crate) const IDENTITY: Self = Self::from_diagonal(1.0, 1.0); /// The basis that will flip something along the X axis when used in a /// transformation. - pub const FLIP_X: Self = Self::from_diagonal(-1.0, 1.0); + pub(crate) const FLIP_X: Self = Self::from_diagonal(-1.0, 1.0); /// The basis that will flip something along the X axis when used in a /// transformation. - pub const FLIP_Y: Self = Self::from_diagonal(1.0, -1.0); + pub(crate) const FLIP_Y: Self = Self::from_diagonal(1.0, -1.0); /// Create a diagonal matrix from the given values. - pub const fn from_diagonal(x: f32, y: f32) -> Self { + pub(crate) const fn from_diagonal(x: f32, y: f32) -> Self { Self::from_cols(Vector2::new(x, 0.0), Vector2::new(0.0, y)) } /// Create a new basis from 2 basis vectors. - pub const fn from_cols(x: Vector2, y: Vector2) -> Self { + pub(crate) const fn from_cols(x: Vector2, y: Vector2) -> Self { Self { cols: [x, y] } } - /// Create a new basis from 2 row vectors. These are *not* basis vectors. - pub const fn from_rows(x: Vector2, y: Vector2) -> Self { - Self::from_cols(x, y).transposed() - } - /// Create a `Basis2D` from an angle. - pub fn from_angle(angle: f32) -> Self { + pub(crate) fn from_angle(angle: f32) -> Self { glam::Mat2::from_angle(angle).to_front() } /// Returns the scale of the matrix. #[must_use] - pub fn scale(&self) -> Vector2 { + pub(crate) fn scale(&self) -> Vector2 { let det_sign = self.determinant().signum(); Vector2::new(self.cols[0].length(), det_sign * self.cols[1].length()) } /// Introduces an additional scaling. #[must_use] - pub fn scaled(self, scale: Vector2) -> Self { + pub(crate) fn scaled(self, scale: Vector2) -> Self { Self { cols: [self.cols[0] * scale.x, self.cols[1] * scale.y], } } /// Returns the determinant of the matrix. - pub fn determinant(&self) -> f32 { + pub(crate) fn determinant(&self) -> f32 { self.glam(|mat| mat.determinant()) } @@ -402,14 +409,9 @@ impl Basis2D { self.glam(|mat| mat.inverse()) } - /// Returns whether each component is finite. - pub fn is_finite(&self) -> bool { - self.glam(|mat| mat.is_finite()) - } - /// Returns the orthonormalized version of the basis. #[must_use] - pub fn orthonormalized(self) -> Self { + pub(crate) fn orthonormalized(self) -> Self { assert!( !is_equal_approx(self.determinant(), 0.0), "Determinant should not be zero." @@ -426,21 +428,15 @@ impl Basis2D { Self::from_cols(x, y) } - /// Introduces an additional rotation. - #[must_use] - pub fn rotated(self, angle: f32) -> Self { - self * Self::from_angle(angle) - } - /// Returns the rotation of the matrix - pub fn rotation(&self) -> f32 { + pub(crate) fn rotation(&self) -> f32 { // Translated from Godot f32::atan2(self.cols[0].y, self.cols[0].x) } /// Returns the skew of the matrix #[must_use] - pub fn skew(&self) -> f32 { + pub(crate) fn skew(&self) -> f32 { // Translated from Godot let det_sign = self.determinant().signum(); self.cols[0] @@ -450,37 +446,21 @@ impl Basis2D { - PI * 0.5 } - /// Returns `true` if this basis and `other` are approximately equal, - /// by calling `is_equal_approx` on each column. - pub fn is_equal_approx(&self, other: &Self) -> bool { - self.cols[0].is_equal_approx(other.cols[0]) && self.cols[1].is_equal_approx(other.cols[1]) - } - - /// Returns the transposed version of the matrix. - pub const fn transposed(self) -> Self { - Self { - cols: [ - Vector2::new(self.cols[0].x, self.cols[1].x), - Vector2::new(self.cols[0].y, self.cols[1].y), - ], - } - } - - pub fn set_row_a(&mut self, v: Vector2) { + pub(crate) fn set_row_a(&mut self, v: Vector2) { self.cols[0].x = v.x; self.cols[1].x = v.y; } - pub fn row_a(&self) -> Vector2 { + pub(crate) fn row_a(&self) -> Vector2 { Vector2::new(self.cols[0].x, self.cols[1].x) } - pub fn set_row_b(&mut self, v: Vector2) { + pub(crate) fn set_row_b(&mut self, v: Vector2) { self.cols[0].y = v.x; self.cols[1].y = v.y; } - pub fn row_b(&self) -> Vector2 { + pub(crate) fn row_b(&self) -> Vector2 { Vector2::new(self.cols[0].y, self.cols[1].y) } } @@ -589,7 +569,7 @@ mod test { // Tests translated from Godot. - const DUMMY_TRANSFORM: Transform2D = Transform2D::new( + const DUMMY_TRANSFORM: Transform2D = Transform2D::from_basis_origin( Basis2D::from_cols(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0)), Vector2::new(5.0, 6.0), ); @@ -701,38 +681,39 @@ mod test { let infinite: Vector2 = Vector2::new(f32::NAN, f32::NAN); assert!( - Transform2D::new(Basis2D::from_cols(x, x), x).is_finite(), + Transform2D::from_basis_origin(Basis2D::from_cols(x, x), x).is_finite(), "let with: Transform2D all components finite should be finite", ); assert!( - !Transform2D::new(Basis2D::from_cols(infinite, x), x).is_finite(), + !Transform2D::from_basis_origin(Basis2D::from_cols(infinite, x), x).is_finite(), "let with: Transform2D one component infinite should not be finite.", ); assert!( - !Transform2D::new(Basis2D::from_cols(x, infinite), x).is_finite(), + !Transform2D::from_basis_origin(Basis2D::from_cols(x, infinite), x).is_finite(), "let with: Transform2D one component infinite should not be finite.", ); assert!( - !Transform2D::new(Basis2D::from_cols(x, x), infinite).is_finite(), + !Transform2D::from_basis_origin(Basis2D::from_cols(x, x), infinite).is_finite(), "let with: Transform2D one component infinite should not be finite.", ); assert!( - !Transform2D::new(Basis2D::from_cols(infinite, infinite), x).is_finite(), + !Transform2D::from_basis_origin(Basis2D::from_cols(infinite, infinite), x).is_finite(), "let with: Transform2D two components infinite should not be finite.", ); assert!( - !Transform2D::new(Basis2D::from_cols(infinite, x), infinite).is_finite(), + !Transform2D::from_basis_origin(Basis2D::from_cols(infinite, x), infinite).is_finite(), "let with: Transform2D two components infinite should not be finite.", ); assert!( - !Transform2D::new(Basis2D::from_cols(x, infinite), infinite).is_finite(), + !Transform2D::from_basis_origin(Basis2D::from_cols(x, infinite), infinite).is_finite(), "let with: Transform2D two components infinite should not be finite.", ); assert!( - !Transform2D::new(Basis2D::from_cols(infinite, infinite), infinite).is_finite(), + !Transform2D::from_basis_origin(Basis2D::from_cols(infinite, infinite), infinite) + .is_finite(), "let with: Transform2D three components infinite should not be finite.", ); } diff --git a/itest/rust/src/transform2d_test.rs b/itest/rust/src/transform2d_test.rs index f9c783395..3334e73ea 100644 --- a/itest/rust/src/transform2d_test.rs +++ b/itest/rust/src/transform2d_test.rs @@ -8,8 +8,9 @@ use crate::itest; use godot::prelude::{inner::InnerTransform2D, *}; use godot::private::class_macros::assert_eq_approx; -const TEST_TRANSFORM: Transform2D = Transform2D::new( - Basis2D::from_cols(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0)), +const TEST_TRANSFORM: Transform2D = Transform2D::from_cols( + Vector2::new(1.0, 2.0), + Vector2::new(3.0, 4.0), Vector2::new(5.0, 6.0), ); @@ -50,18 +51,18 @@ fn transform2d_equiv() { inner.get_rotation(), outer.rotation(), |a, b| is_equal_approx(a as f32, b), - "function = get_rotation\n" + "function: get_rotation\n" ); assert_eq_approx!( inner.get_rotation(), outer.rotation(), |a, b| is_equal_approx(a as f32, b), - "function = get_rotation\n" + "function: get_rotation\n" ); assert_eq_approx!( inner.get_skew(), outer.skew(), |a, b| is_equal_approx(a as f32, b), - "function = get_scale\n" + "function: get_scale\n" ); }