Browse Source

Merge pull request #28 from rust3ds/feature/more-math

More math types and operator overloads
pull/32/head
Ian Chamberlain 1 year ago committed by GitHub
parent
commit
c4ae496e57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .github/workflows/ci.yml
  2. 3
      citro3d-sys/Cargo.toml
  3. 4
      citro3d-sys/src/lib.rs
  4. 21
      citro3d/Cargo.toml
  5. 8
      citro3d/examples/triangle.rs
  6. 9
      citro3d/src/lib.rs
  7. 183
      citro3d/src/math.rs
  8. 266
      citro3d/src/math/fvec.rs
  9. 214
      citro3d/src/math/matrix.rs
  10. 285
      citro3d/src/math/ops.rs
  11. 156
      citro3d/src/math/projection.rs
  12. 21
      citro3d/src/uniform.rs

4
.github/workflows/ci.yml

@ -5,13 +5,12 @@ on: @@ -5,13 +5,12 @@ on:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
jobs:
lint:
strategy:
fail-fast: false
matrix:
toolchain:
# Run against a "known good" nightly. Rustc version is 1 day behind the toolchain date
@ -48,6 +47,7 @@ jobs: @@ -48,6 +47,7 @@ jobs:
test:
strategy:
fail-fast: false
matrix:
toolchain:
- nightly-2023-06-01

3
citro3d-sys/Cargo.toml

@ -14,3 +14,6 @@ ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" } @@ -14,3 +14,6 @@ ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" }
bindgen = { version = "0.68.1", features = ["experimental"] }
cc = "1.0.83"
doxygen-rs = "0.4.2"
[dev-dependencies]
shim-3ds = { git = "https://github.com/rust3ds/shim-3ds.git" }

4
citro3d-sys/src/lib.rs

@ -9,3 +9,7 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs")); @@ -9,3 +9,7 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
pub mod gx;
pub use gx::*;
// Prevent linking errors from the standard `test` library when running `cargo 3ds test --lib`.
#[cfg(test)]
extern crate shim_3ds;

21
citro3d/Cargo.toml

@ -6,13 +6,32 @@ version = "0.1.0" @@ -6,13 +6,32 @@ version = "0.1.0"
edition = "2021"
[dependencies]
citro3d-macros = { version = "0.1.0", path = "../citro3d-macros" }
approx = { version = "0.5.1", optional = true }
bitflags = "1.3.2"
bytemuck = { version = "1.10.0", features = ["extern_crate_std"] }
citro3d-macros = { version = "0.1.0", path = "../citro3d-macros" }
citro3d-sys = { git = "https://github.com/rust3ds/citro3d-rs.git" }
ctru-rs = { git = "https://github.com/rust3ds/ctru-rs.git" }
ctru-sys = { git = "https://github.com/rust3ds/ctru-rs.git" }
document-features = "0.2.7"
libc = "0.2.125"
[features]
default = []
## Enable this feature to use the `approx` crate for comparing vectors and matrices.
approx = ["dep:approx"]
[dev-dependencies]
test-runner = { git = "https://github.com/rust3ds/test-runner.git" }
[dev-dependencies.citro3d]
# Basically, this works like `cargo 3ds test --features ...` for building tests
# https://github.com/rust-lang/cargo/issues/2911#issuecomment-749580481
path = "."
features = ["approx"]
[package.metadata.docs.rs]
all-features = true
default-target = "armv6k-nintendo-3ds"
targs = []
cargo-args = ["-Z", "build-std"]

