Skip to main content

gl_utils/
camera3d.rs

1//! Types and functions for 3D rendering.
2
3use std;
4use math_utils as math;
5use math_utils::{approx, num, vek};
6
7use crate::graphics;
8
9const PERSPECTIVE_INITIAL_FOVY : math::Deg <f32> = math::Deg (90.0);
10const PERSPECTIVE_NEAR_PLANE   : f32 =    0.1;
11const PERSPECTIVE_FAR_PLANE    : f32 = 1000.0;
12/// 1.0 units is equal to `ORTHOGRAPHIC_PIXEL_SCALE` pixels at 1.0x zoom
13const ORTHOGRAPHIC_PIXEL_SCALE : f32 =   64.0;
14const ORTHOGRAPHIC_NEAR_PLANE  : f32 =    0.0;
15const ORTHOGRAPHIC_FAR_PLANE   : f32 = 1000.0;
16
17/// Represents a camera ("view") positioned and oriented in a 3D scene with a
18/// 3D transformation and a 3D projection.
19#[derive(Clone, Debug, PartialEq)]
20pub struct Camera3d {
21  // transform
22  /// Position and orientation angles
23  pose                        : math::Pose3 <f32>,
24  /// Basis derived from pose angles
25  orientation                 : math::Rotation3 <f32>,
26  /// Transforms points from world space to camera (view, eye) space as
27  /// specified by the camera position and orientation.
28  ///
29  /// Note that world space is assumed to have 'up' vector Z while in OpenGL
30  /// the negative Z axis is 'into' the screen so this matrix performs the
31  /// required transform mapping positive Y to negative Z and positive Z to
32  /// positive Y.
33  transform_mat_world_to_view : math::Matrix4   <f32>,
34
35  // projection
36  projection3d                : Projection3d
37}
38
39/// The 3D projection which can be either perspective or orthographic
40#[derive(Clone, Debug, PartialEq)]
41pub struct Projection3d {
42  viewport_width  : u16,
43  viewport_height : u16,
44  inner           : Projection3dInner
45}
46
47/// Either a `Perspective` or `Orthographic` projection
48#[derive(Clone, Debug, PartialEq)]
49pub enum Projection3dInner {
50  Perspective {
51    /// Used to create the perspective projection matrix
52    perspective_fov : PerspectiveFov <f32>,
53    /// Constructed from the parameters in `perspective_fov` to transform
54    /// points in view space to 4D homogenous clip coordinates based on a
55    /// perspective projection
56    mat             : math::Matrix4        <f32>
57  },
58  Orthographic {
59    /// Constant factor to zoom in and out orthographically.
60    ///
61    /// The scale at `zoom == 1.0` is defined by the constant
62    /// `ORTHOGRAPHIC_PIXEL_SCALE`.
63    zoom            : f32,
64    /// Used to create the orthographic projection matrix
65    ortho           : vek::FrustumPlanes   <f32>,
66    /// Constructed from the parameters in `ortho` to transform points in view
67    /// space to 4D homogenous clip coordinates based on an orthographic
68    /// projection
69    mat             : math::Matrix4 <f32>
70  }
71}
72
73/// Parameters for a perspective projection based on vertical field-of-view
74/// angle
75#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct PerspectiveFov <S> {
77  pub fovy   : math::Rad <S>,
78  pub aspect : S,
79  pub near   : S,
80  pub far    : S
81}
82
83/// Computes floating point 'width/height' ratio from unsigned resolution
84/// input
85#[inline]
86pub fn aspect_ratio (viewport_width : u16, viewport_height : u16) -> f32 {
87  viewport_width as f32 / viewport_height as f32
88}
89
90/// Builds a 4x4 transformation matrix that will transform points in world
91/// space coordinates to view space coordinates for a given view position and
92/// orientation.
93///
94/// Note this accepts input such that the identity orientation defines the view
95/// looking down the *positive Y axis* with the 'up' vector defined by the
96/// *positive Z axis* and the constructed matrix will send points in front of
97/// the view to the *negative Z axis* and points above the view to the
98/// *positive Y axis*:
99///
100/// ```
101/// # extern crate gl_utils;
102/// # #[macro_use] extern crate math_utils as math;
103/// # fn main () {
104/// # use std::f32::consts::FRAC_PI_2;
105/// # use gl_utils::camera3d::transform_mat_world_to_view;
106/// use math::Point;
107/// use math::num::One;
108/// use math::approx::AbsDiffEq;
109/// let position      = math::Point3::origin();
110/// let orientation   = math::Rotation3::one();
111/// let transform_mat = transform_mat_world_to_view (&position, &orientation);
112/// assert_eq!(
113///  transform_mat,
114///  math::Matrix4::new (
115///    1.0,  0.0, 0.0, 0.0,
116///    0.0,  0.0, 1.0, 0.0,
117///    0.0, -1.0, 0.0, 0.0,
118///    0.0,  0.0, 0.0, 1.0)
119/// );
120/// let point = math::Point3::new (0.0, 10.0, 0.0);
121/// assert_eq!(
122///   math::Point3 (transform_mat.mul_point (point.0)),
123///   math::Point3::new (0.0, 0.0, -10.0)
124/// );
125/// let orientation   = math::Rotation3::from_angle_z (math::Rad (FRAC_PI_2));
126/// let transform_mat = transform_mat_world_to_view (&position, &orientation);
127/// let point = math::Point3::new (-10.0, 0.0, 0.0);
128/// math::approx::assert_relative_eq!(
129///   math::Point3 (transform_mat.mul_point (point.0)),
130///   math::Point3::new (0.0, 0.0, -10.0),
131///   epsilon = 10.0 * f32::default_epsilon()
132/// );
133/// # }
134/// ```
135
136pub fn transform_mat_world_to_view (
137  view_position    : &math::Point3 <f32>,
138  view_orientation : &math::Rotation3 <f32>
139) -> math::Matrix4 <f32> {
140  let eye    = view_position;
141  let target = *view_position + view_orientation.cols.y;
142  let up     = view_orientation.cols.z;
143  math::Matrix4::<f32>::look_at_rh (eye.0, target.0, up)
144}
145
146// private
147fn compute_ortho (viewport_width : u16, viewport_height : u16, zoom : f32)
148  -> vek::FrustumPlanes <f32>
149{
150  let half_scaled_width  = (0.5 * (viewport_width as f32  / zoom))
151    / ORTHOGRAPHIC_PIXEL_SCALE;
152  let half_scaled_height = (0.5 * (viewport_height as f32 / zoom))
153    / ORTHOGRAPHIC_PIXEL_SCALE;
154  vek::FrustumPlanes {
155    left:   -half_scaled_width,
156    right:   half_scaled_width,
157    bottom: -half_scaled_height,
158    top:     half_scaled_height,
159    near:    ORTHOGRAPHIC_NEAR_PLANE,
160    far:     ORTHOGRAPHIC_FAR_PLANE
161  }
162}
163
164impl Camera3d {
165  /// Create a new camera centered at the origin looking down the positive Y
166  /// axis with 'up' vector aligned with the Z axis.
167  #[inline]
168  pub fn new (viewport_width : u16, viewport_height : u16) -> Self {
169    Self::with_pose (
170      viewport_width, viewport_height, Default::default())
171  }
172
173  /// Create a new 3D camera with the given viewport, position, and
174  /// 3D pose
175  pub fn with_pose (
176    viewport_width  : u16,
177    viewport_height : u16,
178    pose            : math::Pose3 <f32>
179  ) -> Self {
180    // transform: world space -> view space
181    let orientation = pose.angles.into();
182    let transform_mat_world_to_view =
183      transform_mat_world_to_view (&pose.position, &orientation);
184
185    // projection: view space -> clip space
186    let projection3d = Projection3d::perspective (
187      viewport_width, viewport_height, PERSPECTIVE_INITIAL_FOVY.into());
188
189    Camera3d {
190      pose,
191      orientation,
192      transform_mat_world_to_view,
193      projection3d
194    }
195  }
196
197  pub const fn position (&self) -> math::Point3 <f32> {
198    self.pose.position
199  }
200
201  pub const fn yaw (&self) -> math::Rad <f32> {
202    self.pose.angles.yaw.angle()
203  }
204
205  pub const fn pitch (&self) -> math::Rad <f32> {
206    self.pose.angles.pitch.angle()
207  }
208
209  pub const fn roll (&self) -> math::Rad <f32> {
210    self.pose.angles.roll.angle()
211  }
212
213  pub const fn orientation (&self) -> math::Rotation3 <f32> {
214    self.orientation
215  }
216
217  pub const fn transform_mat_world_to_view (&self) -> math::Matrix4 <f32> {
218    self.transform_mat_world_to_view
219  }
220
221  pub const fn projection (&self) -> &Projection3d {
222    &self.projection3d
223  }
224
225  /// Should be called when the screen resolution changes.
226  #[inline]
227  pub fn set_viewport_dimensions (&mut self,
228    viewport_width : u16, viewport_height : u16
229  ) {
230    self.projection3d.set_viewport_dimensions (
231      viewport_width, viewport_height);
232  }
233
234  pub fn set_position (&mut self, position : math::Point3 <f32>) {
235    if self.pose.position != position {
236      self.pose.position = position;
237      self.compute_transform();
238    }
239  }
240
241  pub fn set_orientation (&mut self, orientation : math::Rotation3 <f32>) {
242    if self.orientation != orientation {
243      self.orientation = orientation;
244      self.compute_angles();
245    }
246  }
247
248  pub fn scale_fovy_or_zoom (&mut self, scale : f32) {
249    self.projection3d.scale_fovy_or_zoom (scale)
250  }
251
252  pub fn rotate (&mut self,
253    dyaw : math::Rad <f32>, dpitch : math::Rad <f32>, droll : math::Rad <f32>
254  ) {
255    use num::Zero;
256    self.pose.angles.yaw   += dyaw;
257    self.pose.angles.pitch += dpitch;
258    self.pose.angles.roll  += droll;
259    if !dyaw.is_zero() || !dpitch.is_zero() || !droll.is_zero() {
260      self.compute_orientation();
261    }
262  }
263
264  /// Moves the view position relative to the X/Y plane with X and Y direction
265  /// determined by heading and Z always aligned with the Z axis, i.e. the
266  /// translated position is only determined by the current *yaw* not the
267  /// pitch.
268  pub fn move_local_xy (&mut self, dx : f32, dy : f32, dz : f32) {
269    if dx != 0.0 || dy != 0.0 || dz != 0.0 {
270      let xy_basis  = math::Matrix3::rotation_z (self.pose.angles.yaw.angle().0);
271      self.pose.position +=
272        (dx * xy_basis.cols.x) + (dy * xy_basis.cols.y) + (dz * xy_basis.cols.z);
273      self.compute_transform();
274    }
275  }
276
277  /// Updates the camera pose to look at the given point.
278  ///
279  /// If the target point is equal to the camera position, then the identity
280  /// orientation is used.
281  pub fn look_at (&mut self, target : math::Point3 <f32>) {
282    let orientation =
283      math::Rotation3::look_at ((target - self.pose.position).into());
284    self.set_orientation (orientation);
285  }
286
287  /// Returns the raw *world to view transform* and *view to clip projection*
288  /// matrix data, suitable for use as shader uniforms.
289  #[inline]
290  pub fn view_mats (&self) -> ([[f32; 4]; 4], [[f32; 4]; 4]) {
291    ( self.transform_mat_world_to_view.into_col_arrays(),
292      self.projection3d.as_matrix().into_col_arrays()
293    )
294  }
295
296  /// Modify the projection to orthographic
297  pub fn to_orthographic (&mut self, zoom : f32) {
298    self.projection3d.to_orthographic (zoom)
299  }
300
301  /// Modify the projection to perspective
302  pub fn to_perspective (&mut self, fovy : math::Rad <f32>) {
303    self.projection3d.to_perspective (fovy)
304  }
305
306  #[inline]
307  fn compute_transform (&mut self) {
308    self.transform_mat_world_to_view =
309      transform_mat_world_to_view (&self.pose.position, &self.orientation);
310  }
311
312  /// Convenience method to compute orientation from current yaw, pitch, and
313  /// roll followed by updating the transform matrix
314  #[inline]
315  fn compute_orientation (&mut self) {
316    self.orientation = self.pose.angles.into();
317    self.compute_transform();
318  }
319
320  /// Convenience method to compute yaw, pitch, and roll from current
321  /// orientation matrix followed by updating the transform matrix
322  #[inline]
323  fn compute_angles (&mut self) {
324    self.pose.angles = self.orientation.into();
325    self.compute_transform();
326  }
327}
328
329impl Projection3d {
330
331  /// Create a new 3D perspective projection
332  ///
333  /// # Panics
334  ///
335  /// Viewport width or height is zero:
336  ///
337  /// ```should_panic
338  /// # extern crate gl_utils;
339  /// # extern crate math_utils as math;
340  /// # fn main () {
341  /// # use gl_utils::camera3d::Projection3d;
342  /// // panics:
343  /// let perspective = Projection3d::perspective (0, 240, math::Deg (90.0).into());
344  /// # }
345  /// ```
346  ///
347  /// If `fovy` is greater than or equal to $\pi$ radians:
348  ///
349  /// ```should_panic
350  /// # extern crate gl_utils;
351  /// # extern crate math_utils as math;
352  /// # fn main () {
353  /// # use gl_utils::camera3d::Projection3d;
354  /// // panics:
355  /// let perspective = Projection3d::perspective (320, 240, math::Rad (4.0));
356  /// # }
357  /// ```
358  ///
359  /// If `fovy` is less than or equal to $0.0$ radians:
360  ///
361  /// ```should_panic
362  /// # extern crate gl_utils;
363  /// # extern crate math_utils as math;
364  /// # fn main () {
365  /// # use gl_utils::camera3d::Projection3d;
366  /// // panics:
367  /// let perspective = Projection3d::perspective (320, 240, math::Rad (0.0));
368  /// # }
369  /// ```
370
371  pub fn perspective (
372    viewport_width : u16, viewport_height : u16, fovy : math::Rad <f32>
373  ) -> Self {
374    let inner
375      = Projection3dInner::perspective (viewport_width, viewport_height, fovy);
376    Projection3d { viewport_width, viewport_height, inner }
377  }
378
379  /// Create a new 3D orthographic projection
380  ///
381  /// # Panics
382  ///
383  /// Viewport width or height is zero:
384  ///
385  /// ```should_panic
386  /// # extern crate gl_utils;
387  /// # extern crate math_utils as math;
388  /// # fn main () {
389  /// # use gl_utils::camera3d::Projection3d;
390  /// // panics:
391  /// let perspective = Projection3d::perspective (0, 240, math::Deg (90.0).into());
392  /// # }
393  /// ```
394  ///
395  /// If `zoom` is less than or equal to $0.0$:
396  ///
397  /// ```should_panic
398  /// # extern crate gl_utils;
399  /// # fn main () {
400  /// # use gl_utils::camera3d::Projection3d;
401  /// let ortho = Projection3d::orthographic (320, 240, 0.0);  // panics
402  /// # }
403  /// ```
404
405  pub fn orthographic (viewport_width : u16, viewport_height : u16, zoom : f32)
406    -> Self
407  {
408    let inner
409      = Projection3dInner::orthographic (viewport_width, viewport_height, zoom);
410    Projection3d { viewport_width, viewport_height, inner }
411  }
412
413  pub const fn viewport_width (&self) -> u16 {
414    self.viewport_width
415  }
416  pub const fn viewport_height (&self) -> u16 {
417    self.viewport_height
418  }
419
420  /// Returns a reference to the underlying projection matrix
421  #[inline]
422  pub const fn as_matrix (&self) -> &math::Matrix4 <f32> {
423    self.inner.as_matrix()
424  }
425
426  pub const fn is_orthographic (&self) -> bool {
427    match self.inner {
428      Projection3dInner::Orthographic {..} => true,
429      Projection3dInner::Perspective  {..} => false
430    }
431  }
432
433  pub const fn is_perspective (&self) -> bool {
434    match self.inner {
435      Projection3dInner::Orthographic {..} => false,
436      Projection3dInner::Perspective  {..} => true
437    }
438  }
439
440  /// Converts the inner projection type to an `Orthographic` projection with
441  /// the given zoom; if already an orthographic projection this will modify
442  /// the zoom
443  pub fn to_orthographic (&mut self, zoom : f32) {
444    match self.inner {
445      ref mut inner@Projection3dInner::Orthographic { .. } => {
446        inner.set_orthographic_zoom (
447          self.viewport_width, self.viewport_height, zoom);
448      }
449      ref mut inner@Projection3dInner::Perspective  { .. } => {
450        *inner = Projection3dInner::orthographic (
451          self.viewport_width, self.viewport_height, 1.0);
452      }
453    }
454  }
455
456  /// Converts the inner projection type to a `Perspective` projection with the
457  /// given vertical FOV; if already a perspective projection this will modify
458  /// the FOV
459  pub fn to_perspective (&mut self, fovy : math::Rad <f32>) {
460    match self.inner {
461      ref mut inner@Projection3dInner::Orthographic { .. } => {
462        *inner = Projection3dInner::perspective (
463          self.viewport_width, self.viewport_height, fovy);
464      }
465      ref mut inner@Projection3dInner::Perspective  { .. } => {
466        inner.set_perspective_fovy (fovy);
467      }
468    }
469  }
470
471  /// Sets the current viewport dimensions
472  ///
473  /// # Panics
474  ///
475  /// Viewport width or height is zero:
476  ///
477  /// ```should_panic
478  /// # extern crate gl_utils;
479  /// # extern crate math_utils as math;
480  /// # fn main () {
481  /// # use gl_utils::camera3d::Projection3d;
482  /// let mut projection = Projection3d::perspective (320, 240, math::Deg (90.0).into());
483  /// projection.set_viewport_dimensions (0, 240);  // panics
484  /// # }
485  /// ```
486
487  pub fn set_viewport_dimensions (&mut self,
488    viewport_width : u16, viewport_height : u16
489  ) {
490    self.viewport_width  = viewport_width;
491    self.viewport_height = viewport_height;
492    self.inner.update_viewport_dimensions (viewport_width, viewport_height);
493  }
494
495  /// Multiply the current perspective vertical FOV or orthographic zoom by the
496  /// given scale factor.
497  ///
498  /// Will not increase vertical FOV greater than $\pi$ radians, nor will it
499  /// decrease the zoom or vertical FOV to zero.
500  ///
501  /// # Panics
502  ///
503  /// Panics if scale factor is zero or negative:
504  ///
505  /// ```should_panic
506  /// # extern crate gl_utils;
507  /// # extern crate math_utils as math;
508  /// # fn main () {
509  /// # use gl_utils::Camera3d;
510  /// # use math::EuclideanSpace;
511  /// let mut camera3d = Camera3d::new (320, 240);
512  /// camera3d.scale_fovy_or_zoom (-1.0);   // panics
513  /// # }
514  /// ```
515  pub fn scale_fovy_or_zoom (&mut self, scale : f32) {
516    assert!(0.0 < scale);
517    if scale != 1.0 {
518      use approx::AbsDiffEq;
519      match self.inner {
520        Projection3dInner::Perspective {
521          ref mut perspective_fov, ref mut mat
522        } => {
523          let max_fovy = math::Rad (
524            std::f32::consts::PI - f32::default_epsilon()
525          );
526          let min_fovy = math::Rad (f32::default_epsilon());
527          perspective_fov.fovy *= scale;
528          debug_assert!(math::Rad (0.0) <= perspective_fov.fovy);
529          if max_fovy < perspective_fov.fovy {
530            perspective_fov.fovy = max_fovy;
531          } else if perspective_fov.fovy < min_fovy {
532            perspective_fov.fovy = min_fovy;
533          }
534          *mat = graphics::projection_mat_perspective (perspective_fov);
535        }
536        Projection3dInner::Orthographic {
537          ref mut zoom, ref mut ortho, ref mut mat
538        } => {
539          *zoom *= scale;
540          debug_assert!(0.0 <= *zoom);
541          if *zoom < f32::default_epsilon() {
542            *zoom = f32::default_epsilon();
543          }
544          *ortho = compute_ortho (
545            self.viewport_width, self.viewport_height, *zoom);
546          *mat   = graphics::projection_mat_orthographic (ortho);
547        }
548      }
549    }
550  }
551
552}
553
554impl Projection3dInner {
555  fn perspective (
556    viewport_width : u16, viewport_height : u16, fovy : math::Rad <f32>
557  ) -> Self {
558    use approx::AbsDiffEq;
559    assert!(0   < viewport_width);
560    assert!(0   < viewport_height);
561    assert!(0.0 < fovy.0);
562    assert!(fovy.0 <= std::f32::consts::PI - f32::default_epsilon());
563    let perspective_fov = PerspectiveFov {
564        fovy,
565        aspect: aspect_ratio (viewport_width, viewport_height),
566        near:   PERSPECTIVE_NEAR_PLANE,
567        far:    PERSPECTIVE_FAR_PLANE
568    };
569    let mat = graphics::projection_mat_perspective (&perspective_fov);
570    Projection3dInner::Perspective { perspective_fov, mat }
571  }
572
573  fn orthographic (viewport_width : u16, viewport_height : u16, zoom : f32)
574    -> Self
575  {
576    assert!(0   < viewport_width);
577    assert!(0   < viewport_height);
578    assert!(0.0 < zoom);
579    let ortho = compute_ortho (viewport_width, viewport_height, zoom);
580    let mat   = graphics::projection_mat_orthographic (&ortho);
581    Projection3dInner::Orthographic { zoom, ortho, mat }
582  }
583
584  /// Returns a reference to the underlying matrix
585  const fn as_matrix (&self) -> &math::Matrix4 <f32> {
586    match self {
587      Projection3dInner::Perspective  { mat, .. } |
588      Projection3dInner::Orthographic { mat, .. } => mat
589    }
590  }
591
592  fn update_viewport_dimensions (&mut self,
593    viewport_width : u16, viewport_height : u16
594  ) {
595    assert!(0 < viewport_width);
596    assert!(0 < viewport_height);
597    match self {
598      Projection3dInner::Perspective  { perspective_fov, mat } => {
599        perspective_fov.aspect = aspect_ratio (viewport_width, viewport_height);
600        *mat = graphics::projection_mat_perspective (perspective_fov);
601      }
602      Projection3dInner::Orthographic { zoom, ortho, mat } => {
603        *ortho = compute_ortho (viewport_width, viewport_height, *zoom);
604        *mat   = graphics::projection_mat_orthographic (ortho);
605      }
606    }
607  }
608
609  fn set_perspective_fovy (&mut self, new_fovy : math::Rad <f32>) {
610    use approx::AbsDiffEq;
611    let max_fovy = math::Rad (std::f32::consts::PI - f32::default_epsilon());
612    assert!(f32::default_epsilon() < new_fovy.0);
613    assert!(new_fovy <= max_fovy);
614    match *self {
615      Projection3dInner::Perspective {
616        ref mut perspective_fov, ref mut mat
617      } => {
618        if perspective_fov.fovy != new_fovy {
619          perspective_fov.fovy = new_fovy;
620          *mat = graphics::projection_mat_perspective (perspective_fov);
621        }
622      }
623      Projection3dInner::Orthographic {..} =>
624        unreachable!("expected perspective projection")
625    }
626  }
627
628  fn set_orthographic_zoom (&mut self,
629    viewport_width : u16, viewport_height : u16, new_zoom : f32
630  ) {
631    use approx::AbsDiffEq;
632    assert!(f32::default_epsilon() < new_zoom);
633    match *self {
634      Projection3dInner::Orthographic {
635        ref mut zoom, ref mut ortho, ref mut mat
636      } => {
637        if *zoom != new_zoom {
638          *zoom  = new_zoom;
639          *ortho = compute_ortho (viewport_width, viewport_height, *zoom);
640          *mat   = graphics::projection_mat_orthographic (ortho);
641        }
642      }
643      Projection3dInner::Perspective {..} =>
644        unreachable!("expected orthographic projection")
645    }
646  }
647
648}
649
650#[cfg(test)]
651mod tests {
652  use super::*;
653  use math;
654  use approx;
655
656  #[test]
657  fn camera3d_look_at() {
658    use approx::AbsDiffEq;
659    let epsilon = 4.0 * f32::default_epsilon();
660    let mut camera = Camera3d::new (640, 480);
661    camera.look_at ([0.0, 1.0, 0.0].into());
662    approx::assert_relative_eq!(
663      camera.orientation().mat(),
664      math::Rotation3::identity().mat());
665    camera.look_at ([0.0, -1.0, 0.0].into());
666    approx::assert_relative_eq!(
667      camera.orientation().mat(),
668      math::Rotation3::from_angle_z (math::Turn (0.5).into()).mat(),
669      epsilon=epsilon);
670  }
671}