Browse Source

Major refactor of projection API

Finally, something that I don't think totally sucks. Pick some sane
defaults for some fields, and use generics to split between
orthographic and perspective implementations. It's still not perfect but
I think this looks a lot more ergonomic to use vs what I had before (and
compared to the C APIs).
pull/27/head
Ian Chamberlain 1 year ago
parent
commit
65941f671c
No known key found for this signature in database
GPG Key ID: AE5484D09405AA60
  1. 35
      citro3d/examples/triangle.rs
  2. 7
      citro3d/src/lib.rs
  3. 241
      citro3d/src/math.rs
  4. 264
      citro3d/src/math/projection.rs
  5. 5
      citro3d/src/uniform.rs
  6. 3
      rustfmt.toml

35
citro3d/examples/triangle.rs

@ -4,9 +4,7 @@
#![feature(allocator_api)] #![feature(allocator_api)]
use citro3d::macros::include_shader; use citro3d::macros::include_shader;
use citro3d::math::{ use citro3d::math::{AspectRatio, ClipPlanes, Matrix, Projection, StereoDisplacement};
AspectRatio, ClipPlanes, CoordinateOrientation, Matrix, ScreenOrientation, StereoDisplacement,
};
use citro3d::render::ClearFlags; use citro3d::render::ClearFlags;
use citro3d::{attrib, buffer, render, shader}; use citro3d::{attrib, buffer, render, shader};
use ctru::prelude::*; use ctru::prelude::*;
@ -119,13 +117,13 @@ fn main() {
}; };
let Projections { let Projections {
left_eye: left, left_eye,
right_eye: right, right_eye,
center, center,
} = calculate_projections(); } = calculate_projections();
render_to(&mut top_left_target, &left); render_to(&mut top_left_target, &left_eye);
render_to(&mut top_right_target, &right); render_to(&mut top_right_target, &right_eye);
render_to(&mut bottom_target, &center); render_to(&mut bottom_target, &center);
}); });
} }
@ -179,25 +177,14 @@ fn calculate_projections() -> Projections {
far: 100.0, far: 100.0,
}; };
let stereo = StereoDisplacement::new(interocular_distance, screen_depth); let (left, right) = StereoDisplacement::new(interocular_distance, screen_depth);
let (left_eye, right_eye) = Matrix::stereo_projections( let (left_eye, right_eye) =
vertical_fov, Projection::perspective(vertical_fov, AspectRatio::TopScreen, clip_planes)
AspectRatio::TopScreen, .stereo_matrices(left, right);
ScreenOrientation::Rotated,
clip_planes,
CoordinateOrientation::LeftHanded,
stereo,
);
let center = Matrix::perspective_projection( let center =
vertical_fov, Projection::perspective(vertical_fov, AspectRatio::BottomScreen, clip_planes).into();
AspectRatio::BottomScreen,
ScreenOrientation::Rotated,
clip_planes,
CoordinateOrientation::LeftHanded,
None,
);
Projections { Projections {
left_eye, left_eye,

7
citro3d/src/lib.rs

@ -11,7 +11,6 @@ pub mod shader;
pub mod uniform; pub mod uniform;
pub use error::{Error, Result}; pub use error::{Error, Result};
pub use math::Matrix;
use self::uniform::Uniform; use self::uniform::Uniform;
@ -132,7 +131,8 @@ impl Instance {
/// ///
/// ``` /// ```
/// # let _runner = test_runner::GdbRunner::default(); /// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::{uniform, Matrix}; /// # use citro3d::uniform;
/// # use citro3d::math::Matrix;
/// # /// #
/// # let mut instance = citro3d::Instance::new().unwrap(); /// # let mut instance = citro3d::Instance::new().unwrap();
/// let idx = uniform::Index::from(0); /// let idx = uniform::Index::from(0);
@ -149,7 +149,8 @@ impl Instance {
/// ///
/// ``` /// ```
/// # let _runner = test_runner::GdbRunner::default(); /// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::{uniform, Matrix}; /// # use citro3d::uniform;
/// # use citro3d::math::Matrix;
/// # /// #
/// # let mut instance = citro3d::Instance::new().unwrap(); /// # let mut instance = citro3d::Instance::new().unwrap();
/// let idx = uniform::Index::from(0); /// let idx = uniform::Index::from(0);

241
citro3d/src/math.rs

@ -2,6 +2,10 @@
use std::mem::MaybeUninit; use std::mem::MaybeUninit;
mod projection;
pub use projection::{Orthographic, Perspective, Projection};
/// A 4-vector of `u8`s. /// A 4-vector of `u8`s.
#[doc(alias = "C3D_IVec")] #[doc(alias = "C3D_IVec")]
pub struct IVec(citro3d_sys::C3D_IVec); pub struct IVec(citro3d_sys::C3D_IVec);
@ -19,160 +23,25 @@ pub struct FQuat(citro3d_sys::C3D_FQuat);
pub struct Matrix(citro3d_sys::C3D_Mtx); pub struct Matrix(citro3d_sys::C3D_Mtx);
impl Matrix { impl Matrix {
/// Construct the identity matrix. /// Construct the zero matrix.
#[doc(alias = "Mtx_Identity")] #[doc(alias = "Mtx_Zeros")]
pub fn identity() -> Self { pub fn zero() -> Self {
// TODO: should this also be Default::default()?
let mut out = MaybeUninit::uninit(); let mut out = MaybeUninit::uninit();
unsafe { unsafe {
citro3d_sys::Mtx_Identity(out.as_mut_ptr()); citro3d_sys::Mtx_Zeros(out.as_mut_ptr());
Self(out.assume_init()) Self(out.assume_init())
} }
} }
/// Construct a projection matrix suitable for projecting 3D world space onto /// Construct the identity matrix.
/// the 3DS screens. #[doc(alias = "Mtx_Identity")]
/// pub fn identity() -> Self {
/// # Parameters let mut out = MaybeUninit::uninit();
///
/// * `vertical_fov_radians`: the vertical field of view, measured in radians
/// * `aspect_ratio`: The aspect ratio of the projection
/// * `orientation`: the orientation of the projection with respect to the screen
/// * `coordinates`: the handedness of the coordinate system to use
/// * `stereo`: if specified, the offset to displace the projection by
/// for stereoscopic rendering.
///
/// # Examples
///
/// ```
/// # use citro3d::math::*;
/// # use std::f32::consts::PI;
/// #
/// # let _runner = test_runner::GdbRunner::default();
///
/// let clip_planes = ClipPlanes {
/// near: 0.01,
/// far: 100.0,
/// };
///
/// let center = Matrix::perspective_projection(
/// PI / 4.0,
/// AspectRatio::BottomScreen,
/// ScreenOrientation::Rotated,
/// clip_planes,
/// CoordinateOrientation::LeftHanded,
/// None,
/// );
///
/// let right_eye = Matrix::perspective_projection(
/// PI / 4.0,
/// AspectRatio::BottomScreen,
/// ScreenOrientation::Rotated,
/// clip_planes,
/// CoordinateOrientation::LeftHanded,
/// Some(StereoDisplacement {
/// displacement: 1.0,
/// screen_depth: 2.0,
/// }),
/// );
/// ```
#[doc(alias = "Mtx_Persp")]
#[doc(alias = "Mtx_PerspTilt")]
pub fn perspective_projection(
vertical_fov_radians: f32,
aspect_ratio: AspectRatio,
orientation: ScreenOrientation,
clip_plane: ClipPlanes,
coordinates: CoordinateOrientation,
stereo: Option<StereoDisplacement>,
) -> Self {
let mut result = MaybeUninit::uninit();
let left_handed = matches!(coordinates, CoordinateOrientation::LeftHanded);
if let Some(stereo) = stereo {
let initialize_mtx = orientation.perpsective_stereo_builder();
unsafe {
initialize_mtx(
result.as_mut_ptr(),
vertical_fov_radians,
aspect_ratio.into(),
clip_plane.near,
clip_plane.far,
stereo.displacement,
stereo.screen_depth,
left_handed,
);
}
} else {
let initialize_mtx = orientation.perspective_mono_builder();
unsafe { unsafe {
initialize_mtx( citro3d_sys::Mtx_Identity(out.as_mut_ptr());
result.as_mut_ptr(), Self(out.assume_init())
vertical_fov_radians,
aspect_ratio.into(),
clip_plane.near,
clip_plane.far,
left_handed,
);
}
}
let inner = unsafe { result.assume_init() };
Self(inner)
} }
/// Helper function to build both eyes' perspective projection matrices
/// at once. See [`perspective_projection`] for a description of each
/// parameter.
///
/// ```
/// # use std::f32::consts::PI;
/// # use citro3d::math::*;
/// #
/// # let _runner = test_runner::GdbRunner::default();
///
/// let (left_eye, right_eye) = Matrix::stereo_projections(
/// PI / 4.0,
/// AspectRatio::TopScreen,
/// ScreenOrientation::Rotated,
/// ClipPlanes {
/// near: 0.01,
/// far: 100.0,
/// },
/// CoordinateOrientation::LeftHanded,
/// StereoDisplacement::new(0.5, 2.0),
/// );
/// ```
///
/// [`perspective_projection`]: Self::perspective_projection
#[doc(alias = "Mtx_PerspStereo")]
#[doc(alias = "Mtx_PerspStereoTilt")]
pub fn stereo_projections(
vertical_fov_radians: f32,
aspect_ratio: AspectRatio,
orientation: ScreenOrientation,
clip_plane: ClipPlanes,
coordinates: CoordinateOrientation,
(left_eye, right_eye): (StereoDisplacement, StereoDisplacement),
) -> (Self, Self) {
let left = Self::perspective_projection(
vertical_fov_radians,
aspect_ratio,
orientation,
clip_plane,
coordinates,
Some(left_eye),
);
let right = Self::perspective_projection(
vertical_fov_radians,
aspect_ratio,
orientation,
clip_plane,
coordinates,
Some(right_eye),
);
(left, right)
} }
pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx { pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx {
@ -180,16 +49,39 @@ impl Matrix {
} }
} }
// 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)) /// The [orientation](https://en.wikipedia.org/wiki/Orientation_(geometry))
/// (or "handedness") of the coordinate system. /// (or "handedness") of the coordinate system. Coordinates are always +Y-up,
/// +X-right.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum CoordinateOrientation { pub enum CoordinateOrientation {
/// A left-handed coordinate system. /// A left-handed coordinate system. +Z points into the screen.
LeftHanded, LeftHanded,
/// A right-handed coordinate system. /// A right-handed coordinate system. +Z points out of the screen.
RightHanded, 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. /// 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 /// 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 /// in framebuffer coordinates is the physical bottom-left of the screen
@ -203,37 +95,14 @@ pub enum ScreenOrientation {
None, None,
} }
impl ScreenOrientation { impl Default for ScreenOrientation {
fn perspective_mono_builder( fn default() -> Self {
self, Self::Rotated
) -> unsafe extern "C" fn(*mut citro3d_sys::C3D_Mtx, f32, f32, f32, f32, bool) {
match self {
Self::Rotated => citro3d_sys::Mtx_PerspTilt,
Self::None => citro3d_sys::Mtx_Persp,
}
}
fn perpsective_stereo_builder(
self,
) -> unsafe extern "C" fn(*mut citro3d_sys::C3D_Mtx, f32, f32, f32, f32, f32, f32, bool) {
match self {
Self::Rotated => citro3d_sys::Mtx_PerspStereoTilt,
Self::None => citro3d_sys::Mtx_PerspStereo,
}
}
// TODO: orthographic projections
fn ortho_builder(
self,
) -> unsafe extern "C" fn(*mut citro3d_sys::C3D_Mtx, f32, f32, f32, f32, f32, f32, bool) {
match self {
Self::Rotated => citro3d_sys::Mtx_OrthoTilt,
Self::None => citro3d_sys::Mtx_Ortho,
}
} }
} }
/// Configuration for calculating stereoscopic projections. /// Configuration for calculating stereoscopic projections.
// TODO: not totally happy with this name + API yet, but it works for now.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct StereoDisplacement { pub struct StereoDisplacement {
/// The horizontal offset of the eye from center. Negative values /// The horizontal offset of the eye from center. Negative values
@ -269,13 +138,23 @@ impl StereoDisplacement {
} }
} }
/// Configuration for the [frustum](https://en.wikipedia.org/wiki/Viewing_frustum) /// Configuration for the clipping planes of a projection.
/// of a perspective 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)] #[derive(Clone, Copy, Debug)]
pub struct ClipPlanes { pub struct ClipPlanes {
/// The z-depth of the near clip plane. /// The Z-depth of the near clip plane, usually close or equal to zero.
pub near: f32, pub near: f32,
/// The z-depth of the far clip plane. /// The Z-depth of the far clip plane, usually greater than zero.
pub far: f32, pub far: f32,
} }
@ -302,3 +181,5 @@ impl From<AspectRatio> for f32 {
} }
} }
} }
// endregion

264
citro3d/src/math/projection.rs

@ -0,0 +1,264 @@
use std::mem::MaybeUninit;
use std::ops::Range;
use super::{
AspectRatio, ClipPlanes, CoordinateOrientation, Matrix, ScreenOrientation, StereoDisplacement,
};
/// 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`].
#[derive(Clone, Debug)]
pub struct Projection<Kind> {
coordinates: CoordinateOrientation,
rotation: ScreenOrientation,
inner: Kind,
}
impl<Kind> Projection<Kind> {
fn new(inner: Kind) -> Self {
Self {
coordinates: CoordinateOrientation::default(),
rotation: ScreenOrientation::default(),
inner,
}
}
/// Set the coordinate system's orientation for the projection.
/// See [`CoordinateOrientation`] for more details.
pub fn coordinates(&mut self, orientation: CoordinateOrientation) -> &mut Self {
self.coordinates = orientation;
self
}
/// Set the screen rotation for the projection.
/// See [`ScreenOrientation`] for more details.
pub fn screen(&mut self, orientation: ScreenOrientation) -> &mut Self {
self.rotation = orientation;
self
}
}
/// See [`Projection::perspective`].
#[derive(Clone, Debug)]
pub struct Perspective {
vertical_fov_radians: f32,
aspect_ratio: AspectRatio,
clip_planes: ClipPlanes,
stereo: Option<StereoDisplacement>,
}
impl Projection<Perspective> {
/// Construct a projection matrix suitable for projecting 3D world space onto
/// the 3DS screens.
///
/// # Parameters
///
/// * `vertical_fov`: the vertical field of view, measured in radians
/// * `aspect_ratio`: the aspect ratio of the projection
/// * `clip_planes`: the near and far clip planes of the view frustum.
/// [`ClipPlanes`] are always defined by near and far values, regardless
/// of the projection's [`CoordinateOrientation`].
///
/// # Examples
///
/// ```
/// # use citro3d::math::*;
/// # use std::f32::consts::PI;
/// #
/// # let _runner = test_runner::GdbRunner::default();
/// #
/// let clip_planes = ClipPlanes {
/// near: 0.01,
/// far: 100.0,
/// };
///
/// let bottom: Matrix =
/// Projection::perspective(PI / 4.0, AspectRatio::BottomScreen, clip_planes).into();
///
/// let top: Matrix = Projection::perspective(PI / 4.0, AspectRatio::TopScreen, clip_planes).into();
/// ```
#[doc(alias = "Mtx_Persp")]
#[doc(alias = "Mtx_PerspTilt")]
pub fn perspective(
vertical_fov_radians: f32,
aspect_ratio: AspectRatio,
clip_planes: ClipPlanes,
) -> Self {
Self::new(Perspective {
vertical_fov_radians,
aspect_ratio,
clip_planes,
stereo: None,
})
}
/// Helper function to build both eyes' perspective projection matrices
/// at once. See [`StereoDisplacement`] for details on how to configure
/// stereoscopy.
///
/// ```
/// # use std::f32::consts::PI;
/// # use citro3d::math::*;
/// #
/// # let _runner = test_runner::GdbRunner::default();
/// #
/// let (left, right) = StereoDisplacement::new(0.5, 2.0);
/// let (left_eye, right_eye) = Projection::perspective(
/// PI / 4.0,
/// AspectRatio::TopScreen,
/// ClipPlanes {
/// near: 0.01,
/// far: 100.0,
/// },
/// )
/// .stereo_matrices(left, right);
/// ```
#[doc(alias = "Mtx_PerspStereo")]
#[doc(alias = "Mtx_PerspStereoTilt")]
pub fn stereo_matrices(
self,
left_eye: StereoDisplacement,
right_eye: StereoDisplacement,
) -> (Matrix, Matrix) {
// 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);
let right = self.stereo(right_eye);
// Also, we could consider just returning (Self, Self) here? idk
(left.into(), right.into())
}
fn stereo(mut self, displacement: StereoDisplacement) -> Self {
self.inner.stereo = Some(displacement);
self
}
}
impl From<Projection<Perspective>> for Matrix {
fn from(projection: Projection<Perspective>) -> Self {
let Perspective {
vertical_fov_radians,
aspect_ratio,
clip_planes,
stereo,
} = projection.inner;
let mut result = MaybeUninit::uninit();
if let Some(stereo) = stereo {
let make_mtx = match projection.rotation {
ScreenOrientation::Rotated => citro3d_sys::Mtx_PerspStereoTilt,
ScreenOrientation::None => citro3d_sys::Mtx_PerspStereo,
};
unsafe {
make_mtx(
result.as_mut_ptr(),
vertical_fov_radians,
aspect_ratio.into(),
clip_planes.near,
clip_planes.far,
stereo.displacement,
stereo.screen_depth,
projection.coordinates.is_left_handed(),
);
}
} else {
let make_mtx = match projection.rotation {
ScreenOrientation::Rotated => citro3d_sys::Mtx_PerspTilt,
ScreenOrientation::None => citro3d_sys::Mtx_Persp,
};
unsafe {
make_mtx(
result.as_mut_ptr(),
vertical_fov_radians,
aspect_ratio.into(),
clip_planes.near,
clip_planes.far,
projection.coordinates.is_left_handed(),
);
}
}
unsafe { Self(result.assume_init()) }
}
}
/// See [`Projection::orthographic`].
#[derive(Clone, Debug)]
pub struct Orthographic {
clip_planes_x: Range<f32>,
clip_planes_y: Range<f32>,
clip_planes_z: ClipPlanes,
}
impl Projection<Orthographic> {
/// Construct an orthographic projection. The X and Y clip planes are passed
/// as ranges because their coordinates are always oriented the same way
/// (+X right, +Y up).
///
/// The Z [`ClipPlanes`], however, are always defined by
/// near and far values, regardless of the projection's [`CoordinateOrientation`].
///
/// # Example
///
/// ```
/// # let _runner = test_runner::GdbRunner::default();
/// # use citro3d::math::{Projection, ClipPlanes, Matrix};
/// #
/// let mtx: Matrix = Projection::orthographic(
/// 0.0..240.0,
/// 0.0..400.0,
/// ClipPlanes {
/// near: 0.0,
/// far: 100.0,
/// },
/// )
/// .into();
/// ```
#[doc(alias = "Mtx_Ortho")]
#[doc(alias = "Mtx_OrthoTilt")]
pub fn orthographic(
clip_planes_x: Range<f32>,
clip_planes_y: Range<f32>,
clip_planes_z: ClipPlanes,
) -> Self {
Self::new(Orthographic {
clip_planes_x,
clip_planes_y,
clip_planes_z,
})
}
}
impl From<Projection<Orthographic>> for Matrix {
fn from(projection: Projection<Orthographic>) -> Self {
let make_mtx = match projection.rotation {
ScreenOrientation::Rotated => citro3d_sys::Mtx_OrthoTilt,
ScreenOrientation::None => citro3d_sys::Mtx_Ortho,
};
let Orthographic {
clip_planes_x,
clip_planes_y,
clip_planes_z,
} = projection.inner;
let mut out = MaybeUninit::uninit();
unsafe {
make_mtx(
out.as_mut_ptr(),
clip_planes_x.start,
clip_planes_x.end,
clip_planes_y.start,
clip_planes_y.end,
clip_planes_z.near,
clip_planes_z.far,
projection.coordinates.is_left_handed(),
);
Self(out.assume_init())
}
}
}

5
citro3d/src/uniform.rs

@ -1,7 +1,8 @@
//! Common definitions for binding uniforms to shaders. This is primarily //! Common definitions for binding uniforms to shaders. This is primarily
//! done by implementing the [`Uniform`] trait for a given type. //! done by implementing the [`Uniform`] trait for a given type.
use crate::{shader, Instance, Matrix}; use crate::math::Matrix;
use crate::{shader, Instance};
/// The index of a uniform within a [`shader::Program`]. /// The index of a uniform within a [`shader::Program`].
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
@ -20,7 +21,7 @@ impl From<Index> for i32 {
} }
mod private { mod private {
use crate::Matrix; use crate::math::Matrix;
pub trait Sealed {} pub trait Sealed {}
impl Sealed for &Matrix {} impl Sealed for &Matrix {}

3
rustfmt.toml

@ -0,0 +1,3 @@
unstable_features = true
format_code_in_doc_comments = true
group_imports = "StdExternalCrate"
Loading…
Cancel
Save