gl_utils/
camera3d.rs

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