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