citro3d/math/projection.rs
1use std::mem::MaybeUninit;
2use std::ops::Range;
3
4use super::Matrix4;
5
6/// Configuration for a 3D [projection](https://en.wikipedia.org/wiki/3D_projection).
7/// See specific `Kind` implementations for constructors, e.g.
8/// [`Projection::perspective`] and [`Projection::orthographic`].
9///
10/// To use the resulting projection, convert it to a [`Matrix4`] with [`From`]/[`Into`].
11#[derive(Clone, Debug)]
12pub struct Projection<Kind> {
13 coordinates: CoordinateOrientation,
14 rotation: ScreenOrientation,
15 inner: Kind,
16}
17
18impl<Kind> Projection<Kind> {
19 fn new(inner: Kind) -> Self {
20 Self {
21 coordinates: CoordinateOrientation::default(),
22 rotation: ScreenOrientation::default(),
23 inner,
24 }
25 }
26
27 /// Set the coordinate system's orientation for the projection.
28 /// See [`CoordinateOrientation`] for more details.
29 ///
30 /// # Example
31 ///
32 /// ```
33 /// # let _runner = test_runner::GdbRunner::default();
34 /// # use citro3d::math::{Projection, AspectRatio, CoordinateOrientation, Matrix4, ClipPlanes};
35 /// let clip_planes = ClipPlanes {
36 /// near: 0.1,
37 /// far: 100.0,
38 /// };
39 /// let mtx: Matrix4 = Projection::perspective(40.0, AspectRatio::TopScreen, clip_planes)
40 /// .coordinates(CoordinateOrientation::LeftHanded)
41 /// .into();
42 /// ```
43 pub fn coordinates(mut self, orientation: CoordinateOrientation) -> Self {
44 self.coordinates = orientation;
45 self
46 }
47
48 /// Set the screen rotation for the projection.
49 /// See [`ScreenOrientation`] for more details.
50 ///
51 /// # Example
52 ///
53 /// ```
54 /// # let _runner = test_runner::GdbRunner::default();
55 /// # use citro3d::math::{Projection, AspectRatio, ScreenOrientation, Matrix4, ClipPlanes};
56 /// let clip_planes = ClipPlanes {
57 /// near: 0.1,
58 /// far: 100.0,
59 /// };
60 /// let mtx: Matrix4 = Projection::perspective(40.0, AspectRatio::TopScreen, clip_planes)
61 /// .screen(ScreenOrientation::None)
62 /// .into();
63 /// ```
64 pub fn screen(mut self, orientation: ScreenOrientation) -> Self {
65 self.rotation = orientation;
66 self
67 }
68}
69
70/// See [`Projection::perspective`].
71#[derive(Clone, Debug)]
72pub struct Perspective {
73 vertical_fov_radians: f32,
74 aspect_ratio: AspectRatio,
75 clip_planes: ClipPlanes,
76 stereo: Option<StereoDisplacement>,
77}
78
79impl Projection<Perspective> {
80 /// Construct a projection matrix suitable for projecting 3D world space onto
81 /// the 3DS screens.
82 ///
83 /// # Parameters
84 ///
85 /// * `vertical_fov`: the vertical field of view, measured in radians
86 /// * `aspect_ratio`: the aspect ratio of the projection
87 /// * `clip_planes`: the near and far clip planes of the view frustum.
88 /// [`ClipPlanes`] are always defined by near and far values, regardless
89 /// of the projection's [`CoordinateOrientation`].
90 ///
91 /// # Examples
92 ///
93 /// ```
94 /// # use citro3d::math::*;
95 /// # use std::f32::consts::PI;
96 /// #
97 /// # let _runner = test_runner::GdbRunner::default();
98 /// #
99 /// let clip_planes = ClipPlanes {
100 /// near: 0.01,
101 /// far: 100.0,
102 /// };
103 ///
104 /// let bottom: Matrix4 =
105 /// Projection::perspective(PI / 4.0, AspectRatio::BottomScreen, clip_planes).into();
106 ///
107 /// let top: Matrix4 =
108 /// Projection::perspective(PI / 4.0, AspectRatio::TopScreen, clip_planes).into();
109 /// ```
110 #[doc(alias = "Mtx_Persp")]
111 #[doc(alias = "Mtx_PerspTilt")]
112 pub fn perspective(
113 vertical_fov_radians: f32,
114 aspect_ratio: AspectRatio,
115 clip_planes: ClipPlanes,
116 ) -> Self {
117 Self::new(Perspective {
118 vertical_fov_radians,
119 aspect_ratio,
120 clip_planes,
121 stereo: None,
122 })
123 }
124
125 /// Helper function to build both eyes' perspective projection matrices
126 /// at once. See [`StereoDisplacement`] for details on how to configure
127 /// stereoscopy.
128 ///
129 /// ```
130 /// # use std::f32::consts::PI;
131 /// # use citro3d::math::*;
132 /// #
133 /// # let _runner = test_runner::GdbRunner::default();
134 /// #
135 /// let (left, right) = StereoDisplacement::new(0.5, 2.0);
136 /// let (left_eye, right_eye) = Projection::perspective(
137 /// PI / 4.0,
138 /// AspectRatio::TopScreen,
139 /// ClipPlanes {
140 /// near: 0.01,
141 /// far: 100.0,
142 /// },
143 /// )
144 /// .stereo_matrices(left, right);
145 /// ```
146 #[doc(alias = "Mtx_PerspStereo")]
147 #[doc(alias = "Mtx_PerspStereoTilt")]
148 pub fn stereo_matrices(
149 self,
150 left_eye: StereoDisplacement,
151 right_eye: StereoDisplacement,
152 ) -> (Matrix4, Matrix4) {
153 // TODO: we might be able to avoid this clone if there was a conversion
154 // from &Self to Matrix4 instead of Self... but it's probably fine for now
155 let left = self.clone().stereo(left_eye);
156 let right = self.stereo(right_eye);
157 // Also, we could consider just returning (Self, Self) here? idk
158 (left.into(), right.into())
159 }
160
161 fn stereo(mut self, displacement: StereoDisplacement) -> Self {
162 self.inner.stereo = Some(displacement);
163 self
164 }
165}
166
167impl From<Projection<Perspective>> for Matrix4 {
168 fn from(projection: Projection<Perspective>) -> Self {
169 let Perspective {
170 vertical_fov_radians,
171 aspect_ratio,
172 clip_planes,
173 stereo,
174 } = projection.inner;
175
176 let mut result = MaybeUninit::uninit();
177
178 if let Some(stereo) = stereo {
179 let make_mtx = match projection.rotation {
180 ScreenOrientation::Rotated => citro3d_sys::Mtx_PerspStereoTilt,
181 ScreenOrientation::None => citro3d_sys::Mtx_PerspStereo,
182 };
183 unsafe {
184 make_mtx(
185 result.as_mut_ptr(),
186 vertical_fov_radians,
187 aspect_ratio.into(),
188 clip_planes.near,
189 clip_planes.far,
190 stereo.displacement,
191 stereo.screen_depth,
192 projection.coordinates.is_left_handed(),
193 );
194 }
195 } else {
196 let make_mtx = match projection.rotation {
197 ScreenOrientation::Rotated => citro3d_sys::Mtx_PerspTilt,
198 ScreenOrientation::None => citro3d_sys::Mtx_Persp,
199 };
200 unsafe {
201 make_mtx(
202 result.as_mut_ptr(),
203 vertical_fov_radians,
204 aspect_ratio.into(),
205 clip_planes.near,
206 clip_planes.far,
207 projection.coordinates.is_left_handed(),
208 );
209 }
210 }
211
212 unsafe { Self::from_raw(result.assume_init()) }
213 }
214}
215
216/// See [`Projection::orthographic`].
217#[derive(Clone, Debug)]
218pub struct Orthographic {
219 clip_planes_x: Range<f32>,
220 clip_planes_y: Range<f32>,
221 clip_planes_z: ClipPlanes,
222}
223
224impl Projection<Orthographic> {
225 /// Construct an orthographic projection. The X and Y clip planes are passed
226 /// as ranges because their coordinates are always oriented the same way
227 /// (+X right, +Y up).
228 ///
229 /// The Z [`ClipPlanes`], however, are always defined by
230 /// near and far values, regardless of the projection's [`CoordinateOrientation`].
231 ///
232 /// # Example
233 ///
234 /// ```
235 /// # let _runner = test_runner::GdbRunner::default();
236 /// # use citro3d::math::{Projection, ClipPlanes, Matrix4};
237 /// #
238 /// let mtx: Matrix4 = Projection::orthographic(
239 /// 0.0..240.0,
240 /// 0.0..400.0,
241 /// ClipPlanes {
242 /// near: 0.0,
243 /// far: 100.0,
244 /// },
245 /// )
246 /// .into();
247 /// ```
248 #[doc(alias = "Mtx_Ortho")]
249 #[doc(alias = "Mtx_OrthoTilt")]
250 pub fn orthographic(
251 clip_planes_x: Range<f32>,
252 clip_planes_y: Range<f32>,
253 clip_planes_z: ClipPlanes,
254 ) -> Self {
255 Self::new(Orthographic {
256 clip_planes_x,
257 clip_planes_y,
258 clip_planes_z,
259 })
260 }
261}
262
263impl From<Projection<Orthographic>> for Matrix4 {
264 fn from(projection: Projection<Orthographic>) -> Self {
265 let make_mtx = match projection.rotation {
266 ScreenOrientation::Rotated => citro3d_sys::Mtx_OrthoTilt,
267 ScreenOrientation::None => citro3d_sys::Mtx_Ortho,
268 };
269
270 let Orthographic {
271 clip_planes_x,
272 clip_planes_y,
273 clip_planes_z,
274 } = projection.inner;
275
276 let mut out = MaybeUninit::uninit();
277 unsafe {
278 make_mtx(
279 out.as_mut_ptr(),
280 clip_planes_x.start,
281 clip_planes_x.end,
282 clip_planes_y.start,
283 clip_planes_y.end,
284 clip_planes_z.near,
285 clip_planes_z.far,
286 projection.coordinates.is_left_handed(),
287 );
288 Self::from_raw(out.assume_init())
289 }
290 }
291}
292
293// region: Projection configuration
294
295/// The [orientation](https://en.wikipedia.org/wiki/Orientation_(geometry))
296/// (or "handedness") of the coordinate system. Coordinates are always +Y-up,
297/// +X-right.
298#[derive(Clone, Copy, Debug)]
299pub enum CoordinateOrientation {
300 /// A left-handed coordinate system. +Z points into the screen.
301 LeftHanded,
302 /// A right-handed coordinate system. +Z points out of the screen.
303 RightHanded,
304}
305
306impl CoordinateOrientation {
307 pub(crate) fn is_left_handed(self) -> bool {
308 matches!(self, Self::LeftHanded)
309 }
310}
311
312impl Default for CoordinateOrientation {
313 /// This is an opinionated default, but [`RightHanded`](Self::RightHanded)
314 /// seems to be the preferred coordinate system for most
315 /// [examples](https://github.com/devkitPro/3ds-examples)
316 /// from upstream, and is also fairly common in other applications.
317 fn default() -> Self {
318 Self::RightHanded
319 }
320}
321
322/// Whether to rotate a projection to account for the 3DS screen orientation.
323/// Both screens on the 3DS are oriented such that the "top-left" of the screen
324/// in framebuffer coordinates is the physical bottom-left of the screen
325/// (i.e. the "width" is smaller than the "height").
326#[derive(Clone, Copy, Debug)]
327pub enum ScreenOrientation {
328 /// Rotate 90° clockwise to account for the 3DS screen rotation. Most
329 /// applications will use this variant.
330 Rotated,
331 /// Do not apply any extra rotation to the projection.
332 None,
333}
334
335impl Default for ScreenOrientation {
336 fn default() -> Self {
337 Self::Rotated
338 }
339}
340
341/// Configuration for calculating stereoscopic projections.
342// TODO: not totally happy with this name + API yet, but it works for now.
343#[derive(Clone, Copy, Debug)]
344pub struct StereoDisplacement {
345 /// The horizontal offset of the eye from center. Negative values
346 /// correspond to the left eye, and positive values to the right eye.
347 pub displacement: f32,
348 /// The position of the screen, which determines the focal length. Objects
349 /// closer than this depth will appear to pop out of the screen, and objects
350 /// further than this will appear inside the screen.
351 pub screen_depth: f32,
352}
353
354impl StereoDisplacement {
355 /// Construct displacement for the left and right eyes simulataneously.
356 /// The given `interocular_distance` describes the distance between the two
357 /// rendered "eyes". A negative value will be treated the same as a positive
358 /// value of the same magnitude.
359 ///
360 /// See struct documentation for details about the
361 /// [`screen_depth`](Self::screen_depth) parameter.
362 pub fn new(interocular_distance: f32, screen_depth: f32) -> (Self, Self) {
363 let displacement = interocular_distance.abs() / 2.0;
364
365 let left_eye = Self {
366 displacement: -displacement,
367 screen_depth,
368 };
369 let right_eye = Self {
370 displacement,
371 screen_depth,
372 };
373
374 (left_eye, right_eye)
375 }
376}
377
378/// Configuration for the clipping planes of a projection.
379///
380/// For [`Perspective`] projections, this is used for the near and far clip planes
381/// of the [view frustum](https://en.wikipedia.org/wiki/Viewing_frustum).
382///
383/// For [`Orthographic`] projections, this is used for the Z clipping planes of
384/// the projection.
385///
386/// Note that the `near` value should always be less than `far`, regardless of
387/// [`CoordinateOrientation`]. In other words, these values will be negated
388/// when used with a [`RightHanded`](CoordinateOrientation::RightHanded)
389/// orientation.
390#[derive(Clone, Copy, Debug)]
391pub struct ClipPlanes {
392 /// The Z-depth of the near clip plane, usually close or equal to zero.
393 pub near: f32,
394 /// The Z-depth of the far clip plane, usually greater than zero.
395 pub far: f32,
396}
397
398/// The aspect ratio of a projection plane.
399#[derive(Clone, Copy, Debug)]
400#[non_exhaustive]
401#[doc(alias = "C3D_AspectRatioTop")]
402#[doc(alias = "C3D_AspectRatioBot")]
403pub enum AspectRatio {
404 /// The aspect ratio of the 3DS' top screen (per-eye).
405 #[doc(alias = "C3D_AspectRatioTop")]
406 TopScreen,
407 /// The aspect ratio of the 3DS' bottom screen.
408 #[doc(alias = "C3D_AspectRatioBot")]
409 BottomScreen,
410 /// A custom aspect ratio (should be calcualted as `width / height`).
411 Other(f32),
412}
413
414impl From<AspectRatio> for f32 {
415 fn from(ratio: AspectRatio) -> Self {
416 match ratio {
417 AspectRatio::TopScreen => citro3d_sys::C3D_AspectRatioTop as f32,
418 AspectRatio::BottomScreen => citro3d_sys::C3D_AspectRatioBot as f32,
419 AspectRatio::Other(ratio) => ratio,
420 }
421 }
422}
423
424// endregion