Skip to main content

macroquad_ply/
camera.rs

1//! 2D and 3D camera.
2
3use crate::{
4    get_context,
5    math::Rect,
6    prelude::RenderPass,
7    texture::RenderTarget,
8    window::{screen_height, screen_width},
9};
10use glam::{vec2, vec3, Mat4, Vec2, Vec3};
11
12pub trait Camera {
13    fn matrix(&self) -> Mat4;
14    fn depth_enabled(&self) -> bool;
15    fn render_pass(&self) -> Option<RenderPass>;
16    fn viewport(&self) -> Option<(i32, i32, i32, i32)>;
17}
18
19#[derive(Debug)]
20pub struct Camera2D {
21    /// Rotation in degrees.
22    pub rotation: f32,
23    /// Scaling, should be (1.0, 1.0) by default.
24    pub zoom: Vec2,
25    /// Rotation and zoom origin.
26    pub target: Vec2,
27    /// Displacement from target.
28    pub offset: Vec2,
29
30    /// If "render_target" is set - camera will render to texture.
31    ///
32    /// Otherwise to the screen.
33    pub render_target: Option<RenderTarget>,
34
35    /// Part of the screen to render to.
36    ///
37    /// None means the whole screen.
38    ///
39    /// Viewport do not affect camera space, just the render position on the screen.
40    ///
41    /// Useful for things like splitscreen.
42    pub viewport: Option<(i32, i32, i32, i32)>,
43}
44
45impl Camera2D {
46    /// Will make camera space equals given rect.
47    pub fn from_display_rect(rect: Rect) -> Camera2D {
48        let target = vec2(rect.x + rect.w / 2., rect.y + rect.h / 2.);
49
50        Camera2D {
51            target,
52            zoom: vec2(1. / rect.w * 2., -1. / rect.h * 2.),
53            offset: vec2(0., 0.),
54            rotation: 0.,
55
56            render_target: None,
57            viewport: None,
58        }
59    }
60}
61
62impl Default for Camera2D {
63    fn default() -> Camera2D {
64        Camera2D {
65            zoom: vec2(1., 1.),
66            offset: vec2(0., 0.),
67            target: vec2(0., 0.),
68            rotation: 0.,
69
70            render_target: None,
71            viewport: None,
72        }
73    }
74}
75
76impl Camera for Camera2D {
77    fn matrix(&self) -> Mat4 {
78        // gleaned from https://github.com/raysan5/raylib/blob/master/src/core.c#L1528
79
80        // The camera in world-space is set by
81        //   1. Move it to target
82        //   2. Rotate by -rotation and scale by (1/zoom)
83        //      When setting higher scale, it's more intuitive for the world to become bigger (= camera become smaller),
84        //      not for the camera getting bigger, hence the invert. Same deal with rotation.
85        //   3. Move it by (-offset);
86        //      Offset defines target transform relative to screen, but since we're effectively "moving" screen (camera)
87        //      we need to do it into opposite direction (inverse transform)
88
89        // Having camera transform in world-space, inverse of it gives the modelview transform.
90        // Since (A*B*C)' = C'*B'*A', the modelview is
91        //   1. Move to offset
92        //   2. Rotate and Scale
93        //   3. Move by -target
94        let mat_origin = Mat4::from_translation(vec3(-self.target.x, -self.target.y, 0.0));
95        let mat_rotation = Mat4::from_axis_angle(vec3(0.0, 0.0, 1.0), self.rotation.to_radians());
96        let invert_y = if self.render_target.is_some() {
97            1.0
98        } else {
99            -1.0
100        };
101        let mat_scale = Mat4::from_scale(vec3(self.zoom.x, self.zoom.y * invert_y, 1.0));
102        let mat_translation = Mat4::from_translation(vec3(self.offset.x, self.offset.y, 0.0));
103
104        mat_translation * ((mat_scale * mat_rotation) * mat_origin)
105    }
106
107    fn depth_enabled(&self) -> bool {
108        false
109    }
110
111    fn render_pass(&self) -> Option<RenderPass> {
112        self.render_target.as_ref().map(|rt| rt.render_pass.clone())
113    }
114
115    fn viewport(&self) -> Option<(i32, i32, i32, i32)> {
116        self.viewport
117    }
118}
119
120impl Camera2D {
121    /// Returns the screen space position for a 2d camera world space position.
122    ///
123    /// Screen position in window space - from (0, 0) to (screen_width, screen_height()).
124    /// When a viewport is set, returns coordinates within that viewport's screen region.
125    pub fn world_to_screen(&self, point: Vec2) -> Vec2 {
126        let dims = self
127            .viewport()
128            .map(|(vx, vy, vw, vh)| Rect {
129                x: vx as f32,
130                y: screen_height() - (vy + vh) as f32,
131                w: vw as f32,
132                h: vh as f32,
133            })
134            .unwrap_or(Rect {
135                x: 0.0,
136                y: 0.0,
137                w: screen_width(),
138                h: screen_height(),
139            });
140
141        let mat = self.matrix();
142        let transform = mat.transform_point3(vec3(point.x, point.y, 0.));
143
144        vec2(
145            (transform.x / 2. + 0.5) * dims.w + dims.x,
146            (0.5 - transform.y / 2.) * dims.h + dims.y,
147        )
148    }
149
150    /// Returns the world space position for a 2d camera screen space position.
151    ///
152    /// Point is a screen space position, often mouse x and y.
153    pub fn screen_to_world(&self, point: Vec2) -> Vec2 {
154        let dims = self
155            .viewport()
156            .map(|(vx, vy, vw, vh)| Rect {
157                x: vx as f32,
158                y: screen_height() - (vy + vh) as f32,
159                w: vw as f32,
160                h: vh as f32,
161            })
162            .unwrap_or(Rect {
163                x: 0.0,
164                y: 0.0,
165                w: screen_width(),
166                h: screen_height(),
167            });
168
169        let point = vec2(
170            (point.x - dims.x) / dims.w * 2. - 1.,
171            1. - (point.y - dims.y) / dims.h * 2.,
172        );
173        let inv_mat = self.matrix().inverse();
174        let transform = inv_mat.transform_point3(vec3(point.x, point.y, 0.));
175
176        vec2(transform.x, transform.y)
177    }
178}
179
180#[derive(Debug, Clone, Copy)]
181pub enum Projection {
182    Perspective,
183    Orthographics,
184}
185
186#[derive(Debug)]
187pub struct Camera3D {
188    /// Camera position.
189    pub position: Vec3,
190    /// Camera target it looks-at.
191    pub target: Vec3,
192    /// Camera up vector (rotation over its axis).
193    pub up: Vec3,
194    /// Camera field-of-view aperture in Y (radians)
195    /// in perspective, used as near plane width in orthographic.
196    pub fovy: f32,
197    /// Screen aspect ratio.
198    ///
199    /// By default aspect is calculated with screen_width() / screen_height() on each frame.
200    pub aspect: Option<f32>,
201    /// Camera projection type, perspective or orthographics.
202    pub projection: Projection,
203
204    /// If "render_target" is set - camera will render to texture.
205    ///
206    /// Otherwise to the screen.
207    pub render_target: Option<RenderTarget>,
208
209    /// Part of the screen to render to.
210    ///
211    /// None means the whole screen.
212    ///
213    /// Viewport do not affect camera space, just the render position on the screen.
214    ///
215    /// Useful for things like splitscreen.
216    pub viewport: Option<(i32, i32, i32, i32)>,
217
218    /// Camera near plane
219    pub z_near: f32,
220    /// Camera far plane
221    pub z_far: f32,
222}
223
224impl Default for Camera3D {
225    fn default() -> Camera3D {
226        Camera3D {
227            position: vec3(0., -10., 0.),
228            target: vec3(0., 0., 0.),
229            aspect: None,
230            up: vec3(0., 0., 1.),
231            fovy: 45.0_f32.to_radians(),
232            projection: Projection::Perspective,
233            render_target: None,
234            viewport: None,
235            z_near: 0.01,
236            z_far: 10000.0,
237        }
238    }
239}
240
241impl Camera for Camera3D {
242    fn matrix(&self) -> Mat4 {
243        let aspect = self.aspect.unwrap_or(screen_width() / screen_height());
244
245        match self.projection {
246            Projection::Perspective => {
247                Mat4::perspective_rh_gl(self.fovy, aspect, self.z_near, self.z_far)
248                    * Mat4::look_at_rh(self.position, self.target, self.up)
249            }
250            Projection::Orthographics => {
251                let top = self.fovy / 2.0;
252                let right = top * aspect;
253
254                Mat4::orthographic_rh_gl(-right, right, -top, top, self.z_near, self.z_far)
255                    * Mat4::look_at_rh(self.position, self.target, self.up)
256            }
257        }
258    }
259
260    fn depth_enabled(&self) -> bool {
261        true
262    }
263
264    fn render_pass(&self) -> Option<RenderPass> {
265        self.render_target.as_ref().map(|rt| rt.render_pass.clone())
266    }
267
268    fn viewport(&self) -> Option<(i32, i32, i32, i32)> {
269        self.viewport
270    }
271}
272
273/// Set active 2D or 3D camera.
274pub fn set_camera(camera: &dyn Camera) {
275    let context = get_context();
276
277    // flush previous camera draw calls
278    context.perform_render_passes();
279
280    context
281        .gl
282        .render_pass(camera.render_pass().map(|rt| rt.raw_miniquad_id()));
283
284    context.gl.viewport(camera.viewport());
285    context.gl.depth_test(camera.depth_enabled());
286    context.camera_matrix = Some(camera.matrix());
287}
288
289/// Reset default 2D camera mode.
290pub fn set_default_camera() {
291    let context = get_context();
292
293    // flush previous camera draw calls
294    context.perform_render_passes();
295
296    context.gl.render_pass(None);
297    context.gl.viewport(None);
298    context.gl.depth_test(false);
299    context.camera_matrix = None;
300}
301
302pub(crate) struct CameraState {
303    render_pass: Option<miniquad::RenderPass>,
304    depth_test: bool,
305    matrix: Option<Mat4>,
306}
307
308pub fn push_camera_state() {
309    let context = get_context();
310
311    let camera_state = CameraState {
312        render_pass: context.gl.get_active_render_pass(),
313        depth_test: context.gl.is_depth_test_enabled(),
314        matrix: context.camera_matrix,
315    };
316    context.camera_stack.push(camera_state);
317}
318
319pub fn pop_camera_state() {
320    let context = get_context();
321
322    if let Some(camera_state) = context.camera_stack.pop() {
323        context.perform_render_passes();
324
325        context.gl.render_pass(camera_state.render_pass);
326        context.gl.depth_test(camera_state.depth_test);
327        context.camera_matrix = camera_state.matrix;
328    }
329}