8
citro3d/examples/triangle.rs

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
#![feature(allocator_api)]
use citro3d::macros::include_shader;
use citro3d::math::{AspectRatio, ClipPlanes, Matrix, Projection, StereoDisplacement};
use citro3d::math::{AspectRatio, ClipPlanes, Matrix4, Projection, StereoDisplacement};
use citro3d::render::ClearFlags;
use citro3d::{attrib, buffer, render, shader};
use ctru::prelude::*;
@ -158,9 +158,9 @@ where @@ -158,9 +158,9 @@ where
}
struct Projections {
left_eye: Matrix,
right_eye: Matrix,
center: Matrix,
left_eye: Matrix4,
right_eye: Matrix4,
center: Matrix4,
}
fn calculate_projections() -> Projections {

9
citro3d/src/lib.rs

@ -1,6 +1,13 @@ @@ -1,6 +1,13 @@
//! Safe Rust bindings to `citro3d`.
#![feature(custom_test_frameworks)]
#![test_runner(test_runner::run_gdb)]
#![feature(doc_cfg)]
#![feature(doc_auto_cfg)]
//! Safe Rust bindings to `citro3d`. This crate wraps `citro3d-sys` to provide
//! safer APIs for graphics programs targeting the 3DS.
//!
//! ## Feature flags
#![doc = document_features::document_features!()]
pub mod attrib;
pub mod buffer;

183
citro3d/src/math.rs

@ -1,185 +1,24 @@ @@ -1,185 +1,24 @@
//! Safe wrappers for working with matrix and vector types provided by `citro3d`.
use std::mem::MaybeUninit;
// TODO: bench FFI calls into `inline statics` generated by bindgen, vs
// reimplementing some of those calls. Many of them are pretty trivial impls
mod fvec;
mod matrix;
mod ops;
mod projection;
pub use projection::{Orthographic, Perspective, Projection};
pub use fvec::{FVec, FVec3, FVec4};
pub use matrix::{Matrix, Matrix3, Matrix4};
pub use projection::{
AspectRatio, ClipPlanes, CoordinateOrientation, Orthographic, Perspective, Projection,
ScreenOrientation, StereoDisplacement,
};
/// 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);
/// A 4x4 row-major matrix of `f32`s.
#[doc(alias = "C3D_Mtx")]
pub struct Matrix(citro3d_sys::C3D_Mtx);
impl Matrix {
/// Construct the zero matrix.
#[doc(alias = "Mtx_Zeros")]
pub fn zero() -> Self {
// TODO: should this also be Default::default()?
let mut out = MaybeUninit::uninit();
unsafe {
citro3d_sys::Mtx_Zeros(out.as_mut_ptr());
Self(out.assume_init())
}
}
/// Construct the identity matrix.
#[doc(alias = "Mtx_Identity")]
pub fn identity() -> Self {
let mut out = MaybeUninit::uninit();
unsafe {
citro3d_sys::Mtx_Identity(out.as_mut_ptr());
Self(out.assume_init())
}
}
pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx {
&self.0
}
}
// region: Projection configuration
//
// TODO: maybe move into `mod projection`, or hoist `projection::*` into here.
// it will probably mostly depend on how big all the matrices/vec impls get.
// Also worth considering is whether `mod projection` should be pub.
/// The [orientation](https://en.wikipedia.org/wiki/Orientation_(geometry))
/// (or "handedness") of the coordinate system. Coordinates are always +Y-up,
/// +X-right.
#[derive(Clone, Copy, Debug)]
pub enum CoordinateOrientation {
/// A left-handed coordinate system. +Z points into the screen.
LeftHanded,
/// A right-handed coordinate system. +Z points out of the screen.
RightHanded,
}
impl CoordinateOrientation {
pub(crate) fn is_left_handed(self) -> bool {
matches!(self, Self::LeftHanded)
}
}
impl Default for CoordinateOrientation {
/// This is an opinionated default, but [`RightHanded`](Self::RightHanded)
/// seems to be the preferred coordinate system for most
/// [examples](https://github.com/devkitPro/3ds-examples)
/// from upstream, and is also fairly common in other applications.
fn default() -> Self {
Self::RightHanded
}
}
/// Whether to rotate a projection to account for the 3DS screen orientation.
/// Both screens on the 3DS are oriented such that the "top-left" of the screen
/// in framebuffer coordinates is the physical bottom-left of the screen
/// (i.e. the "width" is smaller than the "height").
#[derive(Clone, Copy, Debug)]
pub enum ScreenOrientation {
/// Rotate 90° clockwise to account for the 3DS screen rotation. Most
/// applications will use this variant.
Rotated,
/// Do not apply any extra rotation to the projection.
None,
}
impl Default for ScreenOrientation {
fn default() -> Self {
Self::Rotated
}
}
/// Configuration for calculating stereoscopic projections.
// TODO: not totally happy with this name + API yet, but it works for now.
#[derive(Clone, Copy, Debug)]
pub struct StereoDisplacement {
/// The horizontal offset of the eye from center. Negative values
/// correspond to the left eye, and positive values to the right eye.
pub displacement: f32,
/// The position of the screen, which determines the focal length. Objects
/// closer than this depth will appear to pop out of the screen, and objects
/// further than this will appear inside the screen.
pub screen_depth: f32,
}
impl StereoDisplacement {
/// Construct displacement for the left and right eyes simulataneously.
/// The given `interocular_distance` describes the distance between the two
/// rendered "eyes". A negative value will be treated the same as a positive
/// value of the same magnitude.
///
/// See struct documentation for details about the
/// [`screen_depth`](Self::screen_depth) parameter.
pub fn new(interocular_distance: f32, screen_depth: f32) -> (Self, Self) {
let displacement = interocular_distance.abs() / 2.0;
let left_eye = Self {
displacement: -displacement,
screen_depth,
};
let right_eye = Self {
displacement,
screen_depth,
};
(left_eye, right_eye)
}
}
/// Configuration for the clipping planes of a projection.
///
/// For [`Perspective`] projections, this is used for the near and far clip planes
/// of the [view frustum](https://en.wikipedia.org/wiki/Viewing_frustum).
///
/// For [`Orthographic`] projections, this is used for the Z clipping planes of
/// the projection.
///
/// Note that the `near` value should always be less than `far`, regardless of
/// [`CoordinateOrientation`]. In other words, these values will be negated
/// when used with a [`RightHanded`](CoordinateOrientation::RightHanded)
/// orientation.
#[derive(Clone, Copy, Debug)]
pub struct ClipPlanes {
/// The Z-depth of the near clip plane, usually close or equal to zero.
pub near: f32,
/// The Z-depth of the far clip plane, usually greater than zero.
pub far: f32,
}
/// The aspect ratio of a projection plane.
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum AspectRatio {
/// The aspect ratio of the 3DS' top screen (per-eye).
#[doc(alias = "C3D_AspectRatioTop")]
TopScreen,
/// The aspect ratio of the 3DS' bottom screen.
#[doc(alias = "C3D_AspectRatioBot")]
BottomScreen,
/// A custom aspect ratio (should be calcualted as `width / height`).
Other(f32),
}
impl From<AspectRatio> for f32 {
fn from(ratio: AspectRatio) -> Self {
match ratio {
AspectRatio::TopScreen => citro3d_sys::C3D_AspectRatioTop as f32,
AspectRatio::BottomScreen => citro3d_sys::C3D_AspectRatioBot as f32,
AspectRatio::Other(ratio) => ratio,
}
}
}
// endregion

266
citro3d/src/math/fvec.rs

@ -0,0 +1,266 @@ @@ -0,0 +1,266 @@
//! Floating-point vectors.
use std::fmt;
/// A vector of `f32`s.
#[derive(Clone, Copy)]
pub struct FVec<const N: usize>(pub(crate) 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<const N: usize> fmt::Debug for FVec<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let inner = unsafe { self.0.__bindgen_anon_1 };
let type_name = std::any::type_name::<Self>().split("::").last().unwrap();
f.debug_tuple(type_name).field(&inner).finish()
}
}
impl<const N: usize> FVec<N> {
/// The vector's `x` component (also called the `i` component of `ijk[r]`).
#[doc(alias = "i")]
pub fn x(self) -> f32 {
unsafe { self.0.__bindgen_anon_1.x }
}
/// The vector's `y` component (also called the `j` component of `ijk[r]`).
#[doc(alias = "j")]
pub fn y(self) -> f32 {
unsafe { self.0.__bindgen_anon_1.y }
}
/// The vector's `i` component (also called the `k` component of `ijk[r]`).
#[doc(alias = "k")]
pub fn z(self) -> f32 {
unsafe { self.0.__bindgen_anon_1.z }
}
}
impl FVec4 {
/// The vector's `w` component (also called `r` for the real component of `ijk[r]`).
#[doc(alias = "r")]
pub fn w(self) -> f32 {
unsafe { self.0.__bindgen_anon_1.w }
}
/// Create a new [`FVec4`] from its components.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec4;
/// let v = FVec4::new(1.0, 2.0, 3.0, 4.0);
/// ```
#[doc(alias = "FVec4_New")]
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`.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec4;
/// # use approx::assert_abs_diff_eq;
/// let v = FVec4::splat(1.0);
/// assert_abs_diff_eq!(v, FVec4::new(1.0, 1.0, 1.0, 1.0));
/// ```
pub fn splat(v: f32) -> Self {
Self::new(v, v, v, v)
}
/// Divide the vector's XYZ components by its W component.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec4;
/// # use approx::assert_abs_diff_eq;
/// let v = FVec4::new(2.0, 4.0, 6.0, 2.0);
/// assert_abs_diff_eq!(v.perspective_divide(), FVec4::new(1.0, 2.0, 3.0, 1.0));
/// ```
#[doc(alias = "FVec4_PerspDivide")]
pub fn perspective_divide(self) -> Self {
Self(unsafe { citro3d_sys::FVec4_PerspDivide(self.0) })
}
/// The dot product of two vectors.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec4;
/// # use approx::assert_abs_diff_eq;
/// let v1 = FVec4::new(1.0, 2.0, 3.0, 4.0);
/// let v2 = FVec4::new(1.0, 0.5, 1.0, 0.5);
/// assert_abs_diff_eq!(v1.dot(v2), 7.0);
/// ```
#[doc(alias = "FVec4_Dot")]
pub fn dot(self, rhs: Self) -> f32 {
unsafe { citro3d_sys::FVec4_Dot(self.0, rhs.0) }
}
/// The magnitude of the vector.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec4;
/// # use approx::assert_abs_diff_eq;
/// let v = FVec4::splat(1.0);
/// assert_abs_diff_eq!(v.magnitude(), 2.0);
/// ```
#[doc(alias = "FVec4_Magnitude")]
pub fn magnitude(self) -> f32 {
unsafe { citro3d_sys::FVec4_Magnitude(self.0) }
}
/// Normalize the vector to a magnitude of `1.0`.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec4;
/// # use approx::assert_abs_diff_eq;
/// let v = FVec4::new(1.0, 2.0, 2.0, 4.0);
/// assert_abs_diff_eq!(v.normalize(), FVec4::new(0.2, 0.4, 0.4, 0.8));
/// ```
#[doc(alias = "FVec3_Normalize")]
pub fn normalize(self) -> Self {
Self(unsafe { citro3d_sys::FVec4_Normalize(self.0) })
}
}
impl FVec3 {
/// Create a new [`FVec3`] from its components.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec3;
/// let v = FVec3::new(1.0, 2.0, 3.0);
/// ```
#[doc(alias = "FVec3_New")]
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`.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec3;
/// let v = FVec3::splat(1.0);
/// ```
pub fn splat(v: f32) -> Self {
Self::new(v, v, v)
}
/// The distance between two points in 3D space.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec3;
/// # use approx::assert_abs_diff_eq;
/// let l = FVec3::new(1.0, 3.0, 4.0);
/// let r = FVec3::new(0.0, 1.0, 2.0);
///
/// assert_abs_diff_eq!(l.distance(r), 3.0);
/// ```
#[doc(alias = "FVec3_Distance")]
pub fn distance(self, rhs: Self) -> f32 {
unsafe { citro3d_sys::FVec3_Distance(self.0, rhs.0) }
}
/// The cross product of two 3D vectors.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec3;
/// # use approx::assert_abs_diff_eq;
/// let l = FVec3::new(1.0, 0.0, 0.0);
/// let r = FVec3::new(0.0, 1.0, 0.0);
/// assert_abs_diff_eq!(l.cross(r), FVec3::new(0.0, 0.0, 1.0));
/// ```
#[doc(alias = "FVec3_Cross")]
pub fn cross(self, rhs: Self) -> Self {
Self(unsafe { citro3d_sys::FVec3_Cross(self.0, rhs.0) })
}
/// The dot product of two vectors.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec3;
/// # use approx::assert_abs_diff_eq;
/// let l = FVec3::new(1.0, 2.0, 3.0);
/// let r = FVec3::new(3.0, 2.0, 1.0);
/// assert_abs_diff_eq!(l.dot(r), 10.0);
/// ```
#[doc(alias = "FVec3_Dot")]
pub fn dot(self, rhs: Self) -> f32 {
unsafe { citro3d_sys::FVec3_Dot(self.0, rhs.0) }
}
/// The magnitude of the vector.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec3;
/// # use approx::assert_abs_diff_eq;
/// let v = FVec3::splat(3.0f32.sqrt());
/// assert_abs_diff_eq!(v.magnitude(), 3.0);
/// ```
#[doc(alias = "FVec3_Magnitude")]
pub fn magnitude(self) -> f32 {
unsafe { citro3d_sys::FVec3_Magnitude(self.0) }
}
/// Normalize the vector to a magnitude of `1.0`.
///
/// # Example
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::FVec3;
/// # use approx::assert_abs_diff_eq;
/// let v = FVec3::splat(1.0);
/// assert_abs_diff_eq!(v.normalize(), FVec3::splat(1.0 / 3.0_f32.sqrt()));
/// ```
#[doc(alias = "FVec3_Normalize")]
pub fn normalize(self) -> Self {
Self(unsafe { citro3d_sys::FVec3_Normalize(self.0) })
}
}
#[cfg(test)]
mod tests {
use approx::assert_abs_diff_eq;
use super::*;
#[test]
fn fvec4() {
let v = FVec4::new(1.0, 2.0, 3.0, 4.0);
let actual = [v.x(), v.y(), v.z(), v.w()];
let expected = [1.0, 2.0, 3.0, 4.0];
assert_abs_diff_eq!(&actual[..], &expected[..]);
}
#[test]
fn fvec3() {
let v = FVec3::new(1.0, 2.0, 3.0);
let actual = [v.x(), v.y(), v.z()];
let expected = [1.0, 2.0, 3.0];
assert_abs_diff_eq!(&actual[..], &expected[..]);
}
}

