From 2301e5022cdf756f81ded5492a21c3676bdfe716 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sat, 30 Sep 2023 22:00:53 -0400 Subject: [PATCH 1/6] Start porting , vectors and matrices --- citro3d/examples/triangle.rs | 114 ++++++++++++++--------------------- citro3d/src/lib.rs | 36 ++++++++++- citro3d/src/math.rs | 87 ++++++++++++++++++++++++++ citro3d/src/shader.rs | 27 ++++++++- 4 files changed, 191 insertions(+), 73 deletions(-) create mode 100644 citro3d/src/math.rs diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index 8f3522f..e8ca2c2 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -3,13 +3,10 @@ #![feature(allocator_api)] -use std::ffi::CStr; -use std::mem::MaybeUninit; - use citro3d::macros::include_shader; +use citro3d::math::{CoordinateSystem, Matrix}; use citro3d::render::{self, ClearFlags}; -use citro3d::{attrib, buffer, shader}; -use citro3d_sys::C3D_Mtx; +use citro3d::{attrib, buffer, shader, AspectRatio}; use ctru::prelude::*; use ctru::services::gfx::{RawFrameBuffer, Screen, TopScreen3D}; @@ -92,7 +89,10 @@ fn main() { let mut buf_info = buffer::Info::new(); let (attr_info, vbo_idx) = prepare_vbos(&mut buf_info, &vbo_data); - let projection_uniform_idx = scene_init(&mut program); + scene_init(&mut program); + + let projection_uniform_idx = program.get_uniform_location("projection").unwrap(); + while apt.main_loop() { hid.scan_input(); @@ -109,14 +109,7 @@ fn main() { let clear_color: u32 = 0x7F_7F_7F_FF; target.clear(ClearFlags::ALL, clear_color, 0); - unsafe { - // Update the uniforms - citro3d_sys::C3D_FVUnifMtx4x4( - ctru_sys::GPU_VERTEX_SHADER, - projection_uniform_idx.into(), - projection, - ); - } + instance.update_vertex_uniform_mat4x4(projection_uniform_idx, projection); instance.set_attr_info(&attr_info); @@ -165,16 +158,12 @@ where } struct Projections { - left: C3D_Mtx, - right: C3D_Mtx, - center: C3D_Mtx, + left: Matrix, + right: Matrix, + center: Matrix, } fn calculate_projections() -> Projections { - let mut left_eye = MaybeUninit::uninit(); - let mut right_eye = MaybeUninit::uninit(); - let mut center = MaybeUninit::uninit(); - // 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() }; @@ -182,53 +171,48 @@ fn calculate_projections() -> Projections { let near = 0.01; let far = 100.0; - let fovy = 40.0_f32.to_radians(); + let fov_y = 40.0_f32.to_radians(); let screen = 2.0; - unsafe { - citro3d_sys::Mtx_PerspStereoTilt( - left_eye.as_mut_ptr(), - fovy, - citro3d_sys::C3D_AspectRatioTop as f32, - near, - far, - -iod, - screen, - true, - ); - - citro3d_sys::Mtx_PerspStereoTilt( - right_eye.as_mut_ptr(), - fovy, - citro3d_sys::C3D_AspectRatioTop as f32, - near, - far, - iod, - screen, - true, - ); - - citro3d_sys::Mtx_PerspTilt( - center.as_mut_ptr(), - fovy, - citro3d_sys::C3D_AspectRatioBot as f32, - near, - far, - true, - ); - - Projections { - left: left_eye.assume_init(), - right: right_eye.assume_init(), - center: center.assume_init(), - } + let left_eye = Matrix::perspective_stereo_tilt( + fov_y, + AspectRatio::TopScreen, + near, + far, + -iod, + screen, + CoordinateSystem::LeftHanded, + ); + + let right_eye = Matrix::perspective_stereo_tilt( + fov_y, + AspectRatio::TopScreen, + near, + far, + iod, + screen, + CoordinateSystem::LeftHanded, + ); + + let center = Matrix::perspective_tilt( + fov_y, + AspectRatio::BottomScreen, + near, + far, + CoordinateSystem::LeftHanded, + ); + + Projections { + left: left_eye, + right: right_eye, + center, } } -fn scene_init(program: &mut shader::Program) -> i8 { +fn scene_init(program: &mut shader::Program) { // Load the vertex shader, create a shader program and bind it unsafe { - citro3d_sys::C3D_BindProgram(program.as_raw()); + citro3d_sys::C3D_BindProgram(program.as_raw_mut()); // Configure the first fragment shading substage to just pass through the vertex color // See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight @@ -242,13 +226,5 @@ fn scene_init(program: &mut shader::Program) -> i8 { 0, ); citro3d_sys::C3D_TexEnvFunc(env, citro3d_sys::C3D_Both, ctru_sys::GPU_REPLACE); - - // Get the location of the uniforms - let projection_name = CStr::from_bytes_with_nul(b"projection\0").unwrap(); - - ctru_sys::shaderInstanceGetUniformLocation( - (*program.as_raw()).vertexShader, - projection_name.as_ptr(), - ) } } diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index c7dbe0e..68a25ae 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -5,11 +5,12 @@ pub mod attrib; pub mod buffer; pub mod error; +pub mod math; pub mod render; pub mod shader; -use citro3d_sys::C3D_FrameDrawOn; pub use error::{Error, Result}; +pub use math::Matrix; pub mod macros { //! Helper macros for working with shaders. @@ -53,7 +54,7 @@ impl Instance { /// Fails if the given target cannot be used for drawing. pub fn select_render_target(&mut self, target: &render::Target<'_>) -> Result<()> { let _ = self; - if unsafe { C3D_FrameDrawOn(target.as_raw()) } { + if unsafe { citro3d_sys::C3D_FrameDrawOn(target.as_raw()) } { Ok(()) } else { Err(Error::InvalidRenderTarget) @@ -121,6 +122,19 @@ impl Instance { ); } } + + // TODO: need separate versions for vertex/geometry and different dimensions? + // Maybe we could do something nicer with const generics, or something, although + // it will probably be tricker + pub fn update_vertex_uniform_mat4x4(&mut self, index: i8, matrix: &Matrix) { + unsafe { + citro3d_sys::C3D_FVUnifMtx4x4( + ctru_sys::GPU_VERTEX_SHADER, + index.into(), + matrix.as_raw(), + ) + } + } } impl Drop for Instance { @@ -130,3 +144,21 @@ 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 new file mode 100644 index 0000000..0ee26f1 --- /dev/null +++ b/citro3d/src/math.rs @@ -0,0 +1,87 @@ +//! Safe wrappers for working with matrix and vector types provided by `citro3d`. + +use std::mem::MaybeUninit; + +use crate::AspectRatio; + +/// A 4-vector of [`u8`]s. +pub struct IntVec(citro3d_sys::C3D_IVec); + +/// A 4-vector of [`f32`]s. +pub struct FloatVec(citro3d_sys::C3D_FVec); + +/// A quaternion, internally represented the same way as [`FVec`]. +pub struct Quaternion(citro3d_sys::C3D_FQuat); + +/// A 4x4 row-major matrix of [`f32`]s. +pub struct Matrix(citro3d_sys::C3D_Mtx); + +/// Whether to use left-handed or right-handed coordinates for calculations. +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] // This probably is exhaustive, but just in case +pub enum CoordinateSystem { + LeftHanded, + RightHanded, +} + +impl Matrix { + // TODO: this could probably be generalized with something like builder or options + // pattern. Should look and see what the different citro3d implementations look like + pub fn perspective_stereo_tilt( + fov_y: f32, + aspect_ratio: AspectRatio, + near: f32, + far: f32, + interocular_distance: f32, + /* better name ?? */ screen_depth: f32, + coordinates: CoordinateSystem, + ) -> Self { + let mut result = MaybeUninit::uninit(); + + let inner = unsafe { + citro3d_sys::Mtx_PerspStereoTilt( + result.as_mut_ptr(), + fov_y, + aspect_ratio.into(), + near, + far, + interocular_distance, + screen_depth, + matches!(coordinates, CoordinateSystem::LeftHanded), + ); + + result.assume_init() + }; + + Self(inner) + } + + pub fn perspective_tilt( + fov_y: f32, + aspect_ratio: AspectRatio, + near: f32, + far: f32, + coordinates: CoordinateSystem, + ) -> Self { + let mut result = MaybeUninit::uninit(); + + let inner = unsafe { + citro3d_sys::Mtx_PerspTilt( + result.as_mut_ptr(), + fov_y, + aspect_ratio.into(), + near, + far, + matches!(coordinates, CoordinateSystem::LeftHanded), + ); + + result.assume_init() + }; + + Self(inner) + } + + pub(crate) fn as_raw(&self) -> *const citro3d_sys::C3D_Mtx { + &self.0 + } +} diff --git a/citro3d/src/shader.rs b/citro3d/src/shader.rs index d3c0060..8500e2d 100644 --- a/citro3d/src/shader.rs +++ b/citro3d/src/shader.rs @@ -5,6 +5,7 @@ //! documentation for . use std::error::Error; +use std::ffi::CString; use std::mem::MaybeUninit; /// A PICA200 shader program. It may have one or both of: @@ -66,8 +67,30 @@ impl Program { } } + // TODO: newtype for index? + pub fn get_uniform_location(&self, name: &str) -> crate::Result { + let vertex_instance = unsafe { (*self.as_raw()).vertexShader }; + if vertex_instance.is_null() { + return Err(todo!()); + } + + let name = CString::new(name).map_err(|e| -> crate::Error { todo!() })?; + + let idx = + unsafe { ctru_sys::shaderInstanceGetUniformLocation(vertex_instance, name.as_ptr()) }; + + if idx < 0 { + Err(todo!()) + } else { + Ok(idx) + } + } // TODO: pub(crate) - pub fn as_raw(&mut self) -> *mut ctru_sys::shaderProgram_s { + pub fn as_raw(&self) -> *const ctru_sys::shaderProgram_s { + &self.program + } + + pub fn as_raw_mut(&mut self) -> *mut ctru_sys::shaderProgram_s { &mut self.program } } @@ -75,7 +98,7 @@ impl Program { impl Drop for Program { fn drop(&mut self) { unsafe { - let _ = ctru_sys::shaderProgramFree(self.as_raw()); + let _ = ctru_sys::shaderProgramFree(self.as_raw_mut()); } } } From 7084a22764bc0bedfc4434005311d4d8d520b477 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sun, 1 Oct 2023 00:22:56 -0400 Subject: [PATCH 2/6] Minor docs and error handling cleanup --- citro3d/src/error.rs | 11 +++++++++++ citro3d/src/math.rs | 8 ++++---- citro3d/src/shader.rs | 18 ++++++++++++------ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/citro3d/src/error.rs b/citro3d/src/error.rs index 29acd73..9a99089 100644 --- a/citro3d/src/error.rs +++ b/citro3d/src/error.rs @@ -1,5 +1,6 @@ //! General-purpose error and result types returned by public APIs of this crate. +use std::ffi::NulError; use std::num::TryFromIntError; use std::sync::TryLockError; @@ -31,6 +32,10 @@ pub enum Error { /// The given memory could not be converted to a physical address for sharing /// with the GPU. Data should be allocated with [`ctru::linear`]. InvalidMemoryLocation, + /// The given name was not valid for the requested purpose. + InvalidName, + /// The requested resource could not be found. + NotFound, } impl From for Error { @@ -44,3 +49,9 @@ impl From> for Error { Self::LockHeld } } + +impl From for Error { + fn from(_: NulError) -> Self { + Self::InvalidName + } +} diff --git a/citro3d/src/math.rs b/citro3d/src/math.rs index 0ee26f1..2ce9dd6 100644 --- a/citro3d/src/math.rs +++ b/citro3d/src/math.rs @@ -4,16 +4,16 @@ use std::mem::MaybeUninit; use crate::AspectRatio; -/// A 4-vector of [`u8`]s. +/// A 4-vector of `u8`s. pub struct IntVec(citro3d_sys::C3D_IVec); -/// A 4-vector of [`f32`]s. +/// A 4-vector of `f32`s. pub struct FloatVec(citro3d_sys::C3D_FVec); -/// A quaternion, internally represented the same way as [`FVec`]. +/// A quaternion, internally represented the same way as [`FloatVec`]. pub struct Quaternion(citro3d_sys::C3D_FQuat); -/// A 4x4 row-major matrix of [`f32`]s. +/// A 4x4 row-major matrix of `f32`s. pub struct Matrix(citro3d_sys::C3D_Mtx); /// Whether to use left-handed or right-handed coordinates for calculations. diff --git a/citro3d/src/shader.rs b/citro3d/src/shader.rs index 8500e2d..158fbb5 100644 --- a/citro3d/src/shader.rs +++ b/citro3d/src/shader.rs @@ -67,20 +67,26 @@ impl Program { } } - // TODO: newtype for index? + /// Get the index of a uniform by name. + /// + /// # Errors + /// + /// * If the given `name` contains a null byte + /// * If a uniform with the given `name` could not be found pub fn get_uniform_location(&self, name: &str) -> crate::Result { let vertex_instance = unsafe { (*self.as_raw()).vertexShader }; - if vertex_instance.is_null() { - return Err(todo!()); - } + assert!( + !vertex_instance.is_null(), + "vertex shader should never be null!" + ); - let name = CString::new(name).map_err(|e| -> crate::Error { todo!() })?; + let name = CString::new(name)?; let idx = unsafe { ctru_sys::shaderInstanceGetUniformLocation(vertex_instance, name.as_ptr()) }; if idx < 0 { - Err(todo!()) + Err(crate::Error::NotFound) } else { Ok(idx) } From 974ee2b742385c3ac8193dd653cb01a1f4b1ea76 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Thu, 5 Oct 2023 21:19:49 -0400 Subject: [PATCH 3/6] Consolidate matrix construction API --- citro3d/examples/triangle.rs | 63 ++++++++------- citro3d/src/math.rs | 147 +++++++++++++++++++++++------------ 2 files changed, 132 insertions(+), 78 deletions(-) diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index e8ca2c2..61d4620 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -4,7 +4,7 @@ #![feature(allocator_api)] use citro3d::macros::include_shader; -use citro3d::math::{CoordinateSystem, Matrix}; +use citro3d::math::{ClipPlane, CoordinateSystem, Matrix, Orientation, Stereoscopic}; use citro3d::render::{self, ClearFlags}; use citro3d::{attrib, buffer, shader, AspectRatio}; use ctru::prelude::*; @@ -117,8 +117,8 @@ fn main() { }; let Projections { - left, - right, + left_eye: left, + right_eye: right, center, } = calculate_projections(); @@ -158,8 +158,8 @@ where } struct Projections { - left: Matrix, - right: Matrix, + left_eye: Matrix, + right_eye: Matrix, center: Matrix, } @@ -167,44 +167,51 @@ 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 iod = slider_val / 4.0; + let interocular_distance = slider_val / 4.0; - let near = 0.01; - let far = 100.0; - let fov_y = 40.0_f32.to_radians(); - let screen = 2.0; + let vertical_fov = 40.0_f32.to_radians(); + let screen_depth = 2.0; - let left_eye = Matrix::perspective_stereo_tilt( - fov_y, + let clip_plane = ClipPlane { + near: 0.01, + far: 100.0, + }; + + let stereoscopic = Stereoscopic::Stereo { + interocular_distance, + screen_depth, + }; + + let left_eye = Matrix::perspective_projection( + vertical_fov, AspectRatio::TopScreen, - near, - far, - -iod, - screen, + Orientation::Natural, + clip_plane, + stereoscopic, CoordinateSystem::LeftHanded, ); - let right_eye = Matrix::perspective_stereo_tilt( - fov_y, + let right_eye = Matrix::perspective_projection( + vertical_fov, AspectRatio::TopScreen, - near, - far, - iod, - screen, + Orientation::Natural, + clip_plane, + stereoscopic.invert(), CoordinateSystem::LeftHanded, ); - let center = Matrix::perspective_tilt( - fov_y, + let center = Matrix::perspective_projection( + vertical_fov, AspectRatio::BottomScreen, - near, - far, + Orientation::Natural, + clip_plane, + Stereoscopic::Mono, CoordinateSystem::LeftHanded, ); Projections { - left: left_eye, - right: right_eye, + left_eye, + right_eye, center, } } diff --git a/citro3d/src/math.rs b/citro3d/src/math.rs index 2ce9dd6..734e4f2 100644 --- a/citro3d/src/math.rs +++ b/citro3d/src/math.rs @@ -16,67 +16,61 @@ pub struct Quaternion(citro3d_sys::C3D_FQuat); /// A 4x4 row-major matrix of `f32`s. pub struct Matrix(citro3d_sys::C3D_Mtx); -/// Whether to use left-handed or right-handed coordinates for calculations. -#[derive(Clone, Copy, Debug)] -#[non_exhaustive] // This probably is exhaustive, but just in case -pub enum CoordinateSystem { - LeftHanded, - RightHanded, -} - impl Matrix { - // TODO: this could probably be generalized with something like builder or options - // pattern. Should look and see what the different citro3d implementations look like - pub fn perspective_stereo_tilt( - fov_y: f32, + // TODO: does it make sense to have a helper that builds both left and right + // eyes for stereoscopic at the same time? + + /// Construct a projection matrix suitable for projecting 3D world space onto + /// the 3DS screens. + pub fn perspective_projection( + vertical_fov: f32, aspect_ratio: AspectRatio, - near: f32, - far: f32, - interocular_distance: f32, - /* better name ?? */ screen_depth: f32, + orientation: Orientation, + clip_plane: ClipPlane, + stereo: Stereoscopic, coordinates: CoordinateSystem, ) -> Self { - let mut result = MaybeUninit::uninit(); - - let inner = unsafe { - citro3d_sys::Mtx_PerspStereoTilt( - result.as_mut_ptr(), - fov_y, - aspect_ratio.into(), - near, - far, + let (make_mtx_persp, make_mtx_stereo); + + 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, + }; + + make_mtx_persp = move |a, b, c, d, e, f| unsafe { make_mtx(a, b, c, d, e, f) }; + &make_mtx_persp + } + Stereoscopic::Stereo { interocular_distance, screen_depth, - matches!(coordinates, CoordinateSystem::LeftHanded), - ); - - result.assume_init() + } => { + 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 + } }; - Self(inner) - } + let left_handed = matches!(coordinates, CoordinateSystem::LeftHanded); - pub fn perspective_tilt( - fov_y: f32, - aspect_ratio: AspectRatio, - near: f32, - far: f32, - coordinates: CoordinateSystem, - ) -> Self { 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 { - citro3d_sys::Mtx_PerspTilt( - result.as_mut_ptr(), - fov_y, - aspect_ratio.into(), - near, - far, - matches!(coordinates, CoordinateSystem::LeftHanded), - ); - - result.assume_init() - }; + let inner = unsafe { result.assume_init() }; Self(inner) } @@ -85,3 +79,56 @@ impl Matrix { &self.0 } } + +/// Whether to use left-handed or right-handed coordinates for calculations. +#[derive(Clone, Copy, Debug)] +pub enum CoordinateSystem { + LeftHanded, + 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. +#[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, +} + +#[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 Stereoscopic { + /// Flip the stereoscopic projection for the opposite eye. + pub fn invert(self) -> Self { + match self { + Self::Stereo { + interocular_distance, + screen_depth, + } => Self::Stereo { + interocular_distance: -interocular_distance, + screen_depth, + }, + mono => mono, + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct ClipPlane { + pub near: f32, + pub far: f32, +} From dcab5508f95537e92ce87a372e5e58b6a0d97643 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Thu, 5 Oct 2023 21:53:52 -0400 Subject: [PATCH 4/6] Use a simple trait for binding uniforms It's not much but it should be extensible enough to apply for other uniform types. We might want a generic impl for &[u8] or something as well to support custom uniform types, but that gets trickier. --- citro3d/examples/triangle.rs | 4 ++-- citro3d/src/lib.rs | 20 +++++++++----------- citro3d/src/shader.rs | 30 +++++++++++++++++++++++------- citro3d/src/uniform.rs | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 20 deletions(-) create mode 100644 citro3d/src/uniform.rs diff --git a/citro3d/examples/triangle.rs b/citro3d/examples/triangle.rs index 61d4620..4009f5b 100644 --- a/citro3d/examples/triangle.rs +++ b/citro3d/examples/triangle.rs @@ -91,7 +91,7 @@ fn main() { scene_init(&mut program); - let projection_uniform_idx = program.get_uniform_location("projection").unwrap(); + let projection_uniform_idx = program.get_uniform("projection").unwrap(); while apt.main_loop() { hid.scan_input(); @@ -109,7 +109,7 @@ fn main() { let clear_color: u32 = 0x7F_7F_7F_FF; target.clear(ClearFlags::ALL, clear_color, 0); - instance.update_vertex_uniform_mat4x4(projection_uniform_idx, projection); + instance.bind_vertex_uniform(projection_uniform_idx, projection); instance.set_attr_info(&attr_info); diff --git a/citro3d/src/lib.rs b/citro3d/src/lib.rs index 68a25ae..53c1350 100644 --- a/citro3d/src/lib.rs +++ b/citro3d/src/lib.rs @@ -8,10 +8,13 @@ pub mod error; pub mod math; pub mod render; pub mod shader; +pub mod uniform; pub use error::{Error, Result}; pub use math::Matrix; +use self::uniform::Uniform; + pub mod macros { //! Helper macros for working with shaders. pub use citro3d_macros::*; @@ -123,17 +126,12 @@ impl Instance { } } - // TODO: need separate versions for vertex/geometry and different dimensions? - // Maybe we could do something nicer with const generics, or something, although - // it will probably be tricker - pub fn update_vertex_uniform_mat4x4(&mut self, index: i8, matrix: &Matrix) { - unsafe { - citro3d_sys::C3D_FVUnifMtx4x4( - ctru_sys::GPU_VERTEX_SHADER, - index.into(), - matrix.as_raw(), - ) - } + 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) { + uniform.bind(self, shader::Type::Geometry, index); } } diff --git a/citro3d/src/shader.rs b/citro3d/src/shader.rs index 158fbb5..9d775f8 100644 --- a/citro3d/src/shader.rs +++ b/citro3d/src/shader.rs @@ -8,10 +8,12 @@ use std::error::Error; use std::ffi::CString; use std::mem::MaybeUninit; +use crate::uniform; + /// A PICA200 shader program. It may have one or both of: /// -/// * A vertex shader [`Library`] -/// * A geometry shader [`Library`] +/// * A [vertex](Type::Vertex) shader [`Library`] +/// * A [geometry](Type::Geometry) shader [`Library`] /// /// The PICA200 does not support user-programmable fragment shaders. pub struct Program { @@ -73,7 +75,7 @@ impl Program { /// /// * If the given `name` contains a null byte /// * If a uniform with the given `name` could not be found - pub fn get_uniform_location(&self, name: &str) -> crate::Result { + pub fn get_uniform(&self, name: &str) -> crate::Result { let vertex_instance = unsafe { (*self.as_raw()).vertexShader }; assert!( !vertex_instance.is_null(), @@ -88,14 +90,15 @@ impl Program { if idx < 0 { Err(crate::Error::NotFound) } else { - Ok(idx) + Ok(idx.into()) } } - // TODO: pub(crate) - pub fn as_raw(&self) -> *const ctru_sys::shaderProgram_s { + + pub(crate) fn as_raw(&self) -> *const ctru_sys::shaderProgram_s { &self.program } + // TODO: pub(crate) pub fn as_raw_mut(&mut self) -> *mut ctru_sys::shaderProgram_s { &mut self.program } @@ -109,6 +112,19 @@ impl Drop for Program { } } +/// The type of a shader. +#[repr(u32)] +pub enum Type { + Vertex = ctru_sys::GPU_VERTEX_SHADER, + Geometry = ctru_sys::GPU_GEOMETRY_SHADER, +} + +impl From for u32 { + fn from(value: Type) -> Self { + value as u32 + } +} + /// A PICA200 Shader Library (commonly called DVLB). This can be comprised of /// one or more [`Entrypoint`]s, but most commonly has one vertex shader and an /// optional geometry shader. @@ -131,7 +147,7 @@ impl Library { // SAFETY: we're trusting the parse implementation doesn't mutate // the contents of the data. From a quick read it looks like that's // correct and it should just take a const arg in the API. - aligned.as_ptr() as *mut _, + aligned.as_ptr().cast_mut(), aligned.len().try_into()?, ) })) diff --git a/citro3d/src/uniform.rs b/citro3d/src/uniform.rs new file mode 100644 index 0000000..7033a22 --- /dev/null +++ b/citro3d/src/uniform.rs @@ -0,0 +1,33 @@ +use crate::{shader, Instance, Matrix}; + +#[derive(Copy, Clone, Debug)] +pub struct Index(i8); + +impl From for Index { + fn from(value: i8) -> Self { + Self(value) + } +} + +impl From for i32 { + fn from(value: Index) -> Self { + value.0.into() + } +} + +mod private { + use crate::Matrix; + + pub trait Sealed {} + impl Sealed for Matrix {} +} + +pub trait Uniform: private::Sealed { + 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) { + unsafe { citro3d_sys::C3D_FVUnifMtx4x4(type_.into(), index.into(), self.as_raw()) } + } +} From 2b1e1db28b0293d179e8634f7b1cf46f5896092d Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sun, 8 Oct 2023 21:55:36 -0400 Subject: [PATCH 5/6] Refactor matrix API and add doctests etc Also add some doc aliases for citro3d functions. We could probably stand to add more aliases to other wrappers too. --- citro3d-macros/src/lib.rs | 2 + citro3d/examples/triangle.rs | 44 ++--- citro3d/src/buffer.rs | 1 - citro3d/src/lib.rs | 48 ++--- citro3d/src/math.rs | 334 ++++++++++++++++++++++++++--------- citro3d/src/shader.rs | 2 + citro3d/src/uniform.rs | 18 +- 7 files changed, 315 insertions(+), 134 deletions(-) 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()) } } } From 65941f671c0874b044e487bb2deac7053c3d877c Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Wed, 11 Oct 2023 23:53:48 -0400 Subject: [PATCH 6/6] 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). --- citro3d/examples/triangle.rs | 39 ++--- citro3d/src/lib.rs | 7 +- citro3d/src/math.rs | 241 ++++++++---------------------- citro3d/src/math/projection.rs | 264 +++++++++++++++++++++++++++++++++ citro3d/src/uniform.rs | 5 +- rustfmt.toml | 3 + 6 files changed, 348 insertions(+), 211 deletions(-) create mode 100644 citro3d/src/math/projection.rs create mode 100644 rustfmt.toml 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"