nsys-gl-utils 0.11.4

OpenGL and graphics utilities
Documentation
//! Types and functions for 2D rendering.
//!
//! The `Camera2d` type represents a view positioned and oriented in 2D *world
//! space* defined as a left-hand^* coordinate system with a default scale of 1
//! unit == 1 pixel, with the origin at the center of the screen.
//!
//! ^*: "left-hand" in the sense of using the left hand with palm facing down
//! (away) and taking the thumb to be the first coordinate and index finger to
//! be the second coordinate
//!
//! The camera position and orientation defines the view space transform from
//! world coordinates to camera coordinates. This is represented as a transform
//! where the viewpoint is the x,y position of the camera and the z coordinate
//! is set to 0.0, looking down the negative z axis, and the "up" vector as the
//! y vector of the 2D camera orientation with the z component set to 0.0.
//!
//! The viewport dimensions and zoom factor defines the view space to clip space
//! orthographic projection. Because world space is defined with the origin in
//! the center of the screen, the frustum planes are +/- half screen widths,
//! scaled by the zoom factor.
//!
//! TODO: clarify coordinates when rendering into viewports that don't cover the
//! entire screen
//!
//! Two functions are provided to map between screen space and world space:
//! `Camera2d::screen_to_world` and `Camera2d::world_to_screen`.
//!
//! For information on tile-based rendering and tile coordinates, see the
//! [tile](crate::tile) module.

use math_utils as math;
use math_utils::vek;
use math_utils::num_traits as num;
use crate::graphics;

/// Represents a camera ("view") positioned and oriented in a 2D scene with a
/// 2D transformation and a 2D projection
#[derive(Clone, Debug, PartialEq)]
pub struct Camera2d {
  // transform
  /// Position in 2D world space
  position                    : math::Point2  <f32>,
  /// Yaw represents a counter-clockwise rotation restricted to the range
  /// $[-\pi, \pi]$.
  yaw                         : math::Rad <f32>,
  /// Basis derived from `yaw`.
  orientation                 : math::Rotation2 <f32>,
  /// Transforms points from 2D world space to 2D camera (view, eye) space as
  /// specified by the camera position and orientation.
  transform_mat_world_to_view : math::Matrix4 <f32>,

  // projection
  viewport_width              : u16,
  viewport_height             : u16,
  /// Determines the extent of the view represented in the `ortho` structure
  zoom                        : f32,
  /// Used to create the ortho projection matrix
  ortho                       : vek::FrustumPlanes <f32>,
  /// Constructed from the parameters in `ortho` to transform points in 2D view
  /// space to 4D homogenous clip coordinates.
  projection_mat_ortho        : math::Matrix4 <f32>
}

impl Camera2d {
  /// Create a new camera centered at the origin looking down the positive Y
  /// axis with 'up' vector aligned with the Z axis.
  ///
  /// The orthographic projection is defined such that at the initial zoom
  /// level of 1.0, one unit in world space is one *pixel*, so the points in
  /// the bounding rectangle defined by minimum point `(-half_screen_width+1,
  /// -half_screen_height+1)` and maximum point `(half_screen_width,
  /// half_screen_height)` are visible.
  pub fn new (viewport_width : u16, viewport_height : u16) -> Self {
    use math::Point;
    use num::One;
    // transform: world space -> view space
    let position    = math::Point2::origin();
    let yaw         = math::Rad (0.0);
    let orientation = math::Rotation2::one();
    let transform_mat_world_to_view =
      transform_mat_world_to_view (position, orientation);

    // projection: view space -> clip space
    let zoom  = 1.0;
    let ortho = Self::ortho_from_viewport_zoom (
      viewport_width, viewport_height, zoom);
    let projection_mat_ortho = graphics::projection_mat_orthographic (&ortho);

    Camera2d {
      position,
      yaw,
      orientation,
      transform_mat_world_to_view,
      viewport_width,
      viewport_height,
      zoom,
      ortho,
      projection_mat_ortho
    }
  }