214
citro3d/src/math/matrix.rs

@ -0,0 +1,214 @@ @@ -0,0 +1,214 @@
use std::mem::MaybeUninit;
pub use private::Matrix;
use super::{CoordinateOrientation, FVec3};
mod private {
use std::fmt;
/// An `M`x`N` row-major matrix of `f32`s.
#[doc(alias = "C3D_Mtx")]
#[derive(Clone)]
pub struct Matrix<const M: usize, const N: usize>(citro3d_sys::C3D_Mtx);
impl<const M: usize, const N: usize> Matrix<M, N> {
const ROW_SIZE: () = assert!(M == 3 || M == 4);
const COLUMN_SIZE: () = assert!(N > 0 && N <= 4);
// This constructor validates, at compile time, that the
// constructed matrix is 3xN or 4xN matrix, where 0 < N ≤ 4.
// We put this struct in a submodule to enforce that nothing creates
// a Matrix without calling this constructor.
#[allow(clippy::let_unit_value)]
pub(crate) fn new(value: citro3d_sys::C3D_Mtx) -> Self {
let () = Self::ROW_SIZE;
let () = Self::COLUMN_SIZE;
Self(value)
}
pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx {
&self.0
}
pub(crate) fn into_raw(self) -> citro3d_sys::C3D_Mtx {
self.0
}
pub(crate) fn as_mut(&mut self) -> *mut citro3d_sys::C3D_Mtx {
&mut self.0
}
/// Trim the matrix down to only the rows and columns we care about,
/// since the inner representation is always 4x4.
///
/// NOTE: this probably shouldn't be used in hot paths since it copies
/// the underlying storage. For some use cases slicing might be better,
/// although the underlying slice would always contain extra values for
/// matrices smaller than 4x4.
pub(crate) fn as_rows(&self) -> [[f32; N]; M] {
let rows = unsafe { self.0.r }.map(|row| -> [f32; N] {
// Rows are stored in WZYX order, so we slice from back to front.
// UNWRAP: N ≤ 4, so slicing to a smaller array should always work
unsafe { row.c[(4 - N)..].try_into() }.unwrap()
});
// UNWRAP: M ≤ 4, so slicing to a smaller array should always work
rows[..M].try_into().unwrap()
}
}
impl<const M: usize, const N: usize> fmt::Debug for Matrix<M, N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let inner = self.as_rows().map(|mut row| {
// Rows are stored in WZYX order which is opposite of how most people
// probably expect, so reverse each row in-place for debug printing
row.reverse();
row
});
let type_name = std::any::type_name::<Self>().split("::").last().unwrap();
f.debug_tuple(type_name).field(&inner).finish()
}
}
}
/// A 3x3 row-major matrix of `f32`s.
pub type Matrix3 = Matrix<3, 3>;
/// A 4x4 row-major matrix of `f32`s.
pub type Matrix4 = Matrix<4, 4>;
impl<const M: usize, const N: usize> Matrix<M, N> {
/// Construct the zero matrix.
#[doc(alias = "Mtx_Zeros")]
pub fn zero() -> Self {
// TODO: should this also be Default::default()?
let mut out = MaybeUninit::uninit();
unsafe {
citro3d_sys::Mtx_Zeros(out.as_mut_ptr());
Self::new(out.assume_init())
}
}
/// Transpose the matrix, swapping rows and columns.
#[doc(alias = "Mtx_Transpose")]
pub fn transpose(mut self) -> Matrix<N, M> {
unsafe {
citro3d_sys::Mtx_Transpose(self.as_mut());
}
Matrix::new(self.into_raw())
}
// region: Matrix transformations
//
// NOTE: the `bRightSide` arg common to many of these APIs flips the order of
// operations so that a transformation occurs as self(T) instead of T(self).
// For now I'm not sure if that's a common use case, but if needed we could
// probably have some kinda wrapper type that does transformations in the
// opposite order, or an enum arg for these APIs or something.
/// Translate a transformation matrix by the given amounts in the X, Y, and Z
/// directions.
pub fn translate(&mut self, x: f32, y: f32, z: f32) {
unsafe { citro3d_sys::Mtx_Translate(self.as_mut(), x, y, z, false) }
}
/// Scale a transformation matrix by the given amounts in the X, Y, and Z directions.
pub fn scale(&mut self, x: f32, y: f32, z: f32) {
unsafe { citro3d_sys::Mtx_Scale(self.as_mut(), x, y, z) }
}
/// Rotate a transformation matrix by the given angle around the given axis.
pub fn rotate(&mut self, axis: FVec3, angle: f32) {
unsafe { citro3d_sys::Mtx_Rotate(self.as_mut(), axis.0, angle, false) }
}
/// Rotate a transformation matrix by the given angle around the X axis.
pub fn rotate_x(&mut self, angle: f32) {
unsafe { citro3d_sys::Mtx_RotateX(self.as_mut(), angle, false) }
}
/// Rotate a transformation matrix by the given angle around the Y axis.
pub fn rotate_y(&mut self, angle: f32) {
unsafe { citro3d_sys::Mtx_RotateY(self.as_mut(), angle, false) }
}
/// Rotate a transformation matrix by the given angle around the Z axis.
pub fn rotate_z(&mut self, angle: f32) {
unsafe { citro3d_sys::Mtx_RotateZ(self.as_mut(), angle, false) }
}
// endregion
}
impl<const N: usize> Matrix<N, N> {
/// Find the inverse of the matrix.
///
/// # Errors
///
/// If the matrix has no inverse, it will be returned unchanged as an [`Err`].
#[doc(alias = "Mtx_Inverse")]
pub fn inverse(mut self) -> Result<Self, Self> {
let determinant = unsafe { citro3d_sys::Mtx_Inverse(self.as_mut()) };
if determinant == 0.0 {
Err(self)
} else {
Ok(self)
}
}
/// Construct the identity matrix.
#[doc(alias = "Mtx_Identity")]
pub fn identity() -> Self {
let mut out = MaybeUninit::uninit();
unsafe {
citro3d_sys::Mtx_Identity(out.as_mut_ptr());
Self::new(out.assume_init())
}
}
}
impl Matrix3 {
/// Construct a 3x3 matrix with the given values on the diagonal.
#[doc(alias = "Mtx_Diagonal")]
pub fn diagonal(x: f32, y: f32, z: f32) -> Self {
let mut out = MaybeUninit::uninit();
unsafe {
citro3d_sys::Mtx_Diagonal(out.as_mut_ptr(), x, y, z, 0.0);
Self::new(out.assume_init())
}
}
}
impl Matrix4 {
/// Construct a 4x4 matrix with the given values on the diagonal.
#[doc(alias = "Mtx_Diagonal")]
pub fn diagonal(x: f32, y: f32, z: f32, w: f32) -> Self {
let mut out = MaybeUninit::uninit();
unsafe {
citro3d_sys::Mtx_Diagonal(out.as_mut_ptr(), x, y, z, w);
Self::new(out.assume_init())
}
}
/// Construct a 3D transformation matrix for a camera, given its position,
/// target, and upward direction.
pub fn looking_at(
camera_position: FVec3,
camera_target: FVec3,
camera_up: FVec3,
coordinates: CoordinateOrientation,
) -> Self {
let mut out = MaybeUninit::uninit();
unsafe {
citro3d_sys::Mtx_LookAt(
out.as_mut_ptr(),
camera_position.0,
camera_target.0,
camera_up.0,
coordinates.is_left_handed(),
);
Self::new(out.assume_init())
}
}
}

