diff --git a/CHANGELOG.md b/CHANGELOG.md index b1db0f81..52146f4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added implementation of `HashMap.retain()`. - Added support for affine backgrounds (tiled modes 1 and 2) which allows for scaling, rotating etc of tiled backgrounds. - Added support for 256 colour backgrounds (when working with affine ones). +- Added affine matrix module. This allows for manipulation of affine matricies for use in backgrounds and in the future objects. - Added support for dynamic sprites generated at runtime, some parts of this may change significantly so breaking changes are expected here. ### Changes diff --git a/agb/src/display/affine.rs b/agb/src/display/affine.rs new file mode 100644 index 00000000..222293ec --- /dev/null +++ b/agb/src/display/affine.rs @@ -0,0 +1,246 @@ +#![deny(missing_docs)] +//! # Affine matricies for the Game Boy Advance +//! +//! An affine matrix represents an affine transformation, an affine +//! transformation being one which preserves parallel lines (note that this +//! therefore cannot represent perspective seen in games like Super Mario Kart). +//! Affine matricies are used in two places on the GBA, for affine backgrounds +//! and for affine objects. +//! +//! # Linear Algebra basics +//! As a matrix, they can be manipulated using linear algebra, although you +//! shouldn't need to know linear algebra to use this apart from a few things +//! +//! If `A` and `B` are matricies, then matrix `C = A * B` represents the +//! transformation `A` performed on `B`, or alternatively `C` is transformation +//! `B` followed by transformation `A`. +//! +//! Additionally matrix multiplication is not commutative, meaning swapping the +//! order changes the result, or `A * B ≢ B * A`. + +use core::{ + convert::{TryFrom, TryInto}, + ops::{Mul, MulAssign}, +}; + +use agb_fixnum::{Num, Vector2D}; + +type AffineMatrixElement = Num; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +/// An affine matrix stored in a way that is efficient for the GBA to perform +/// operations on. This implements multiplication. +pub struct AffineMatrix { + a: AffineMatrixElement, + b: AffineMatrixElement, + c: AffineMatrixElement, + d: AffineMatrixElement, + x: AffineMatrixElement, + y: AffineMatrixElement, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// The error emitted upon a conversion that could not be performed due to +/// overflowing the destination data size +pub struct OverflowError(pub(crate) ()); + +impl AffineMatrix { + #[must_use] + /// The Identity matrix. The identity matrix can be thought of as 1 and is + /// represented by `I`. For a matrix `A`, `A ≡ A * I ≡ I * A`. + pub fn identity() -> Self { + AffineMatrix { + a: 1.into(), + b: 0.into(), + c: 0.into(), + d: 1.into(), + x: 0.into(), + y: 0.into(), + } + } + + #[must_use] + /// Generates the matrix that represents a rotation + pub fn from_rotation(angle: Num) -> Self { + fn from_rotation(angle: Num) -> AffineMatrix { + let cos = angle.cos().change_base(); + let sin = angle.sin().change_base(); + + // This might look backwards, but the gba does texture mapping, ie a + // point in screen base is transformed using the matrix to graphics + // space rather than how you might conventionally think of it. + AffineMatrix { + a: cos, + b: sin, + c: -sin, + d: cos, + x: 0.into(), + y: 0.into(), + } + } + from_rotation(angle.rem_euclid(1.into()).change_base()) + } + + // Identity for rotation / scale / skew + /// Generates the matrix that represents a translation by the position + #[must_use] + pub fn from_translation(position: Vector2D>) -> Self { + AffineMatrix { + a: 1.into(), + b: 0.into(), + c: 0.into(), + d: 1.into(), + x: position.x, + y: position.y, + } + } + + #[must_use] + /// The position fields of the matrix + pub fn position(&self) -> Vector2D> { + (self.x, self.y).into() + } + + /// Attempts to convert the matrix to one which can be used in affine + /// backgrounds. + pub fn try_to_background(&self) -> Result { + Ok(AffineMatrixBackground { + a: self.a.to_raw().try_into().map_err(|_| OverflowError(()))?, + b: self.a.to_raw().try_into().map_err(|_| OverflowError(()))?, + c: self.a.to_raw().try_into().map_err(|_| OverflowError(()))?, + d: self.a.to_raw().try_into().map_err(|_| OverflowError(()))?, + x: self.a.to_raw(), + y: self.a.to_raw(), + }) + } + + #[must_use] + /// Converts the matrix to one which can be used in affine backgrounds + /// wrapping any value which is too large to be represented there. + pub fn to_background_wrapping(&self) -> AffineMatrixBackground { + AffineMatrixBackground { + a: self.a.to_raw() as i16, + b: self.a.to_raw() as i16, + c: self.a.to_raw() as i16, + d: self.a.to_raw() as i16, + x: self.a.to_raw(), + y: self.a.to_raw(), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[repr(C, packed(4))] +/// An affine matrix that can be used in affine backgrounds +pub struct AffineMatrixBackground { + // Internally these can be thought of as Num + a: i16, + b: i16, + c: i16, + d: i16, + // These are Num + x: i32, + y: i32, +} + +impl TryFrom for AffineMatrixBackground { + type Error = OverflowError; + + fn try_from(value: AffineMatrix) -> Result { + value.try_to_background() + } +} + +impl AffineMatrixBackground { + #[must_use] + /// Converts to the affine matrix that is usable in performing efficient + /// calculations. + pub fn to_affine_matrix(&self) -> AffineMatrix { + AffineMatrix { + a: Num::from_raw(self.a.into()), + b: Num::from_raw(self.b.into()), + c: Num::from_raw(self.c.into()), + d: Num::from_raw(self.d.into()), + x: Num::from_raw(self.x), + y: Num::from_raw(self.y), + } + } +} + +impl From for AffineMatrix { + fn from(mat: AffineMatrixBackground) -> Self { + mat.to_affine_matrix() + } +} + +impl Default for AffineMatrix { + fn default() -> Self { + AffineMatrix::identity() + } +} + +impl Mul for AffineMatrix { + type Output = Self; + fn mul(self, rhs: Self) -> Self::Output { + AffineMatrix { + a: self.a * rhs.a + self.b + rhs.c, + b: self.a * rhs.b + self.b * rhs.d, + c: self.c * rhs.a + self.d * rhs.c, + d: self.c * rhs.b + self.d * rhs.d, + x: self.a * rhs.x + self.b * rhs.y + self.x, + y: self.c * rhs.x + self.d * rhs.y + self.y, + } + } +} + +impl Mul> for AffineMatrix { + type Output = Self; + fn mul(self, rhs: Num) -> Self::Output { + self * AffineMatrix { + a: rhs, + b: 0.into(), + c: 0.into(), + d: rhs, + x: 0.into(), + y: 0.into(), + } + } +} + +impl MulAssign> for AffineMatrix { + fn mul_assign(&mut self, rhs: Num) { + *self = *self * rhs; + } +} + +impl MulAssign for AffineMatrix { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +#[cfg(test)] +mod tests { + use crate::fixnum::num; + + use super::*; + + #[test_case] + fn test_simple_multiply(_: &mut crate::Gba) { + let position = (20, 10).into(); + + let a = AffineMatrix::from_translation(position); + let b = AffineMatrix::default(); + + let c = a * b; + + assert_eq!(c.position(), position); + + let d = AffineMatrix::from_rotation::<2>(num!(0.5)); + + let e = a * d; + + assert_eq!(e.position(), position); + assert_eq!(d * d, AffineMatrix::identity()); + } +} diff --git a/agb/src/display/mod.rs b/agb/src/display/mod.rs index 620c1070..ab0110eb 100644 --- a/agb/src/display/mod.rs +++ b/agb/src/display/mod.rs @@ -25,6 +25,7 @@ pub mod video; pub mod blend; pub mod window; +pub mod affine; mod font; pub use font::{Font, FontLetter};