Browse Source

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.
pull/27/head
Ian Chamberlain 1 year ago
parent
commit
2b1e1db28b
No known key found for this signature in database
GPG Key ID: AE5484D09405AA60
  1. 2
      citro3d-macros/src/lib.rs
  2. 44
      citro3d/examples/triangle.rs
  3. 1
      citro3d/src/buffer.rs
  4. 48
      citro3d/src/lib.rs
  5. 334
      citro3d/src/math.rs
  6. 2
      citro3d/src/shader.rs
  7. 18
      citro3d/src/uniform.rs

2
citro3d-macros/src/lib.rs

@ -1,3 +1,5 @@ @@ -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)]

44
citro3d/examples/triangle.rs

@ -4,9 +4,11 @@ @@ -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 { @@ -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 {

1
citro3d/src/buffer.rs

@ -100,7 +100,6 @@ impl Info { @@ -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],

48
citro3d/src/lib.rs

@ -126,11 +126,37 @@ impl Instance { @@ -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 { @@ -142,21 +168,3 @@ impl Drop for Instance {
}
}
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum AspectRatio {
TopScreen,
BottomScreen,
Other(f32),
}
impl From<AspectRatio> for f32 {
fn from(ratio: AspectRatio) -> Self {
match ratio {
AspectRatio::TopScreen => citro3d_sys::C3D_AspectRatioTop as f32,
AspectRatio::BottomScreen => citro3d_sys::C3D_AspectRatioBot as f32,
AspectRatio::Other(ratio) => ratio,
}
}
}

334
citro3d/src/math.rs

@ -2,133 +2,303 @@ @@ -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<StereoDisplacement>,
) -> 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<AspectRatio> for f32 {
fn from(ratio: AspectRatio) -> Self {
match ratio {
AspectRatio::TopScreen => citro3d_sys::C3D_AspectRatioTop as f32,
AspectRatio::BottomScreen => citro3d_sys::C3D_AspectRatioBot as f32,
AspectRatio::Other(ratio) => ratio,
}
}
}

2
citro3d/src/shader.rs

@ -115,7 +115,9 @@ impl Drop for Program { @@ -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,
}

18
citro3d/src/uniform.rs

@ -1,5 +1,9 @@ @@ -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 { @@ -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()) }
}
}

Loading…
Cancel
Save