  pub fn yaw (&self) -> math::Rad <f32> {
    self.yaw
  }
  pub fn position (&self) -> math::Point2 <f32> {
    self.position
  }
  pub fn orientation (&self) -> math::Rotation2 <f32> {
    self.orientation
  }
  pub fn viewport_width (&self) -> u16 {
    self.viewport_width
  }
  pub fn viewport_height (&self) -> u16 {
    self.viewport_height
  }
  pub fn transform_mat_world_to_view (&self) -> math::Matrix4 <f32> {
    self.transform_mat_world_to_view
  }
  pub fn zoom (&self) -> f32 {
    self.zoom
  }
  pub fn ortho (&self) -> vek::FrustumPlanes <f32> {
    self.ortho
  }
  pub fn projection_mat_ortho (&self) -> math::Matrix4 <f32> {
    self.projection_mat_ortho
  }

  /// Convert a screen space coordinate to world space based on the current
  /// view.
  ///
  /// ```
  /// # use gl_utils::Camera2d;
  /// let [width, height] = [320, 240];
  /// let mut camera2d = Camera2d::new (width, height);
  /// let screen_coord = [width as f32 / 2.0, height as f32 / 2.0].into();
  /// assert_eq!(camera2d.screen_to_world (screen_coord).0, [0.0, 0.0].into());
  /// ```
  pub fn screen_to_world (&self, screen_coord : math::Point2 <f32>)
    -> math::Point2 <f32>
  {
    let screen_dimensions = [self.viewport_width, self.viewport_height].into();
    let ndc_2d_coord =
      graphics::screen_2d_to_ndc_2d (screen_dimensions, screen_coord);
    let ndc_coord    = ndc_2d_coord.0.with_z (0.0).with_w (1.0);
    let view_coord   = self.projection_mat_ortho().transposed() * ndc_coord;
    let world_coord  =
      self.transform_mat_world_to_view().transposed() * view_coord;
    world_coord.xy().into()
  }

  /// Convert a world space coordinate to screen space based on the current
  /// view.
  ///
  /// ```
  /// # use gl_utils::Camera2d;
  /// # use math_utils as math;
  /// let [width, height] = [320, 240];
  /// let mut camera2d = Camera2d::new (width, height);
  /// let world_coord  = math::Point::origin();
  /// assert_eq!(camera2d.world_to_screen (world_coord).0,
  ///   [width as f32 / 2.0, height as f32 / 2.0].into());
  /// ```
  pub fn world_to_screen (&self, world_coord : math::Point2 <f32>)
    -> math::Point2 <f32>
  {
    let screen_dimensions = [self.viewport_width, self.viewport_height].into();
    let world_4d_coord    = world_coord.0.with_z (0.0).with_w (1.0);
    let view_coord        = self.transform_mat_world_to_view() * world_4d_coord;
    let clip_coord        = self.projection_mat_ortho() * view_coord;
    let ndc_coord         = clip_coord.xy().into();
    graphics::ndc_2d_to_screen_2d (screen_dimensions, ndc_coord)
  }

  /// Should be called when the screen resolution changes to update the
  /// orthographic projection state.
  ///
  /// # Panics
  ///
  /// Panics if the viewport width or height are zero:
  ///
  /// ```should_panic
  /// # use gl_utils::Camera2d;
  /// let mut camera2d = Camera2d::new (320, 240);
  /// camera2d.set_viewport_dimensions (0, 0); // panics
  /// ```
  pub fn set_viewport_dimensions (&mut self,
    viewport_width : u16, viewport_height : u16
  ) {
    assert!(0 < viewport_width);
    assert!(0 < viewport_height);
    self.viewport_width  = viewport_width;
    self.viewport_height = viewport_height;
    self.compute_ortho();
  }

  pub fn set_position (&mut self, position : math::Point2 <f32>) {
    if self.position != position {
      self.position = position;
      self.compute_transform();
    }
  }

  /// Set the zoom level.
  ///
  /// # Panics
  ///
  /// Panics if scale factor is zero or negative:
  ///
  /// ```should_panic
  /// # extern crate gl_utils;
  /// # fn main () {
  /// # use gl_utils::Camera2d;
  /// let mut camera2d = Camera2d::new (320, 240);
  /// camera2d.set_zoom (-1.0);   // panics
  /// # }
  /// ```
  pub fn set_zoom (&mut self, zoom : f32) {
    assert!(0.0 < zoom);
    if self.zoom != zoom {
      self.zoom = zoom;
      self.compute_ortho();
    }
  }

