diff --git a/citro3d-macros/src/lib.rs b/citro3d-macros/src/lib.rs index 3d77b3f..4e52c88 100644 --- a/citro3d-macros/src/lib.rs +++ b/citro3d-macros/src/lib.rs @@ -1,3 +1,5 @@ +//! Procedural macro helpers for `citro3d`. + // we're already nightly-only so might as well use unstable proc macro APIs. #![feature(proc_macro_span)] diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index 4009f5b..c631867 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -4,9 +4,11 @@ #![feature(allocator_api)] use citro3d::macros::include_shader; -use citro3d::math::{ClipPlane, CoordinateSystem, Matrix, Orientation, Stereoscopic}; -use citro3d::render::{self, ClearFlags}; -use citro3d::{attrib, buffer, shader, AspectRatio}; +use citro3d::math::{ + AspectRatio, ClipPlanes, CoordinateOrientation, Matrix, ScreenOrientation, StereoDisplacement, +}; +use citro3d::render::ClearFlags; +use citro3d::{attrib, buffer, render, shader}; use ctru::prelude::*; use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D}; @@ -167,46 +169,34 @@ fn calculate_projections() -> Projections { // TODO: it would be cool to allow playing around with these parameters on // the fly with D-pad, etc. let slider_val = unsafe { ctru_sys::osGet3DSliderState() }; - let interocular_distance = slider_val / 4.0; + let interocular_distance = slider_val / 2.0; let vertical_fov = 40.0_f32.to_radians(); let screen_depth = 2.0; - let clip_plane = ClipPlane { + let clip_planes = ClipPlanes { near: 0.01, far: 100.0, }; - let stereoscopic = Stereoscopic::Stereo { - interocular_distance, - screen_depth, - }; - - let left_eye = Matrix::perspective_projection( - vertical_fov, - AspectRatio::TopScreen, - Orientation::Natural, - clip_plane, - stereoscopic, - CoordinateSystem::LeftHanded, - ); + let stereo = StereoDisplacement::new(interocular_distance, screen_depth); - let right_eye = Matrix::perspective_projection( + let (left_eye, right_eye) = Matrix::stereo_projections( vertical_fov, AspectRatio::TopScreen, - Orientation::Natural, - clip_plane, - stereoscopic.invert(), - CoordinateSystem::LeftHanded, + ScreenOrientation::Rotated, + clip_planes, + CoordinateOrientation::LeftHanded, + stereo, ); let center = Matrix::perspective_projection( vertical_fov, AspectRatio::BottomScreen, - Orientation::Natural, - clip_plane, - Stereoscopic::Mono, - CoordinateSystem::LeftHanded, + ScreenOrientation::Rotated, + clip_planes, + CoordinateOrientation::LeftHanded, + None, ); Projections { diff --git a/citro3d/src/buffer.rs b/citro3d/src/buffer.rs index 534a487..b86be01 100644 --- a/citro3d/src/buffer.rs +++ b/citro3d/src/buffer.rs @@ -100,7 +100,6 @@ impl Info { /// /// * if `vbo_data` is not allocated with the [`ctru::linear`] allocator /// * if the maximum number (12) of VBOs are already registered - /// pub fn add<'this, 'vbo, 'idx, T>( &'this mut self, vbo_data: &'vbo [T], diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index 53c1350..ab10646 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -126,11 +126,37 @@ impl Instance { } } - pub fn bind_vertex_uniform(&mut self, index: uniform::Index, uniform: &impl Uniform) { + /// Bind a uniform to the given `index` in the vertex shader for the next draw call. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::{uniform, Matrix}; + /// # + /// # let mut instance = citro3d::Instance::new().unwrap(); + /// let idx = uniform::Index::from(0); + /// let mtx = Matrix::identity(); + /// instance.bind_vertex_uniform(idx, &mtx); + /// ``` + pub fn bind_vertex_uniform(&mut self, index: uniform::Index, uniform: impl Uniform) { uniform.bind(self, shader::Type::Vertex, index); } - pub fn bind_geometry_uniform(&mut self, index: uniform::Index, uniform: &impl Uniform) { + /// Bind a uniform to the given `index` in the geometry shader for the next draw call. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # use citro3d::{uniform, Matrix}; + /// # + /// # let mut instance = citro3d::Instance::new().unwrap(); + /// let idx = uniform::Index::from(0); + /// let mtx = Matrix::identity(); + /// instance.bind_geometry_uniform(idx, &mtx); + /// ``` + pub fn bind_geometry_uniform(&mut self, index: uniform::Index, uniform: impl Uniform) { uniform.bind(self, shader::Type::Geometry, index); } } @@ -142,21 +168,3 @@ impl Drop for Instance { } } } - -#[derive(Clone, Copy, Debug)] -#[non_exhaustive] -pub enum AspectRatio { - TopScreen, - BottomScreen, - Other(f32), -} - -impl From 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, - } - } -} diff --git a/citro3d/src/math.rs b/citro3d/src/math.rs index 734e4f2..cf67cad 100644 --- a/citro3d/src/math.rs +++ b/citro3d/src/math.rs @@ -2,133 +2,303 @@ use std::mem::MaybeUninit; -use crate::AspectRatio; - /// A 4-vector of `u8`s. -pub struct IntVec(citro3d_sys::C3D_IVec); +#[doc(alias = "C3D_IVec")] +pub struct IVec(citro3d_sys::C3D_IVec); /// A 4-vector of `f32`s. -pub struct FloatVec(citro3d_sys::C3D_FVec); +#[doc(alias = "C3D_FVec")] +pub struct FVec(citro3d_sys::C3D_FVec); -/// A quaternion, internally represented the same way as [`FloatVec`]. -pub struct Quaternion(citro3d_sys::C3D_FQuat); +/// 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 { - // TODO: does it make sense to have a helper that builds both left and right - // eyes for stereoscopic at the same time? + /// 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()) + } + } /// 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: f32, + vertical_fov_radians: f32, aspect_ratio: AspectRatio, - orientation: Orientation, - clip_plane: ClipPlane, - stereo: Stereoscopic, - coordinates: CoordinateSystem, + orientation: ScreenOrientation, + clip_plane: ClipPlanes, + coordinates: CoordinateOrientation, + stereo: Option, ) -> Self { - let (make_mtx_persp, make_mtx_stereo); + let mut result = MaybeUninit::uninit(); - let initialize_mtx: &dyn Fn(_, _, _, _, _, _) -> _ = match stereo { - Stereoscopic::Mono => { - let make_mtx = match orientation { - Orientation::Natural => citro3d_sys::Mtx_PerspTilt, - Orientation::HardwareDefault => citro3d_sys::Mtx_Persp, - }; + let left_handed = matches!(coordinates, CoordinateOrientation::LeftHanded); - make_mtx_persp = move |a, b, c, d, e, f| unsafe { make_mtx(a, b, c, d, e, f) }; - &make_mtx_persp + 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, + ); } - Stereoscopic::Stereo { - interocular_distance, - screen_depth, - } => { - let make_mtx = match orientation { - Orientation::Natural => citro3d_sys::Mtx_PerspStereoTilt, - Orientation::HardwareDefault => citro3d_sys::Mtx_PerspStereo, - }; - - make_mtx_stereo = move |a, b, c, d, e, f| unsafe { - make_mtx(a, b, c, d, interocular_distance, screen_depth, e, f) - }; - &make_mtx_stereo + } 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 left_handed = matches!(coordinates, CoordinateSystem::LeftHanded); - - let mut result = MaybeUninit::uninit(); - initialize_mtx( - result.as_mut_ptr(), - vertical_fov, - 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 } } -/// Whether to use left-handed or right-handed coordinates for calculations. +/// The [orientation](https://en.wikipedia.org/wiki/Orientation_(geometry)) +/// (or "handedness") of the coordinate system. #[derive(Clone, Copy, Debug)] -pub enum CoordinateSystem { +pub enum CoordinateOrientation { + /// A left-handed coordinate system. LeftHanded, + /// A right-handed coordinate system. RightHanded, } -/// Whether to rotate a projection to account for the 3DS screen configuration. -/// Both screens on the 3DS are oriented such that the "top" of the screen is -/// on the [left | right] ? side of the device when it's held normally, so -/// projections must account for this extra rotation to display in the correct -/// 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 +/// 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 Orientation { - /// Rotate the projection 90° to account for the 3DS screen rotation. - Natural, - /// Don't rotate the projection at all. - HardwareDefault, +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, } -#[derive(Clone, Copy, Debug)] -// TODO: better name -pub enum Stereoscopic { - Mono, - Stereo { - interocular_distance: f32, - // TODO: better name? At least docstring - screen_depth: f32, - }, -} +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, + } + } -impl Stereoscopic { - /// Flip the stereoscopic projection for the opposite eye. - pub fn invert(self) -> Self { + fn perpsective_stereo_builder( + self, + ) -> unsafe extern "C" fn(*mut citro3d_sys::C3D_Mtx, f32, f32, f32, f32, f32, f32, bool) { match self { - Self::Stereo { - interocular_distance, - screen_depth, - } => Self::Stereo { - interocular_distance: -interocular_distance, - screen_depth, - }, - mono => mono, + 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. +#[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 [frustum](https://en.wikipedia.org/wiki/Viewing_frustum) +/// of a perspective projection. #[derive(Clone, Copy, Debug)] -pub struct ClipPlane { +pub struct ClipPlanes { + /// The z-depth of the near clip plane. pub near: f32, + /// The z-depth of the far clip plane. 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 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, + } + } +} diff --git a/citro3d/src/shader.rs b/citro3d/src/shader.rs index 9d775f8..39b9aa8 100644 --- a/citro3d/src/shader.rs +++ b/citro3d/src/shader.rs @@ -115,7 +115,9 @@ impl Drop for Program { /// The type of a shader. #[repr(u32)] pub enum Type { + /// A vertex shader. Vertex = ctru_sys::GPU_VERTEX_SHADER, + /// A geometry shader. Geometry = ctru_sys::GPU_GEOMETRY_SHADER, } diff --git a/citro3d/src/uniform.rs b/citro3d/src/uniform.rs index 7033a22..c4fbf8b 100644 --- a/citro3d/src/uniform.rs +++ b/citro3d/src/uniform.rs @@ -1,5 +1,9 @@ +//! 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}; +/// The index of a uniform within a [`shader::Program`]. #[derive(Copy, Clone, Debug)] pub struct Index(i8); @@ -19,15 +23,21 @@ mod private { use crate::Matrix; pub trait Sealed {} - impl Sealed for Matrix {} + impl Sealed for &Matrix {} } +/// A shader uniform. This trait is implemented for types that can be bound to +/// shaders to be used as a uniform input to the shader. pub trait Uniform: private::Sealed { - fn bind(&self, instance: &mut Instance, shader_type: shader::Type, index: Index); + /// Bind the uniform to the given shader index for the given shader type. + /// An [`Instance`] is required to prevent concurrent binding of different + /// uniforms to the same index. + fn bind(self, instance: &mut Instance, shader_type: shader::Type, index: Index); } -impl Uniform for Matrix { - fn bind(&self, _instance: &mut Instance, type_: shader::Type, index: Index) { +impl Uniform for &Matrix { + #[doc(alias = "C3D_FVUnifMtx4x4")] + fn bind(self, _instance: &mut Instance, type_: shader::Type, index: Index) { unsafe { citro3d_sys::C3D_FVUnifMtx4x4(type_.into(), index.into(), self.as_raw()) } } }