285
citro3d/src/math/ops.rs

@ -0,0 +1,285 @@ @@ -0,0 +1,285 @@
use std::borrow::Borrow;
use std::mem::MaybeUninit;
use std::ops::{Add, Deref, Div, Mul, Neg, Sub};
#[cfg(feature = "approx")]
use approx::AbsDiffEq;
use super::{FVec, FVec3, FVec4, Matrix, Matrix3, Matrix4};
// region: FVec4 math operators
impl Add for FVec4 {
type Output = Self;
#[doc(alias = "FVec4_Add")]
fn add(self, rhs: Self) -> Self::Output {
Self(unsafe { citro3d_sys::FVec4_Add(self.0, rhs.0) })
}
}
impl Sub for FVec4 {
type Output = Self;
#[doc(alias = "FVec4_Subtract")]
fn sub(self, rhs: Self) -> Self::Output {
Self(unsafe { citro3d_sys::FVec4_Subtract(self.0, rhs.0) })
}
}
impl Neg for FVec4 {
type Output = Self;
#[doc(alias = "FVec4_Negate")]
fn neg(self) -> Self::Output {
Self(unsafe { citro3d_sys::FVec4_Negate(self.0) })
}
}
impl Mul<f32> for FVec4 {
type Output = Self;
#[doc(alias = "FVec4_Scale")]
fn mul(self, rhs: f32) -> Self::Output {
Self(unsafe { citro3d_sys::FVec4_Scale(self.0, rhs) })
}
}
// endregion
// region: FVec3 math operators
impl Add for FVec3 {
type Output = Self;
#[doc(alias = "FVec3_Add")]
fn add(self, rhs: Self) -> Self::Output {
Self(unsafe { citro3d_sys::FVec3_Add(self.0, rhs.0) })
}
}
impl Sub for FVec3 {
type Output = Self;
#[doc(alias = "FVec3_Subtract")]
fn sub(self, rhs: Self) -> Self::Output {
Self(unsafe { citro3d_sys::FVec3_Subtract(self.0, rhs.0) })
}
}
impl Neg for FVec3 {
type Output = Self;
#[doc(alias = "FVec3_Negate")]
fn neg(self) -> Self::Output {
Self(unsafe { citro3d_sys::FVec3_Negate(self.0) })
}
}
impl Mul<f32> for FVec3 {
type Output = Self;
#[doc(alias = "FVec3_Scale")]
fn mul(self, rhs: f32) -> Self::Output {
Self(unsafe { citro3d_sys::FVec3_Scale(self.0, rhs) })
}
}
// endregion
impl<const N: usize> Div<f32> for FVec<N>
where
FVec<N>: Mul<f32>,
{
type Output = <Self as Mul<f32>>::Output;
fn div(self, rhs: f32) -> Self::Output {
self * (1.0 / rhs)
}
}
impl<const N: usize> PartialEq for FVec<N> {
fn eq(&self, other: &Self) -> bool {
let range = (4 - N)..;
unsafe { self.0.c[range.clone()] == other.0.c[range] }
}
}
impl<const N: usize> Eq for FVec<N> {}
#[cfg(feature = "approx")]
impl<const N: usize> AbsDiffEq for FVec<N> {
type Epsilon = f32;
fn default_epsilon() -> Self::Epsilon {
// See https://docs.rs/almost/latest/almost/#why-another-crate
// for rationale of using this over just EPSILON
f32::EPSILON.sqrt()
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
let range = (4 - N)..;
let (lhs, rhs) = unsafe { (&self.0.c[range.clone()], &other.0.c[range]) };
lhs.abs_diff_eq(rhs, epsilon)
}
}
// region: Matrix math operators
impl<Rhs: Borrow<Self>, const M: usize, const N: usize> Add<Rhs> for &Matrix<M, N> {
type Output = <Self as Deref>::Target;
fn add(self, rhs: Rhs) -> Self::Output {
let mut out = MaybeUninit::uninit();
unsafe {
citro3d_sys::Mtx_Add(out.as_mut_ptr(), self.as_raw(), rhs.borrow().as_raw());
Matrix::new(out.assume_init())
}
}
}
impl<Rhs: Borrow<Self>, const M: usize, const N: usize> Sub<Rhs> for &Matrix<M, N> {
type Output = <Self as Deref>::Target;
fn sub(self, rhs: Rhs) -> Self::Output {
let mut out = MaybeUninit::uninit();
unsafe {
citro3d_sys::Mtx_Subtract(out.as_mut_ptr(), self.as_raw(), rhs.borrow().as_raw());
Matrix::new(out.assume_init())
}
}
}
impl<const M: usize, const N: usize, const P: usize> Mul<&Matrix<N, P>> for &Matrix<M, N> {
type Output = Matrix<M, P>;
fn mul(self, rhs: &Matrix<N, P>) -> Self::Output {
let mut out = MaybeUninit::uninit();
unsafe {
citro3d_sys::Mtx_Multiply(out.as_mut_ptr(), self.as_raw(), rhs.as_raw());
Matrix::new(out.assume_init())
}
}
}
impl<const M: usize, const N: usize, const P: usize> Mul<Matrix<N, P>> for &Matrix<M, N> {
type Output = Matrix<M, P>;
fn mul(self, rhs: Matrix<N, P>) -> Self::Output {
self * &rhs
}
}
impl Mul<FVec3> for &Matrix3 {
type Output = FVec3;
fn mul(self, rhs: FVec3) -> Self::Output {
FVec(unsafe { citro3d_sys::Mtx_MultiplyFVec3(self.as_raw(), rhs.0) })
}
}
impl Mul<FVec4> for &Matrix4 {
type Output = FVec4;
fn mul(self, rhs: FVec4) -> Self::Output {
FVec(unsafe { citro3d_sys::Mtx_MultiplyFVec4(self.as_raw(), rhs.0) })
}
}
impl Mul<FVec3> for &Matrix<4, 3> {
type Output = FVec4;
fn mul(self, rhs: FVec3) -> Self::Output {
FVec(unsafe { citro3d_sys::Mtx_MultiplyFVecH(self.as_raw(), rhs.0) })
}
}
// endregion
impl<Rhs: Borrow<Self>, const M: usize, const N: usize> PartialEq<Rhs> for Matrix<M, N> {
fn eq(&self, other: &Rhs) -> bool {
self.as_rows() == other.borrow().as_rows()
}
}
impl<const M: usize, const N: usize> Eq for Matrix<M, N> {}
#[cfg(feature = "approx")]
#[doc(cfg(feature = "approx"))]
impl<const M: usize, const N: usize> AbsDiffEq for Matrix<M, N> {
type Epsilon = f32;
fn default_epsilon() -> Self::Epsilon {
// See https://docs.rs/almost/latest/almost/#why-another-crate
// for rationale of using this over just EPSILON
f32::EPSILON.sqrt()
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
let lhs = self.as_rows();
let rhs = other.as_rows();
for row in 0..M {
for col in 0..N {
if !lhs[row][col].abs_diff_eq(&rhs[row][col], epsilon) {
return false;
}
}
}
true
}
}
#[cfg(test)]
mod tests {
use approx::assert_abs_diff_eq;
use super::*;
#[test]
fn fvec3() {
let l = FVec3::splat(1.0);
let r = FVec3::splat(2.0);
assert_abs_diff_eq!(l + r, FVec3::splat(3.0));
assert_abs_diff_eq!(l - r, FVec3::splat(-1.0));
assert_abs_diff_eq!(-l, FVec3::splat(-1.0));
assert_abs_diff_eq!(l * 1.5, FVec3::splat(1.5));
assert_abs_diff_eq!(l / 2.0, FVec3::splat(0.5));
}
#[test]
fn fvec4() {
let l = FVec4::splat(1.0);
let r = FVec4::splat(2.0);
assert_abs_diff_eq!(l + r, FVec4::splat(3.0));
assert_abs_diff_eq!(l - r, FVec4::splat(-1.0));
assert_abs_diff_eq!(-l, FVec4::splat(-1.0));
assert_abs_diff_eq!(l * 1.5, FVec4::splat(1.5));
assert_abs_diff_eq!(l / 2.0, FVec4::splat(0.5));
}
#[test]
fn matrix3() {
let l = Matrix3::diagonal(1.0, 2.0, 3.0);
let r = Matrix3::identity();
let (l, r) = (&l, &r);
assert_abs_diff_eq!(&(l * r), l);
assert_abs_diff_eq!(&(l + r), &Matrix3::diagonal(2.0, 3.0, 4.0));
assert_abs_diff_eq!(&(l - r), &Matrix3::diagonal(0.0, 1.0, 2.0));
}
#[test]
fn matrix4() {
let l = Matrix4::diagonal(1.0, 2.0, 3.0, 4.0);
let r = Matrix4::identity();
let (l, r) = (&l, &r);
assert_abs_diff_eq!(&(l * r), l);
assert_abs_diff_eq!(&(l + r), &Matrix4::diagonal(2.0, 3.0, 4.0, 5.0));
assert_abs_diff_eq!(&(l - r), &Matrix4::diagonal(0.0, 1.0, 2.0, 3.0));
}
}

