Ian Chamberlain
1 year ago
committed by
GitHub
10 changed files with 637 additions and 83 deletions
@ -0,0 +1,185 @@ |
|||||||
|
//! Safe wrappers for working with matrix and vector types provided by `citro3d`.
|
||||||
|
|
||||||
|
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); |
||||||
|
|
||||||
|
/// A 4-vector of `f32`s.
|
||||||
|
#[doc(alias = "C3D_FVec")] |
||||||
|
pub struct FVec(citro3d_sys::C3D_FVec); |
||||||
|
|
||||||
|
/// 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 { |
||||||
|
/// 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 { |
||||||
|
let mut out = MaybeUninit::uninit(); |
||||||
|
unsafe { |
||||||
|
citro3d_sys::Mtx_Identity(out.as_mut_ptr()); |
||||||
|
Self(out.assume_init()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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. Coordinates are always +Y-up,
|
||||||
|
/// +X-right.
|
||||||
|
#[derive(Clone, Copy, Debug)] |
||||||
|
pub enum CoordinateOrientation { |
||||||
|
/// A left-handed coordinate system. +Z points into the screen.
|
||||||
|
LeftHanded, |
||||||
|
/// 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
|
||||||
|
/// (i.e. the "width" is smaller than the "height").
|
||||||
|
#[derive(Clone, Copy, Debug)] |
||||||
|
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, |
||||||
|
} |
||||||
|
|
||||||
|
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
|
||||||
|
/// 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 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, usually close or equal to zero.
|
||||||
|
pub near: f32, |
||||||
|
/// The Z-depth of the far clip plane, usually greater than zero.
|
||||||
|
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, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// endregion
|
@ -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()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
//! Common definitions for binding uniforms to shaders. This is primarily
|
||||||
|
//! done by implementing the [`Uniform`] trait for a given type.
|
||||||
|
|
||||||
|
use crate::math::Matrix; |
||||||
|
use crate::{shader, Instance}; |
||||||
|
|
||||||
|
/// The index of a uniform within a [`shader::Program`].
|
||||||
|
#[derive(Copy, Clone, Debug)] |
||||||
|
pub struct Index(i8); |
||||||
|
|
||||||
|
impl From<i8> for Index { |
||||||
|
fn from(value: i8) -> Self { |
||||||
|
Self(value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl From<Index> for i32 { |
||||||
|
fn from(value: Index) -> Self { |
||||||
|
value.0.into() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
mod private { |
||||||
|
use crate::math::Matrix; |
||||||
|
|
||||||
|
pub trait Sealed {} |
||||||
|
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 { |
||||||
|
/// 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 { |
||||||
|
#[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…
Reference in new issue