diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index c631867..b367906 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -4,9 +4,7 @@ #![feature(allocator_api)] use citro3d::macros::include_shader; -use citro3d::math::{ - AspectRatio, ClipPlanes, CoordinateOrientation, Matrix, ScreenOrientation, StereoDisplacement, -}; +use citro3d::math::{AspectRatio, ClipPlanes, Matrix, Projection, StereoDisplacement}; use citro3d::render::ClearFlags; use citro3d::{attrib, buffer, render, shader}; use ctru::prelude::*; @@ -119,13 +117,13 @@ fn main() { }; let Projections { - left_eye: left, - right_eye: right, + left_eye, + right_eye, center, } = calculate_projections(); - render_to(&mut top_left_target, &left); - render_to(&mut top_right_target, &right); + render_to(&mut top_left_target, &left_eye); + render_to(&mut top_right_target, &right_eye); render_to(&mut bottom_target, ¢er); }); } @@ -179,25 +177,14 @@ fn calculate_projections() -> Projections { far: 100.0, }; - let stereo = StereoDisplacement::new(interocular_distance, screen_depth); - - let (left_eye, right_eye) = Matrix::stereo_projections( - vertical_fov, - AspectRatio::TopScreen, - ScreenOrientation::Rotated, - clip_planes, - CoordinateOrientation::LeftHanded, - stereo, - ); - - let center = Matrix::perspective_projection( - vertical_fov, - AspectRatio::BottomScreen, - ScreenOrientation::Rotated, - clip_planes, - CoordinateOrientation::LeftHanded, - None, - ); + let (left, right) = StereoDisplacement::new(interocular_distance, screen_depth); + + let (left_eye, right_eye) = + Projection::perspective(vertical_fov, AspectRatio::TopScreen, clip_planes) + .stereo_matrices(left, right); + + let center = + Projection::perspective(vertical_fov, AspectRatio::BottomScreen, clip_planes).into(); Projections { left_eye, diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index ab10646..0ba340d 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -11,7 +11,6 @@ pub mod shader; pub mod uniform; pub use error::{Error, Result}; -pub use math::Matrix; use self::uniform::Uniform; @@ -132,7 +131,8 @@ impl Instance { /// /// ``` /// # 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 idx = uniform::Index::from(0); @@ -149,7 +149,8 @@ impl Instance { /// /// ``` /// # 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 idx = uniform::Index::from(0); diff --git a/citro3d/src/math.rs b/citro3d/src/math.rs index cf67cad..479ef71 100644 --- a/citro3d/src/math.rs +++ b/citro3d/src/math.rs @@ -2,6 +2,10 @@ use std::mem::MaybeUninit; +mod projection; + +pub use projection::{Orthographic, Perspective, Projection}; + /// A 4-vector of `u8`s. #[doc(alias = "C3D_IVec")] pub struct IVec(citro3d_sys::C3D_IVec); @@ -19,6 +23,17 @@ pub struct FQuat(citro3d_sys::C3D_FQuat); 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 { @@ -29,167 +44,44 @@ impl Matrix { } } - /// Construct a projection matrix suitable for projecting 3D world space onto - /// the 3DS screens. - /// - /// # Parameters - /// - /// * `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, - ) -> 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 { - initialize_mtx( - result.as_mut_ptr(), - 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 { &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. +/// (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. + /// A left-handed coordinate system. +Z points into the screen. LeftHanded, - /// A right-handed coordinate system. + /// 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 @@ -203,37 +95,14 @@ pub enum ScreenOrientation { None, } -impl ScreenOrientation { - fn perspective_mono_builder( - self, - ) -> 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, - } +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 @@ -269,13 +138,23 @@ impl StereoDisplacement { } } -/// Configuration for the [frustum](https://en.wikipedia.org/wiki/Viewing_frustum) -/// of a perspective projection. +/// 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. + /// 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. + /// The Z-depth of the far clip plane, usually greater than zero. pub far: f32, } @@ -302,3 +181,5 @@ impl From for f32 { } } } + +// endregion diff --git a/citro3d/src/math/projection.rs b/citro3d/src/math/projection.rs new file mode 100644 index 0000000..6833fb9 --- /dev/null +++ b/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 { + coordinates: CoordinateOrientation, + rotation: ScreenOrientation, + inner: Kind, +} + +impl Projection { + 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, +} + +impl Projection { + /// 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> for Matrix { + fn from(projection: Projection) -> 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, + clip_planes_y: Range, + clip_planes_z: ClipPlanes, +} + +impl Projection { + /// 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, + clip_planes_y: Range, + clip_planes_z: ClipPlanes, + ) -> Self { + Self::new(Orthographic { + clip_planes_x, + clip_planes_y, + clip_planes_z, + }) + } +} + +impl From> for Matrix { + fn from(projection: Projection) -> 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()) + } + } +} diff --git a/citro3d/src/uniform.rs b/citro3d/src/uniform.rs index c4fbf8b..c6a1d75 100644 --- a/citro3d/src/uniform.rs +++ b/citro3d/src/uniform.rs @@ -1,7 +1,8 @@ //! Common definitions for binding uniforms to shaders. This is primarily //! 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`]. #[derive(Copy, Clone, Debug)] @@ -20,7 +21,7 @@ impl From for i32 { } mod private { - use crate::Matrix; + use crate::math::Matrix; pub trait Sealed {} impl Sealed for &Matrix {} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..e8c7ca3 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +unstable_features = true +format_code_in_doc_comments = true +group_imports = "StdExternalCrate"