macroquad/
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    pub fn world_to_screen(&self, point: Vec2) -> Vec2 {
125        let mat = self.matrix();
126        let transform = mat.transform_point3(vec3(point.x, point.y, 0.));
127
128        vec2(
129            (transform.x / 2. + 0.5) * screen_width(),
130            (0.5 - transform.y / 2.) * screen_height(),
131        )
132    }
133
134    /// Returns the world space position for a 2d camera screen space position.
135    ///
136    /// Point is a screen space position, often mouse x and y.
137    pub fn screen_to_world(&self, point: Vec2) -> Vec2 {
138        let dims = self
139            .viewport()
140            .map(|(vx, vy, vw, vh)| Rect {
141                x: vx as f32,
142                y: screen_height() - (vy + vh) as f32,
143                w: vw as f32,
144                h: vh as f32,
145            })
146            .unwrap_or(Rect {
147                x: 0.0,
148                y: 0.0,
149                w: screen_width(),
150                h: screen_height(),
151            });
152
153        let point = vec2(
154            (point.x - dims.x) / dims.w * 2. - 1.,
155            1. - (point.y - dims.y) / dims.h * 2.,
156        );
157        let inv_mat = self.matrix().inverse();
158        let transform = inv_mat.transform_point3(vec3(point.x, point.y, 0.));
159
160        vec2(transform.x, transform.y)
161    }
162}
163
164#[derive(Debug, Clone, Copy)]
165pub enum Projection {
166    Perspective,
167    Orthographics,
168}
169
170#[derive(Debug)]
171pub struct Camera3D {
172    /// Camera position.
173    pub position: Vec3,
174    /// Camera target it looks-at.
175    pub target: Vec3,
176    /// Camera up vector (rotation over its axis).
177    pub up: Vec3,
178    /// Camera field-of-view aperture in Y (radians)
179    /// in perspective, used as near plane width in orthographic.
180    pub fovy: f32,
181    /// Screen aspect ratio.
182    ///
183    /// By default aspect is calculated with screen_width() / screen_height() on each frame.
184    pub aspect: Option<f32>,
185    /// Camera projection type, perspective or orthographics.
186    pub projection: Projection,
187
188    /// If "render_target" is set - camera will render to texture.
189    ///
190    /// Otherwise to the screen.
191    pub render_target: Option<RenderTarget>,
192
193    /// Part of the screen to render to.
194    ///
195    /// None means the whole screen.
196    ///
197    /// Viewport do not affect camera space, just the render position on the screen.
198    ///
199    /// Useful for things like splitscreen.
200    pub viewport: Option<(i32, i32, i32, i32)>,
201
202    /// Camera near plane
203    pub z_near: f32,
204    /// Camera far plane
205    pub z_far: f32,
206}
207
208impl Default for Camera3D {
209    fn default() -> Camera3D {
210        Camera3D {
211            position: vec3(0., -10., 0.),
212            target: vec3(0., 0., 0.),
213            aspect: None,
214            up: vec3(0., 0., 1.),
215            fovy: 45.0_f32.to_radians(),
216            projection: Projection::Perspective,
217            render_target: None,
218            viewport: None,
219            z_near: 0.01,
220            z_far: 10000.0,
221        }
222    }
223}
224
225impl Camera for Camera3D {
226    fn matrix(&self) -> Mat4 {
227        let aspect = self.aspect.unwrap_or(screen_width() / screen_height());
228
229        match self.projection {
230            Projection::Perspective => {
231                Mat4::perspective_rh_gl(self.fovy, aspect, self.z_near, self.z_far)
232                    * Mat4::look_at_rh(self.position, self.target, self.up)
233            }
234            Projection::Orthographics => {
235                let top = self.fovy / 2.0;
236                let right = top * aspect;
237
238                Mat4::orthographic_rh_gl(-right, right, -top, top, self.z_near, self.z_far)
239                    * Mat4::look_at_rh(self.position, self.target, self.up)
240            }
241        }
242    }
243
244    fn depth_enabled(&self) -> bool {
245        true
246    }
247
248    fn render_pass(&self) -> Option<RenderPass> {
249        self.render_target.as_ref().map(|rt| rt.render_pass.clone())
250    }
251
252    fn viewport(&self) -> Option<(i32, i32, i32, i32)> {
253        self.viewport
254    }
255}
256
257/// Set active 2D or 3D camera.
258pub fn set_camera(camera: &dyn Camera) {
259    let context = get_context();
260
261    // flush previous camera draw calls
262    context.perform_render_passes();
263
264    context
265        .gl
266        .render_pass(camera.render_pass().map(|rt| rt.raw_miniquad_id()));
267
268    context.gl.viewport(camera.viewport());
269    context.gl.depth_test(camera.depth_enabled());
270    context.camera_matrix = Some(camera.matrix());
271}
272
273/// Reset default 2D camera mode.
274pub fn set_default_camera() {
275    let context = get_context();
276
277    // flush previous camera draw calls
278    context.perform_render_passes();
279
280    context.gl.render_pass(None);
281    context.gl.viewport(None);
282    context.gl.depth_test(false);
283    context.camera_matrix = None;
284}
285
286pub(crate) struct CameraState {
287    render_pass: Option<miniquad::RenderPass>,
288    depth_test: bool,
289    matrix: Option<Mat4>,
290}
291
292pub fn push_camera_state() {
293    let context = get_context();
294
295    let camera_state = CameraState {
296        render_pass: context.gl.get_active_render_pass(),
297        depth_test: context.gl.is_depth_test_enabled(),
298        matrix: context.camera_matrix,
299    };
300    context.camera_stack.push(camera_state);
301}
302
303pub fn pop_camera_state() {
304    let context = get_context();
305
306    if let Some(camera_state) = context.camera_stack.pop() {
307        context.perform_render_passes();
308
309        context.gl.render_pass(camera_state.render_pass);
310        context.gl.depth_test(camera_state.depth_test);
311        context.camera_matrix = camera_state.matrix;
312    }
313}