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 = Projection3dInner::perspective (viewport_width, viewport_height, fovy);
375    Projection3d { viewport_width, viewport_height, inner }
376  }
377
378  /// Create a new 3D orthographic projection
379  ///
380  /// # Panics
381  ///
382  /// Viewport width or height is zero:
383  ///
384  /// ```should_panic
385  /// # extern crate gl_utils;
386  /// # extern crate math_utils as math;
387  /// # fn main () {
388  /// # use gl_utils::camera3d::Projection3d;
389  /// // panics:
390  /// let perspective = Projection3d::perspective (0, 240, math::Deg (90.0).into());
391  /// # }
392  /// ```
393  ///
394  /// If `zoom` is less than or equal to `0.0`:
395  ///
396  /// ```should_panic
397  /// # extern crate gl_utils;
398  /// # fn main () {
399  /// # use gl_utils::camera3d::Projection3d;
400  /// let ortho = Projection3d::orthographic (320, 240, 0.0);  // panics
401  /// # }
402  /// ```
403
404  pub fn orthographic (viewport_width : u16, viewport_height : u16, zoom : f32) -> Self {
405    let inner = Projection3dInner::orthographic (viewport_width, viewport_height, zoom);
406    Projection3d { viewport_width, viewport_height, inner }
407  }
408
409  pub const fn viewport_width (&self) -> u16 {
410    self.viewport_width
411  }
412  pub const fn viewport_height (&self) -> u16 {
413    self.viewport_height
414  }
415
416  /// Returns a reference to the underlying projection matrix
417  #[inline]
418  pub const fn as_matrix (&self) -> &math::Matrix4 <f32> {
419    self.inner.as_matrix()
420  }
421
422  pub const fn is_orthographic (&self) -> bool {
423    match self.inner {
424      Projection3dInner::Orthographic {..} => true,
425      Projection3dInner::Perspective  {..} => false
426    }
427  }
428
429  pub const fn is_perspective (&self) -> bool {
430    match self.inner {
431      Projection3dInner::Orthographic {..} => false,
432      Projection3dInner::Perspective  {..} => true
433    }
434  }
435
436  /// Converts the inner projection type to an `Orthographic` projection with
437  /// the given zoom; if already an orthographic projection this will modify
438  /// the zoom
439  pub fn to_orthographic (&mut self, zoom : f32) {
440    match self.inner {
441      ref mut inner@Projection3dInner::Orthographic { .. } => {
442        inner.set_orthographic_zoom (
443          self.viewport_width, self.viewport_height, zoom);
444      }
445      ref mut inner@Projection3dInner::Perspective  { .. } => {
446        *inner = Projection3dInner::orthographic (
447          self.viewport_width, self.viewport_height, 1.0);
448      }
449    }
450  }
451
452  /// Converts the inner projection type to a `Perspective` projection with the
453  /// given vertical FOV; if already a perspective projection this will modify
454  /// the FOV
455  pub fn to_perspective (&mut self, fovy : math::Rad <f32>) {
456    match self.inner {
457      ref mut inner@Projection3dInner::Orthographic { .. } => {
458        *inner = Projection3dInner::perspective (
459          self.viewport_width, self.viewport_height, fovy);
460      }
461      ref mut inner@Projection3dInner::Perspective  { .. } => {
462        inner.set_perspective_fovy (fovy);
463      }
464    }
465  }
466
467  /// Sets the current viewport dimensions
468  ///
469  /// # Panics
470  ///
471  /// Viewport width or height is zero:
472  ///
473  /// ```should_panic
474  /// # extern crate gl_utils;
475  /// # extern crate math_utils as math;
476  /// # fn main () {
477  /// # use gl_utils::camera3d::Projection3d;
478  /// let mut projection = Projection3d::perspective (320, 240, math::Deg (90.0).into());
479  /// projection.set_viewport_dimensions (0, 240);  // panics
480  /// # }
481  /// ```
482
483  pub fn set_viewport_dimensions (&mut self,
484    viewport_width : u16, viewport_height : u16
485  ) {
486    self.viewport_width  = viewport_width;
487    self.viewport_height = viewport_height;
488    self.inner.update_viewport_dimensions (viewport_width, viewport_height);
489  }
490
491  /// Multiply the current perspective vertical FOV or orthographic zoom by the
492  /// given scale factor.
493  ///
494  /// Will not increase vertical FOV greater than $\pi$ radians, nor will it
495  /// decrease the zoom or vertical FOV to zero.
496  ///
497  /// # Panics
498  ///
499  /// Panics if scale factor is zero or negative:
500  ///
501  /// ```should_panic
502  /// # extern crate gl_utils;
503  /// # extern crate math_utils as math;
504  /// # fn main () {
505  /// # use gl_utils::Camera3d;
506  /// # use math::EuclideanSpace;
507  /// let mut camera3d = Camera3d::new (320, 240);
508  /// camera3d.scale_fovy_or_zoom (-1.0);   // panics
509  /// # }
510  /// ```
511  pub fn scale_fovy_or_zoom (&mut self, scale : f32) {
512    assert!(0.0 < scale);
513    if scale != 1.0 {
514      use approx::AbsDiffEq;
515      match self.inner {
516        Projection3dInner::Perspective {
517          ref mut perspective_fov, ref mut mat
518        } => {
519          let max_fovy = math::Rad (
520            std::f32::consts::PI - f32::default_epsilon()
521          );
522          let min_fovy = math::Rad (f32::default_epsilon());
523          perspective_fov.fovy *= scale;
524          debug_assert!(math::Rad (0.0) <= perspective_fov.fovy);
525          if max_fovy < perspective_fov.fovy {
526            perspective_fov.fovy = max_fovy;
527          } else if perspective_fov.fovy < min_fovy {
528            perspective_fov.fovy = min_fovy;
529          }
530          *mat = graphics::projection_mat_perspective (perspective_fov);
531        }
532        Projection3dInner::Orthographic {
533          ref mut zoom, ref mut ortho, ref mut mat
534        } => {
535          *zoom *= scale;
536          debug_assert!(0.0 <= *zoom);
537          if *zoom < f32::default_epsilon() {
538            *zoom = f32::default_epsilon();
539          }
540          *ortho = compute_ortho (
541            self.viewport_width, self.viewport_height, *zoom);
542          *mat   = graphics::projection_mat_orthographic (ortho);
543        }
544      }
545    }
546  }
547}
548
549impl Projection3dInner {
550  fn perspective (
551    viewport_width : u16, viewport_height : u16, fovy : math::Rad <f32>
552  ) -> Self {
553    use approx::AbsDiffEq;
554    assert!(0   < viewport_width);
555    assert!(0   < viewport_height);
556    assert!(0.0 < fovy.0);
557    assert!(fovy.0 <= std::f32::consts::PI - f32::default_epsilon());
558    let perspective_fov = PerspectiveFov {
559        fovy,
560        aspect: aspect_ratio (viewport_width, viewport_height),
561        near:   PERSPECTIVE_NEAR_PLANE,
562        far:    PERSPECTIVE_FAR_PLANE
563    };
564    let mat = graphics::projection_mat_perspective (&perspective_fov);
565    Projection3dInner::Perspective { perspective_fov, mat }
566  }
567
568  fn orthographic (viewport_width : u16, viewport_height : u16, zoom : f32)
569    -> Self
570  {
571    assert!(0   < viewport_width);
572    assert!(0   < viewport_height);
573    assert!(0.0 < zoom);
574    let ortho = compute_ortho (viewport_width, viewport_height, zoom);
575    let mat   = graphics::projection_mat_orthographic (&ortho);
576    Projection3dInner::Orthographic { zoom, ortho, mat }
577  }
578
579  /// Returns a reference to the underlying matrix
580  const fn as_matrix (&self) -> &math::Matrix4 <f32> {
581    match self {
582      Projection3dInner::Perspective  { mat, .. } |
583      Projection3dInner::Orthographic { mat, .. } => mat
584    }
585  }
586
587  fn update_viewport_dimensions (&mut self,
588    viewport_width : u16, viewport_height : u16
589  ) {
590    assert!(0 < viewport_width);
591    assert!(0 < viewport_height);
592    match self {
593      Projection3dInner::Perspective  { perspective_fov, mat } => {
594        perspective_fov.aspect = aspect_ratio (viewport_width, viewport_height);
595        *mat = graphics::projection_mat_perspective (perspective_fov);
596      }
597      Projection3dInner::Orthographic { zoom, ortho, mat } => {
598        *ortho = compute_ortho (viewport_width, viewport_height, *zoom);
599        *mat   = graphics::projection_mat_orthographic (ortho);
600      }
601    }
602  }
603
604  fn set_perspective_fovy (&mut self, new_fovy : math::Rad <f32>) {
605    use approx::AbsDiffEq;
606    let max_fovy = math::Rad (std::f32::consts::PI - f32::default_epsilon());
607    assert!(f32::default_epsilon() < new_fovy.0);
608    assert!(new_fovy <= max_fovy);
609    match *self {
610      Projection3dInner::Perspective {
611        ref mut perspective_fov, ref mut mat
612      } => {
613        if perspective_fov.fovy != new_fovy {
614          perspective_fov.fovy = new_fovy;
615          *mat = graphics::projection_mat_perspective (perspective_fov);
616        }
617      }
618      Projection3dInner::Orthographic {..} =>
619        unreachable!("expected perspective projection")
620    }
621  }
622
623  fn set_orthographic_zoom (&mut self,
624    viewport_width : u16, viewport_height : u16, new_zoom : f32
625  ) {
626    use approx::AbsDiffEq;
627    assert!(f32::default_epsilon() < new_zoom);
628    match *self {
629      Projection3dInner::Orthographic {
630        ref mut zoom, ref mut ortho, ref mut mat
631      } => {
632        if *zoom != new_zoom {
633          *zoom  = new_zoom;
634          *ortho = compute_ortho (viewport_width, viewport_height, *zoom);
635          *mat   = graphics::projection_mat_orthographic (ortho);
636        }
637      }
638      Projection3dInner::Perspective {..} =>
639        unreachable!("expected orthographic projection")
640    }
641  }
642
643}
644
645#[cfg(test)]
646mod tests {
647  use super::*;
648  use math;
649  use approx;
650
651  #[test]
652  fn camera3d_look_at() {
653    use approx::AbsDiffEq;
654    let epsilon = 4.0 * f32::default_epsilon();
655    let mut camera = Camera3d::new (640, 480);
656    camera.look_at ([0.0, 1.0, 0.0].into());
657    approx::assert_relative_eq!(
658      camera.orientation().mat(),
659      math::Rotation3::identity().mat());
660    camera.look_at ([0.0, -1.0, 0.0].into());
661    approx::assert_relative_eq!(
662      camera.orientation().mat(),
663      math::Rotation3::from_angle_z (math::Turn (0.5).into()).mat(),
664      epsilon=epsilon);
665  }
666}