  pub fn rotate (&mut self, delta_yaw : math::Rad <f32>) {
    use std::f32::consts::PI;
    use num::Zero;
    use math::Angle;
    if !delta_yaw.is_zero() {
      self.yaw += delta_yaw;
      if self.yaw < math::Rad (-PI) {
        self.yaw += math::Rad::full_turn();
      } else if self.yaw > math::Rad (PI) {
        self.yaw -= math::Rad::full_turn();
      }
      self.compute_orientation();
    }
  }

  /// Move by delta X and Y values in local coordinates
  pub fn move_local (&mut self, delta_x : f32, delta_y : f32) {
    self.position += (delta_x * (*self.orientation).cols.x)
        + (delta_y * (*self.orientation).cols.y);
    self.compute_transform();
  }

  /// Move camera so that the world-space origin will be centered on the lower
  /// left pixel
  pub fn move_origin_to_bottom_left (&mut self) {
    self.position = [
      (self.viewport_width  / 2) as f32,
      (self.viewport_height / 2) as f32
    ].into();
    self.compute_transform();
  }

  /// Multiply the current zoom by the given scale factor.
  ///
  /// # Panics
  ///
  /// Panics if scale factor is zero or negative:
  ///
  /// ```should_panic
  /// # extern crate gl_utils;
  /// # fn main () {
  /// # use gl_utils::Camera2d;
  /// let mut camera2d = Camera2d::new (320, 240);
  /// camera2d.scale_zoom (-1.0);   // panics
  /// # }
  /// ```
  pub fn scale_zoom (&mut self, scale : f32) {
    assert!(0.0 < scale);
    self.zoom *= scale;
    self.compute_ortho();
  }

  /// Returns the raw *world to view transform* and *ortho projection* matrix
  /// data, suitable for use as shader uniforms.
  #[inline]
  pub fn view_ortho_mats (&self) -> ([[f32; 4]; 4], [[f32; 4]; 4]) {
    ( self.transform_mat_world_to_view.into_col_arrays(),
      self.projection_mat_ortho.into_col_arrays()
    )
  }

  //
  //  private
  //

  #[inline]
  fn compute_orientation (&mut self) {
    self.orientation = math::Rotation2::from_angle (self.yaw);
    self.compute_transform();
  }
  #[inline]
  fn compute_transform (&mut self) {
    self.transform_mat_world_to_view =
      transform_mat_world_to_view (self.position, self.orientation);
  }
  /// Recomputes the ortho structure based on current viewport and zoom.
  fn compute_ortho (&mut self) {
    self.ortho = Self::ortho_from_viewport_zoom (
      self.viewport_width, self.viewport_height, self.zoom);
    self.compute_projection();
  }
  #[inline]
  fn compute_projection (&mut self) {
    self.projection_mat_ortho =
      graphics::projection_mat_orthographic (&self.ortho);
  }

  /// Rounds the viewport to the next lower even resolution which will
  /// cause 1px distortion but prevent mis-sampling of 2D textures at
  /// non-power-of-two zoom levels
  pub (crate) fn ortho_from_viewport_zoom (
    viewport_width : u16, viewport_height : u16, zoom : f32
  ) -> vek::FrustumPlanes <f32> {
    let half_scaled_width  = 0.5 * (
      (viewport_width  - viewport_width  % 2) as f32 / zoom);
    let half_scaled_height = 0.5 * (
      (viewport_height - viewport_height % 2) as f32 / zoom);
    vek::FrustumPlanes {
      left:   -half_scaled_width,
      right:   half_scaled_width,
      bottom: -half_scaled_height,
      top:     half_scaled_height,
      near:   -1.0,
      far:     1.0
    }
  }
}

/// Builds a 4x4 transformation matrix that will transform points in world
/// space coordinates to view space coordinates based on the current 2D view
/// position and orientation.
///
/// The Z coordinate of the position is always `0.0` and the view is always
/// looking down the negative Z axis.
// TODO: doctest ?
pub fn transform_mat_world_to_view (
  view_position    : math::Point2 <f32>,
  view_orientation : math::Rotation2 <f32>
) -> math::Matrix4 <f32> {
  let eye    = view_position.0.with_z (0.0).into();
  let center = eye - math::Vector3::unit_z();
  let up     = view_orientation.cols.y.with_z (0.0);
  math::Matrix4::<f32>::look_at_rh (eye, center, up)
}