Skip to main content

gl_utils/
camera2d.rs

1//! Types and functions for 2D rendering.
2//!
3//! The `Camera2d` type represents a view positioned and oriented in 2D *world space*
4//! defined as a left-hand^* coordinate system with a default scale of 1 unit == 1
5//! pixel, with the origin at the center of the screen.
6//!
7//! ^*: "left-hand" in the sense of using the left hand with palm facing down (away) and
8//! taking the thumb to be the first coordinate and index finger to be the second
9//! coordinate
10//!
11//! The camera position and orientation defines the view space transform from world
12//! coordinates to camera coordinates. This is represented as a transform where the
13//! viewpoint is the x,y position of the camera and the z coordinate is set to 0.0,
14//! looking down the negative z axis, and the "up" vector as the y vector of the 2D
15//! camera orientation with the z component set to 0.0.
16//!
17//! The viewport dimensions and zoom factor defines the view space to clip space
18//! orthographic projection. Because world space is defined with the origin in the
19//! center of the screen, the frustum planes are +/- half screen widths, scaled by the
20//! zoom factor.
21//!
22//! TODO: clarify coordinates when rendering into viewports that don't cover the entire
23//! screen
24//!
25//! Two functions are provided to map between screen space and world space:
26//! `Camera2d::screen_to_world` and `Camera2d::world_to_screen`.
27//!
28//! For information on tile-based rendering and tile coordinates, see the
29//! [tile](crate::tile) module.
30
31use math_utils as math;
32use math_utils::{num, vek};
33use crate::graphics;
34
35/// Represents a camera ("view") positioned and oriented in a 2D scene with a 2D
36/// transformation and a 2D projection
37#[derive(Clone, Debug, PartialEq)]
38pub struct Camera2d {
39  // transform
40  /// Position in 2D world space
41  position                    : math::Point2  <f32>,
42  /// Yaw represents a counter-clockwise rotation restricted to the range $[-\pi, \pi]$.
43  yaw                         : math::Rad <f32>,
44  /// Basis derived from `yaw`.
45  orientation                 : math::Rotation2 <f32>,
46  /// Transforms points from 2D world space to 2D camera (view, eye) space as specified
47  /// by the camera position and orientation.
48  transform_mat_world_to_view : math::Matrix4 <f32>,
49
50  // projection
51  viewport_width              : u16,
52  viewport_height             : u16,
53  /// Determines the extent of the view represented in the `ortho` structure
54  zoom                        : f32,
55  /// Used to create the ortho projection matrix
56  ortho                       : vek::FrustumPlanes <f32>,
57  /// Constructed from the parameters in `ortho` to transform points in 2D view space to
58  /// 4D homogenous clip coordinates.
59  projection_mat_ortho        : math::Matrix4 <f32>
60}
61
62impl Camera2d {
63  /// Create a new camera centered at the origin looking down the positive Y axis with
64  /// 'up' vector aligned with the Z axis.
65  ///
66  /// The orthographic projection is defined such that at the initial zoom level of 1.0,
67  /// one unit in world space is one *pixel*, so the points in the bounding rectangle
68  /// defined by minimum point `(-half_screen_width+1, -half_screen_height+1)` and
69  /// maximum point `(half_screen_width, half_screen_height)` are visible.
70  pub fn new (viewport_width : u16, viewport_height : u16) -> Self {
71    use math::Point;
72    use num::One;
73    // transform: world space -> view space
74    let position    = math::Point2::origin();
75    let yaw         = math::Rad (0.0);
76    let orientation = math::Rotation2::one();
77    let transform_mat_world_to_view =
78      transform_mat_world_to_view (position, orientation);
79
80    // projection: view space -> clip space
81    let zoom  = 1.0;
82    let ortho = Self::ortho_from_viewport_zoom (viewport_width, viewport_height, zoom);
83    let projection_mat_ortho = graphics::projection_mat_orthographic (&ortho);
84
85    Camera2d {
86      position,
87      yaw,
88      orientation,
89      transform_mat_world_to_view,
90      viewport_width,
91      viewport_height,
92      zoom,
93      ortho,
94      projection_mat_ortho
95    }
96  }
97
98  pub const fn yaw (&self) -> math::Rad <f32> {
99    self.yaw
100  }
101  pub const fn position (&self) -> math::Point2 <f32> {
102    self.position
103  }
104  pub const fn orientation (&self) -> math::Rotation2 <f32> {
105    self.orientation
106  }
107  pub const fn viewport_width (&self) -> u16 {
108    self.viewport_width
109  }
110  pub const fn viewport_height (&self) -> u16 {
111    self.viewport_height
112  }
113  pub const fn transform_mat_world_to_view (&self) -> math::Matrix4 <f32> {
114    self.transform_mat_world_to_view
115  }
116  pub const fn zoom (&self) -> f32 {
117    self.zoom
118  }
119  pub const fn ortho (&self) -> vek::FrustumPlanes <f32> {
120    self.ortho
121  }
122  pub const fn projection_mat_ortho (&self) -> math::Matrix4 <f32> {
123    self.projection_mat_ortho
124  }
125
126  /// Convert a screen space coordinate to world space based on the current view.
127  ///
128  /// ```
129  /// # use gl_utils::Camera2d;
130  /// let [width, height] = [320, 240];
131  /// let mut camera2d = Camera2d::new (width, height);
132  /// let screen_coord = [width as f32 / 2.0, height as f32 / 2.0].into();
133  /// assert_eq!(camera2d.screen_to_world (screen_coord).0, [0.0, 0.0].into());
134  /// ```
135  pub fn screen_to_world (&self, screen_coord : math::Point2 <f32>)
136    -> math::Point2 <f32>
137  {
138    let screen_dimensions = [self.viewport_width, self.viewport_height].into();
139    let ndc_2d_coord = graphics::screen_2d_to_ndc_2d (screen_dimensions, screen_coord);
140    let ndc_coord    = ndc_2d_coord.0.with_z (0.0).with_w (1.0);
141    let view_coord   = self.projection_mat_ortho().transposed() * ndc_coord;
142    let world_coord  = self.transform_mat_world_to_view().transposed() * view_coord;
143    world_coord.xy().into()
144  }
145
146  /// Convert a world space coordinate to screen space based on the current view.
147  ///
148  /// ```
149  /// # use gl_utils::Camera2d;
150  /// # use math_utils as math;
151  /// let [width, height] = [320, 240];
152  /// let mut camera2d = Camera2d::new (width, height);
153  /// let world_coord  = math::Point::origin();
154  /// assert_eq!(camera2d.world_to_screen (world_coord).0,
155  ///   [width as f32 / 2.0, height as f32 / 2.0].into());
156  /// ```
157  pub fn world_to_screen (&self, world_coord : math::Point2 <f32>)
158    -> math::Point2 <f32>
159  {
160    let screen_dimensions = [self.viewport_width, self.viewport_height].into();
161    let world_4d_coord    = world_coord.0.with_z (0.0).with_w (1.0);
162    let view_coord        = self.transform_mat_world_to_view() * world_4d_coord;
163    let clip_coord        = self.projection_mat_ortho() * view_coord;
164    let ndc_coord         = clip_coord.xy().into();
165    graphics::ndc_2d_to_screen_2d (screen_dimensions, ndc_coord)
166  }
167
168  /// Should be called when the screen resolution changes to update the orthographic
169  /// projection state.
170  ///
171  /// # Panics
172  ///
173  /// Panics if the viewport width or height are zero:
174  ///
175  /// ```should_panic
176  /// # use gl_utils::Camera2d;
177  /// let mut camera2d = Camera2d::new (320, 240);
178  /// camera2d.set_viewport_dimensions (0, 0); // panics
179  /// ```
180  pub fn set_viewport_dimensions (&mut self,
181    viewport_width : u16, viewport_height : u16
182  ) {
183    assert!(0 < viewport_width);
184    assert!(0 < viewport_height);
185    self.viewport_width  = viewport_width;
186    self.viewport_height = viewport_height;
187    self.compute_ortho();
188  }
189
190  pub fn set_position (&mut self, position : math::Point2 <f32>) {
191    if self.position != position {
192      self.position = position;
193      self.compute_transform();
194    }
195  }
196
197  /// Set the zoom level.
198  ///
199  /// # Panics
200  ///
201  /// Panics if scale factor is zero or negative:
202  ///
203  /// ```should_panic
204  /// # extern crate gl_utils;
205  /// # fn main () {
206  /// # use gl_utils::Camera2d;
207  /// let mut camera2d = Camera2d::new (320, 240);
208  /// camera2d.set_zoom (-1.0);   // panics
209  /// # }
210  /// ```
211  pub fn set_zoom (&mut self, zoom : f32) {
212    assert!(0.0 < zoom);
213    if self.zoom != zoom {
214      self.zoom = zoom;
215      self.compute_ortho();
216    }
217  }
218
219  pub fn rotate (&mut self, delta_yaw : math::Rad <f32>) {
220    use std::f32::consts::PI;
221    use num::Zero;
222    use math::Angle;
223    if !delta_yaw.is_zero() {
224      self.yaw += delta_yaw;
225      if self.yaw < math::Rad (-PI) {
226        self.yaw += math::Rad::full_turn();
227      } else if self.yaw > math::Rad (PI) {
228        self.yaw -= math::Rad::full_turn();
229      }
230      self.compute_orientation();
231    }
232  }
233
234  /// Move by delta X and Y values in local coordinates
235  pub fn move_local (&mut self, delta_x : f32, delta_y : f32) {
236    self.position += (delta_x * self.orientation.cols.x)
237      + (delta_y * self.orientation.cols.y);
238    self.compute_transform();
239  }
240
241  /// Move camera so that the world-space origin will be centered on the lower left
242  /// pixel
243  pub fn move_origin_to_bottom_left (&mut self) {
244    self.position = [
245      (self.viewport_width  / 2) as f32,
246      (self.viewport_height / 2) as f32
247    ].into();
248    self.compute_transform();
249  }
250
251  /// Multiply the current zoom by the given scale factor.
252  ///
253  /// # Panics
254  ///
255  /// Panics if scale factor is zero or negative:
256  ///
257  /// ```should_panic
258  /// # extern crate gl_utils;
259  /// # fn main () {
260  /// # use gl_utils::Camera2d;
261  /// let mut camera2d = Camera2d::new (320, 240);
262  /// camera2d.scale_zoom (-1.0);   // panics
263  /// # }
264  /// ```
265  pub fn scale_zoom (&mut self, scale : f32) {
266    assert!(0.0 < scale);
267    self.zoom *= scale;
268    self.compute_ortho();
269  }
270
271  /// Returns the raw *world to view transform* and *ortho projection* matrix data,
272  /// suitable for use as shader uniforms.
273  #[inline]
274  pub fn view_ortho_mats (&self) -> ([[f32; 4]; 4], [[f32; 4]; 4]) {
275    ( self.transform_mat_world_to_view.into_col_arrays(),
276      self.projection_mat_ortho.into_col_arrays()
277    )
278  }
279
280  //
281  //  private
282  //
283
284  #[inline]
285  fn compute_orientation (&mut self) {
286    self.orientation = math::Rotation2::from_angle (self.yaw);
287    self.compute_transform();
288  }
289  #[inline]
290  fn compute_transform (&mut self) {
291    self.transform_mat_world_to_view =
292      transform_mat_world_to_view (self.position, self.orientation);
293  }
294  /// Recomputes the ortho structure based on current viewport and zoom.
295  fn compute_ortho (&mut self) {
296    self.ortho = Self::ortho_from_viewport_zoom (
297      self.viewport_width, self.viewport_height, self.zoom);
298    self.compute_projection();
299  }
300  #[inline]
301  fn compute_projection (&mut self) {
302    self.projection_mat_ortho = graphics::projection_mat_orthographic (&self.ortho);
303  }
304
305  /// Rounds the viewport to the next lower even resolution which will cause 1px
306  /// distortion but prevent mis-sampling of 2D textures at non-power-of-two zoom levels
307  pub (crate) fn ortho_from_viewport_zoom (
308    viewport_width : u16, viewport_height : u16, zoom : f32
309  ) -> vek::FrustumPlanes <f32> {
310    let half_scaled_width  =
311      0.5 * ((viewport_width  - viewport_width  % 2) as f32 / zoom);
312    let half_scaled_height =
313      0.5 * ((viewport_height - viewport_height % 2) as f32 / zoom);
314    vek::FrustumPlanes {
315      left:   -half_scaled_width,
316      right:   half_scaled_width,
317      bottom: -half_scaled_height,
318      top:     half_scaled_height,
319      near:   -1.0,
320      far:     1.0
321    }
322  }
323}
324
325/// Builds a 4x4 transformation matrix that will transform points in world space
326/// coordinates to view space coordinates based on the current 2D view position and
327/// orientation.
328///
329/// The Z coordinate of the position is always `0.0` and the view is always looking down
330/// the negative Z axis.
331// TODO: doctest ?
332pub fn transform_mat_world_to_view (
333  view_position    : math::Point2 <f32>,
334  view_orientation : math::Rotation2 <f32>
335) -> math::Matrix4 <f32> {
336  let eye    = view_position.0.with_z (0.0);
337  let center = eye - math::Vector3::unit_z();
338  let up     = view_orientation.cols.y.with_z (0.0);
339  math::Matrix4::<f32>::look_at_rh (eye, center, up)
340}