156
citro3d/src/math/projection.rs

@ -1,15 +1,13 @@ @@ -1,15 +1,13 @@
use std::mem::MaybeUninit;
use std::ops::Range;
use super::{
AspectRatio, ClipPlanes, CoordinateOrientation, Matrix, ScreenOrientation, StereoDisplacement,
};
use super::Matrix4;
/// Configuration for a 3D [projection](https://en.wikipedia.org/wiki/3D_projection).
/// See specific `Kind` implementations for constructors, e.g.
/// [`Projection::perspective`] and [`Projection::orthographic`].
///
/// To use the resulting projection, convert it to a [`Matrix`] with [`From`]/[`Into`].
/// To use the resulting projection, convert it to a [`Matrix`](super::Matrix) with [`From`]/[`Into`].
#[derive(Clone, Debug)]
pub struct Projection<Kind> {
coordinates: CoordinateOrientation,
@ -75,10 +73,11 @@ impl Projection<Perspective> { @@ -75,10 +73,11 @@ impl Projection<Perspective> {
/// far: 100.0,
/// };
///
/// let bottom: Matrix =
/// let bottom: Matrix4 =
/// Projection::perspective(PI / 4.0, AspectRatio::BottomScreen, clip_planes).into();
///
/// let top: Matrix = Projection::perspective(PI / 4.0, AspectRatio::TopScreen, clip_planes).into();
/// let top: Matrix4 =
/// Projection::perspective(PI / 4.0, AspectRatio::TopScreen, clip_planes).into();
/// ```
#[doc(alias = "Mtx_Persp")]
#[doc(alias = "Mtx_PerspTilt")]
@ -122,7 +121,7 @@ impl Projection<Perspective> { @@ -122,7 +121,7 @@ impl Projection<Perspective> {
self,
left_eye: StereoDisplacement,
right_eye: StereoDisplacement,
) -> (Matrix, Matrix) {
) -> (Matrix4, Matrix4) {
// TODO: we might be able to avoid this clone if there was a conversion
// from &Self to Matrix instead of Self... but it's probably fine for now
let left = self.clone().stereo(left_eye);
@ -137,7 +136,7 @@ impl Projection<Perspective> { @@ -137,7 +136,7 @@ impl Projection<Perspective> {
}
}
impl From<Projection<Perspective>> for Matrix {
impl From<Projection<Perspective>> for Matrix4 {
fn from(projection: Projection<Perspective>) -> Self {
let Perspective {
vertical_fov_radians,
@ -182,7 +181,7 @@ impl From<Projection<Perspective>> for Matrix { @@ -182,7 +181,7 @@ impl From<Projection<Perspective>> for Matrix {
}
}
unsafe { Self(result.assume_init()) }
unsafe { Self::new(result.assume_init()) }
}
}
@ -206,9 +205,9 @@ impl Projection<Orthographic> { @@ -206,9 +205,9 @@ impl Projection<Orthographic> {
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::{Projection, ClipPlanes, Matrix};
/// # use citro3d::math::{Projection, ClipPlanes, Matrix4};
/// #
/// let mtx: Matrix = Projection::orthographic(
/// let mtx: Matrix4 = Projection::orthographic(
/// 0.0..240.0,
/// 0.0..400.0,
/// ClipPlanes {
@ -233,7 +232,7 @@ impl Projection<Orthographic> { @@ -233,7 +232,7 @@ impl Projection<Orthographic> {
}
}
impl From<Projection<Orthographic>> for Matrix {
impl From<Projection<Orthographic>> for Matrix4 {
fn from(projection: Projection<Orthographic>) -> Self {
let make_mtx = match projection.rotation {
ScreenOrientation::Rotated => citro3d_sys::Mtx_OrthoTilt,
@ -258,7 +257,138 @@ impl From<Projection<Orthographic>> for Matrix { @@ -258,7 +257,138 @@ impl From<Projection<Orthographic>> for Matrix {
clip_planes_z.far,
projection.coordinates.is_left_handed(),
);
Self(out.assume_init())
Self::new(out.assume_init())
}
}
}
// region: Projection configuration
/// The [orientation](https://en.wikipedia.org/wiki/Orientation_(geometry))
/// (or "handedness") of the coordinate system. Coordinates are always +Y-up,
/// +X-right.
#[derive(Clone, Copy, Debug)]
pub enum CoordinateOrientation {
/// A left-handed coordinate system. +Z points into the screen.
LeftHanded,
/// A right-handed coordinate system. +Z points out of the screen.
RightHanded,
}
impl CoordinateOrientation {
pub(crate) fn is_left_handed(self) -> bool {
matches!(self, Self::LeftHanded)
}
}
impl Default for CoordinateOrientation {
/// This is an opinionated default, but [`RightHanded`](Self::RightHanded)
/// seems to be the preferred coordinate system for most
/// [examples](https://github.com/devkitPro/3ds-examples)
/// from upstream, and is also fairly common in other applications.
fn default() -> Self {
Self::RightHanded
}
}
/// Whether to rotate a projection to account for the 3DS screen orientation.
/// Both screens on the 3DS are oriented such that the "top-left" of the screen
/// in framebuffer coordinates is the physical bottom-left of the screen
/// (i.e. the "width" is smaller than the "height").
#[derive(Clone, Copy, Debug)]
pub enum ScreenOrientation {
/// Rotate 90° clockwise to account for the 3DS screen rotation. Most
/// applications will use this variant.
Rotated,
/// Do not apply any extra rotation to the projection.
None,
}
impl Default for ScreenOrientation {
fn default() -> Self {
Self::Rotated
}
}
/// Configuration for calculating stereoscopic projections.
// TODO: not totally happy with this name + API yet, but it works for now.
#[derive(Clone, Copy, Debug)]
pub struct StereoDisplacement {
/// The horizontal offset of the eye from center. Negative values
/// correspond to the left eye, and positive values to the right eye.
pub displacement: f32,
/// The position of the screen, which determines the focal length. Objects
/// closer than this depth will appear to pop out of the screen, and objects
/// further than this will appear inside the screen.
pub screen_depth: f32,
}
impl StereoDisplacement {
/// Construct displacement for the left and right eyes simulataneously.
/// The given `interocular_distance` describes the distance between the two
/// rendered "eyes". A negative value will be treated the same as a positive
/// value of the same magnitude.
///
/// See struct documentation for details about the
/// [`screen_depth`](Self::screen_depth) parameter.
pub fn new(interocular_distance: f32, screen_depth: f32) -> (Self, Self) {
let displacement = interocular_distance.abs() / 2.0;
let left_eye = Self {
displacement: -displacement,
screen_depth,
};
let right_eye = Self {
displacement,
screen_depth,
};
(left_eye, right_eye)
}
}
/// Configuration for the clipping planes of a projection.
///
/// For [`Perspective`] projections, this is used for the near and far clip planes
/// of the [view frustum](https://en.wikipedia.org/wiki/Viewing_frustum).
///
/// For [`Orthographic`] projections, this is used for the Z clipping planes of
/// the projection.
///
/// Note that the `near` value should always be less than `far`, regardless of
/// [`CoordinateOrientation`]. In other words, these values will be negated
/// when used with a [`RightHanded`](CoordinateOrientation::RightHanded)
/// orientation.
#[derive(Clone, Copy, Debug)]
pub struct ClipPlanes {
/// The Z-depth of the near clip plane, usually close or equal to zero.
pub near: f32,
/// The Z-depth of the far clip plane, usually greater than zero.
pub far: f32,
}
/// The aspect ratio of a projection plane.
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum AspectRatio {
/// The aspect ratio of the 3DS' top screen (per-eye).
#[doc(alias = "C3D_AspectRatioTop")]
TopScreen,
/// The aspect ratio of the 3DS' bottom screen.
#[doc(alias = "C3D_AspectRatioBot")]
BottomScreen,
/// A custom aspect ratio (should be calcualted as `width / height`).
Other(f32),
}
impl From<AspectRatio> for f32 {
fn from(ratio: AspectRatio) -> Self {
match ratio {
AspectRatio::TopScreen => citro3d_sys::C3D_AspectRatioTop as f32,
AspectRatio::BottomScreen => citro3d_sys::C3D_AspectRatioBot as f32,
AspectRatio::Other(ratio) => ratio,
}
}
}
// endregion

21
citro3d/src/uniform.rs

@ -24,7 +24,8 @@ mod private { @@ -24,7 +24,8 @@ mod private {
use crate::math::Matrix;
pub trait Sealed {}
impl Sealed for &Matrix {}
impl<const M: usize, const N: usize> Sealed for &Matrix<M, N> {}
}
/// A shader uniform. This trait is implemented for types that can be bound to
@ -36,9 +37,21 @@ pub trait Uniform: private::Sealed { @@ -36,9 +37,21 @@ pub trait Uniform: private::Sealed {
fn bind(self, instance: &mut Instance, shader_type: shader::Type, index: Index);
}
impl Uniform for &Matrix {
#[doc(alias = "C3D_FVUnifMtx4x4")]
impl<const M: usize> Uniform for &Matrix<M, 4> {
#[doc(alias = "C34_FVUnifMtxNx4")]
#[doc(alias = "C34_FVUnifMtx4x4")]
#[doc(alias = "C34_FVUnifMtx3x4")]
#[doc(alias = "C34_FVUnifMtx2x4")]
fn bind(self, _instance: &mut Instance, type_: shader::Type, index: Index) {
unsafe { citro3d_sys::C3D_FVUnifMtx4x4(type_.into(), index.into(), self.as_raw()) }
unsafe {
citro3d_sys::C3D_FVUnifMtxNx4(
type_.into(),
index.into(),
self.as_raw(),
// UNWRAP: it should be impossible for end users to construct
// a matrix with M > i32::MAX
M.try_into().unwrap(),
);
}
}
}

Loading…
Cancel
Save