diff --git a/citro3d/src/math.rs b/citro3d/src/math.rs index 479ef71..22634ab 100644 --- a/citro3d/src/math.rs +++ b/citro3d/src/math.rs @@ -1,19 +1,22 @@ //! Safe wrappers for working with matrix and vector types provided by `citro3d`. +// TODO: bench FFI calls into `inline statics` generated by bindgen, vs +// reimplementing some of those calls. Many of them are pretty trivial impls + use std::mem::MaybeUninit; mod projection; pub use projection::{Orthographic, Perspective, Projection}; +mod fvec; +mod ops; + +pub use fvec::{FVec, FVec3, FVec4}; /// A 4-vector of `u8`s. #[doc(alias = "C3D_IVec")] pub struct IVec(citro3d_sys::C3D_IVec); -/// A 4-vector of `f32`s. -#[doc(alias = "C3D_FVec")] -pub struct FVec(citro3d_sys::C3D_FVec); - /// A quaternion, internally represented the same way as [`FVec`]. #[doc(alias = "C3D_FQuat")] pub struct FQuat(citro3d_sys::C3D_FQuat); diff --git a/citro3d/src/math/fvec.rs b/citro3d/src/math/fvec.rs new file mode 100644 index 0000000..b1d7f12 --- /dev/null +++ b/citro3d/src/math/fvec.rs @@ -0,0 +1,177 @@ +//! Floating-point vectors. + +use std::fmt; + +/// A vector of `f32`s. +#[derive(Clone, Copy)] +pub struct FVec(pub(super) citro3d_sys::C3D_FVec); + +/// A 3-vector of `f32`s. +pub type FVec3 = FVec<3>; + +/// A 4-vector of `f32`s. +pub type FVec4 = FVec<4>; + +impl fmt::Debug for FVec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let inner = unsafe { self.0.c }; + f.debug_tuple(std::any::type_name::()) + .field(&inner) + .finish() + } +} + +impl PartialEq for FVec +where + Rhs: Copy, + Self: From, +{ + fn eq(&self, other: &Rhs) -> bool { + unsafe { self.0.c == Self::from(*other).0.c } + } +} + +impl Eq for FVec {} + +impl FVec4 { + /// Create a new [`FVec4`] from its components. + pub fn new(x: f32, y: f32, z: f32, w: f32) -> Self { + Self(unsafe { citro3d_sys::FVec4_New(x, y, z, w) }) + } + + /// Create a new [`FVec4`], setting each component to `v`. + pub fn splat(v: f32) -> Self { + Self::new(v, v, v, v) + } + + /// The vector's `w` component (sometimes also called the `r` component of `ijk[r]`). + #[doc(alias = "r")] + pub fn w(&self) -> f32 { + unsafe { self.0.__bindgen_anon_1.w } + } + + /// Divide the vector's XYZ components by its W component. + pub fn perspective_divide(&self) -> Self { + Self(unsafe { citro3d_sys::FVec4_PerspDivide(self.0) }) + } + + /// The dot product of two vectors. + pub fn dot(&self, rhs: &Self) -> f32 { + unsafe { citro3d_sys::FVec3_Dot(self.0, rhs.0) } + } + + /// The magnitude of the vector. + pub fn magnitude(&self) -> f32 { + unsafe { citro3d_sys::FVec3_Magnitude(self.0) } + } + + /// Normalize the vector to a magnitude of `1.0`. + pub fn normalize(&self) -> Self { + Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) }) + } +} + +impl FVec3 { + /// Create a new [`FVec3`] from its components. + pub fn new(x: f32, y: f32, z: f32) -> Self { + Self(unsafe { citro3d_sys::FVec3_New(x, y, z) }) + } + + /// Create a new [`FVec3`], setting each component to the given `v`. + pub fn splat(v: f32) -> Self { + Self::new(v, v, v) + } + + /// The distance between two points in 3D space. + pub fn distance(&self, rhs: &Self) -> f32 { + unsafe { citro3d_sys::FVec3_Distance(self.0, rhs.0) } + } + + /// The cross product of two 3D vectors. + pub fn cross(&self, rhs: &Self) -> Self { + Self(unsafe { citro3d_sys::FVec3_Cross(self.0, rhs.0) }) + } + + /// The dot product of two vectors. + pub fn dot(&self, rhs: &Self) -> f32 { + unsafe { citro3d_sys::FVec3_Dot(self.0, rhs.0) } + } + + /// The magnitude of the vector. + pub fn magnitude(&self) -> f32 { + unsafe { citro3d_sys::FVec3_Magnitude(self.0) } + } + + /// Normalize the vector to a magnitude of `1.0`. + pub fn normalize(&self) -> Self { + Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) }) + } +} + +impl FVec { + /// The vector's `x` component (sometimes also called the `i` component of `ijk[r]`). + pub fn x(&self) -> f32 { + unsafe { self.0.__bindgen_anon_1.x } + } + + /// The vector's `y` component (sometimes also called the `j` component of `ijk[r]`). + pub fn y(&self) -> f32 { + unsafe { self.0.__bindgen_anon_1.y } + } + + /// The vector's `i` component (sometimes also called the `k` component of `ijk[r]`). + pub fn z(&self) -> f32 { + unsafe { self.0.__bindgen_anon_1.z } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fvec4() { + let l = FVec4::new(2.0, 2.0, 2.0, 2.0); + + assert_eq!(l, FVec4::splat(2.0)); + + for component in [l.x(), l.y(), l.z(), l.w()] { + assert!((component - 2.0).abs() < f32::EPSILON); + } + + assert_eq!(l.perspective_divide(), FVec4::splat(1.0)); + + let dot = l.dot(&FVec4::splat(3.0)); + assert!((dot - 24.0).abs() < f32::EPSILON); + + assert!((l.magnitude() - 8.0).abs() < f32::EPSILON); + + let norm = l.normalize(); + assert!((norm.magnitude() - 1.0).abs() < f32::EPSILON); + for component in [l.y(), l.z(), l.w()] { + assert!((component - l.x()).abs() < f32::EPSILON); + } + } + + #[test] + fn fvec3() { + let l = FVec3::new(2.0, 2.0, 2.0); + + assert_eq!(l, FVec3::splat(2.0)); + + for component in [l.x(), l.y(), l.z()] { + assert!((component - 2.0).abs() < f32::EPSILON); + } + + let dot = l.dot(&FVec3::splat(3.0)); + assert!((dot - 18.0).abs() < f32::EPSILON); + + assert!((l.magnitude() - 8.0).abs() < f32::EPSILON); + + let norm = l.normalize(); + assert!((norm.magnitude() - 1.0).abs() < f32::EPSILON); + for component in [l.y(), l.z()] { + assert!((l.x() - component).abs() < f32::EPSILON); + } + } +} diff --git a/citro3d/src/math/ops.rs b/citro3d/src/math/ops.rs new file mode 100644 index 0000000..dffe352 --- /dev/null +++ b/citro3d/src/math/ops.rs @@ -0,0 +1,114 @@ +use std::borrow::Borrow; +use std::ops::{Add, Div, Mul, Neg, Sub}; + +use super::{FVec, FVec3, FVec4}; + +impl> Add for FVec4 { + type Output = Self; + + fn add(self, rhs: Rhs) -> Self::Output { + Self(unsafe { citro3d_sys::FVec4_Add(self.0, rhs.borrow().0) }) + } +} + +impl> Sub for FVec4 { + type Output = Self; + + fn sub(self, rhs: Rhs) -> Self::Output { + Self(unsafe { citro3d_sys::FVec4_Add(self.0, rhs.borrow().0) }) + } +} + +impl Neg for FVec4 { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(unsafe { citro3d_sys::FVec4_Negate(self.0) }) + } +} + +impl Mul for FVec4 { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self(unsafe { citro3d_sys::FVec4_Scale(self.0, rhs) }) + } +} + +impl> Add for FVec3 { + type Output = Self; + + fn add(self, rhs: Rhs) -> Self::Output { + Self(unsafe { citro3d_sys::FVec3_Add(self.0, rhs.borrow().0) }) + } +} + +impl> Sub for FVec3 { + type Output = Self; + + fn sub(self, rhs: Rhs) -> Self::Output { + Self(unsafe { citro3d_sys::FVec3_Add(self.0, rhs.borrow().0) }) + } +} + +impl Neg for FVec3 { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(unsafe { citro3d_sys::FVec3_Negate(self.0) }) + } +} + +impl Mul for FVec3 { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self(unsafe { citro3d_sys::FVec3_Scale(self.0, rhs) }) + } +} + +impl Div for FVec +where + FVec: Mul, +{ + type Output = >::Output; + + fn div(self, rhs: f32) -> Self::Output { + self * (1.0 / rhs) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::op_ref)] + + use super::*; + + #[test] + fn vec3_ops() { + let l = FVec3::splat(1.0); + let r = FVec3::splat(2.0); + + assert_eq!(l + r, FVec3::splat(3.0)); + assert_eq!(l + &r, FVec3::splat(3.0)); + assert_eq!(l - r, FVec3::splat(-1.0)); + assert_eq!(l - &r, FVec3::splat(-1.0)); + assert_eq!(-l, FVec3::splat(-1.0)); + assert_eq!(l * 1.5, FVec3::splat(1.5)); + assert_eq!(l / 2.0, FVec3::splat(0.5)); + } + + #[test] + fn vec4_ops() { + let l = FVec4::splat(1.0); + let r = FVec4::splat(2.0); + + assert_eq!(l + r, FVec4::splat(3.0)); + assert_eq!(l + &r, FVec4::splat(3.0)); + assert_eq!(l - r, FVec4::splat(-1.0)); + assert_eq!(l - &r, FVec4::splat(-1.0)); + assert_eq!(-l, FVec4::splat(-1.0)); + assert_eq!(l * 1.5, FVec4::splat(1.5)); + assert_eq!(l / 2.0, FVec4::splat(0.5)); + } +}