Browse Source
Finally, something that I don't think totally sucks. Pick some sane defaults for some fields, and use generics to split between orthographic and perspective implementations. It's still not perfect but I think this looks a lot more ergonomic to use vs what I had before (and compared to the C APIs).pull/27/head
Ian Chamberlain
1 year ago
6 changed files with 348 additions and 211 deletions
@ -0,0 +1,264 @@
@@ -0,0 +1,264 @@
|
||||
use std::mem::MaybeUninit; |
||||
use std::ops::Range; |
||||
|
||||
use super::{ |
||||
AspectRatio, ClipPlanes, CoordinateOrientation, Matrix, ScreenOrientation, StereoDisplacement, |
||||
}; |
||||
|
||||
/// Configuration for a 3D [projection](https://en.wikipedia.org/wiki/3D_projection).
|
||||
/// See specific `Kind` implementations for constructors, e.g.
|
||||
/// [`Projection::perspective`] and [`Projection::orthographic`].
|
||||
///
|
||||
/// To use the resulting projection, convert it to a [`Matrix`] with [`From`]/[`Into`].
|
||||
#[derive(Clone, Debug)] |
||||
pub struct Projection<Kind> { |
||||
coordinates: CoordinateOrientation, |
||||
rotation: ScreenOrientation, |
||||
inner: Kind, |
||||
} |
||||
|
||||
impl<Kind> Projection<Kind> { |
||||
fn new(inner: Kind) -> Self { |
||||
Self { |
||||
coordinates: CoordinateOrientation::default(), |
||||
rotation: ScreenOrientation::default(), |
||||
inner, |
||||
} |
||||
} |
||||
|
||||
/// Set the coordinate system's orientation for the projection.
|
||||
/// See [`CoordinateOrientation`] for more details.
|
||||
pub fn coordinates(&mut self, orientation: CoordinateOrientation) -> &mut Self { |
||||
self.coordinates = orientation; |
||||
self |
||||
} |
||||
|
||||
/// Set the screen rotation for the projection.
|
||||
/// See [`ScreenOrientation`] for more details.
|
||||
pub fn screen(&mut self, orientation: ScreenOrientation) -> &mut Self { |
||||
self.rotation = orientation; |
||||
self |
||||
} |
||||
} |
||||
|
||||
/// See [`Projection::perspective`].
|
||||
#[derive(Clone, Debug)] |
||||
pub struct Perspective { |
||||
vertical_fov_radians: f32, |
||||
aspect_ratio: AspectRatio, |
||||
clip_planes: ClipPlanes, |
||||
stereo: Option<StereoDisplacement>, |
||||
} |
||||
|
||||
impl Projection<Perspective> { |
||||
/// Construct a projection matrix suitable for projecting 3D world space onto
|
||||
/// the 3DS screens.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `vertical_fov`: the vertical field of view, measured in radians
|
||||
/// * `aspect_ratio`: the aspect ratio of the projection
|
||||
/// * `clip_planes`: the near and far clip planes of the view frustum.
|
||||
/// [`ClipPlanes`] are always defined by near and far values, regardless
|
||||
/// of the projection's [`CoordinateOrientation`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use citro3d::math::*;
|
||||
/// # use std::f32::consts::PI;
|
||||
/// #
|
||||
/// # let _runner = test_runner::GdbRunner::default();
|
||||
/// #
|
||||
/// let clip_planes = ClipPlanes {
|
||||
/// near: 0.01,
|
||||
/// far: 100.0,
|
||||
/// };
|
||||
///
|
||||
/// let bottom: Matrix =
|
||||
/// Projection::perspective(PI / 4.0, AspectRatio::BottomScreen, clip_planes).into();
|
||||
///
|
||||
/// let top: Matrix = Projection::perspective(PI / 4.0, AspectRatio::TopScreen, clip_planes).into();
|
||||
/// ```
|
||||
#[doc(alias = "Mtx_Persp")] |
||||
#[doc(alias = "Mtx_PerspTilt")] |
||||
pub fn perspective( |
||||
vertical_fov_radians: f32, |
||||
aspect_ratio: AspectRatio, |
||||
clip_planes: ClipPlanes, |
||||
) -> Self { |
||||
Self::new(Perspective { |
||||
vertical_fov_radians, |
||||
aspect_ratio, |
||||
clip_planes, |
||||
stereo: None, |
||||
}) |
||||
} |
||||
|
||||
/// Helper function to build both eyes' perspective projection matrices
|
||||
/// at once. See [`StereoDisplacement`] for details on how to configure
|
||||
/// stereoscopy.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::f32::consts::PI;
|
||||
/// # use citro3d::math::*;
|
||||
/// #
|
||||
/// # let _runner = test_runner::GdbRunner::default();
|
||||
/// #
|
||||
/// let (left, right) = StereoDisplacement::new(0.5, 2.0);
|
||||
/// let (left_eye, right_eye) = Projection::perspective(
|
||||
/// PI / 4.0,
|
||||
/// AspectRatio::TopScreen,
|
||||
/// ClipPlanes {
|
||||
/// near: 0.01,
|
||||
/// far: 100.0,
|
||||
/// },
|
||||
/// )
|
||||
/// .stereo_matrices(left, right);
|
||||
/// ```
|
||||
#[doc(alias = "Mtx_PerspStereo")] |
||||
#[doc(alias = "Mtx_PerspStereoTilt")] |
||||
pub fn stereo_matrices( |
||||
self, |
||||
left_eye: StereoDisplacement, |
||||
right_eye: StereoDisplacement, |
||||
) -> (Matrix, Matrix) { |
||||
// TODO: we might be able to avoid this clone if there was a conversion
|
||||
// from &Self to Matrix instead of Self... but it's probably fine for now
|
||||
let left = self.clone().stereo(left_eye); |
||||
let right = self.stereo(right_eye); |
||||
// Also, we could consider just returning (Self, Self) here? idk
|
||||
(left.into(), right.into()) |
||||
} |
||||
|
||||
fn stereo(mut self, displacement: StereoDisplacement) -> Self { |
||||
self.inner.stereo = Some(displacement); |
||||
self |
||||
} |
||||
} |
||||
|
||||
impl From<Projection<Perspective>> for Matrix { |
||||
fn from(projection: Projection<Perspective>) -> Self { |
||||
let Perspective { |
||||
vertical_fov_radians, |
||||
aspect_ratio, |
||||
clip_planes, |
||||
stereo, |
||||
} = projection.inner; |
||||
|
||||
let mut result = MaybeUninit::uninit(); |
||||
|
||||
if let Some(stereo) = stereo { |
||||
let make_mtx = match projection.rotation { |
||||
ScreenOrientation::Rotated => citro3d_sys::Mtx_PerspStereoTilt, |
||||
ScreenOrientation::None => citro3d_sys::Mtx_PerspStereo, |
||||
}; |
||||
unsafe { |
||||
make_mtx( |
||||
result.as_mut_ptr(), |
||||
vertical_fov_radians, |
||||
aspect_ratio.into(), |
||||
clip_planes.near, |
||||
clip_planes.far, |
||||
stereo.displacement, |
||||
stereo.screen_depth, |
||||
projection.coordinates.is_left_handed(), |
||||
); |
||||
} |
||||
} else { |
||||
let make_mtx = match projection.rotation { |
||||
ScreenOrientation::Rotated => citro3d_sys::Mtx_PerspTilt, |
||||
ScreenOrientation::None => citro3d_sys::Mtx_Persp, |
||||
}; |
||||
unsafe { |
||||
make_mtx( |
||||
result.as_mut_ptr(), |
||||
vertical_fov_radians, |
||||
aspect_ratio.into(), |
||||
clip_planes.near, |
||||
clip_planes.far, |
||||
projection.coordinates.is_left_handed(), |
||||
); |
||||
} |
||||
} |
||||
|
||||
unsafe { Self(result.assume_init()) } |
||||
} |
||||
} |
||||
|
||||
/// See [`Projection::orthographic`].
|
||||
#[derive(Clone, Debug)] |
||||
pub struct Orthographic { |
||||
clip_planes_x: Range<f32>, |
||||
clip_planes_y: Range<f32>, |
||||
clip_planes_z: ClipPlanes, |
||||
} |
||||
|
||||
impl Projection<Orthographic> { |
||||
/// Construct an orthographic projection. The X and Y clip planes are passed
|
||||
/// as ranges because their coordinates are always oriented the same way
|
||||
/// (+X right, +Y up).
|
||||
///
|
||||
/// The Z [`ClipPlanes`], however, are always defined by
|
||||
/// near and far values, regardless of the projection's [`CoordinateOrientation`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # let _runner = test_runner::GdbRunner::default();
|
||||
/// # use citro3d::math::{Projection, ClipPlanes, Matrix};
|
||||
/// #
|
||||
/// let mtx: Matrix = Projection::orthographic(
|
||||
/// 0.0..240.0,
|
||||
/// 0.0..400.0,
|
||||
/// ClipPlanes {
|
||||
/// near: 0.0,
|
||||
/// far: 100.0,
|
||||
/// },
|
||||
/// )
|
||||
/// .into();
|
||||
/// ```
|
||||
#[doc(alias = "Mtx_Ortho")] |
||||
#[doc(alias = "Mtx_OrthoTilt")] |
||||
pub fn orthographic( |
||||
clip_planes_x: Range<f32>, |
||||
clip_planes_y: Range<f32>, |
||||
clip_planes_z: ClipPlanes, |
||||
) -> Self { |
||||
Self::new(Orthographic { |
||||
clip_planes_x, |
||||
clip_planes_y, |
||||
clip_planes_z, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
impl From<Projection<Orthographic>> for Matrix { |
||||
fn from(projection: Projection<Orthographic>) -> Self { |
||||
let make_mtx = match projection.rotation { |
||||
ScreenOrientation::Rotated => citro3d_sys::Mtx_OrthoTilt, |
||||
ScreenOrientation::None => citro3d_sys::Mtx_Ortho, |
||||
}; |
||||
|
||||
let Orthographic { |
||||
clip_planes_x, |
||||
clip_planes_y, |
||||
clip_planes_z, |
||||
} = projection.inner; |
||||
|
||||
let mut out = MaybeUninit::uninit(); |
||||
unsafe { |
||||
make_mtx( |
||||
out.as_mut_ptr(), |
||||
clip_planes_x.start, |
||||
clip_planes_x.end, |
||||
clip_planes_y.start, |
||||
clip_planes_y.end, |
||||
clip_planes_z.near, |
||||
clip_planes_z.far, |
||||
projection.coordinates.is_left_handed(), |
||||
); |
||||
Self(out.assume_init()) |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue