Skip to main content

game_toolkit_gfx/
camera.rs

1use bytemuck::{Pod, Zeroable};
2
3/// 2D orthographic camera. Default coordinate system: top-left origin, pixels.
4#[derive(Copy, Clone, Debug)]
5pub struct Camera2D {
6    pub center: [f32; 2],
7    pub zoom: f32,
8    pub viewport: [f32; 2],
9    /// If true, +Y points down (top-left origin). Matches typical 2D screen coords.
10    pub y_down: bool,
11}
12
13impl Camera2D {
14    pub fn new(viewport_w: f32, viewport_h: f32) -> Self {
15        Self {
16            center: [viewport_w * 0.5, viewport_h * 0.5],
17            zoom: 1.0,
18            viewport: [viewport_w, viewport_h],
19            y_down: true,
20        }
21    }
22
23    pub fn resize(&mut self, w: f32, h: f32) {
24        // Keep the top-left world coordinate anchored so screen pixels keep
25        // mapping to the same world origin as the window grows or shrinks.
26        // Without this, `center` stays at the old viewport's midpoint and the
27        // projection drifts: the visible edges no longer line up with world
28        // [0, size], which makes pixel-space content appear off-centre after a
29        // resize. A follow camera overwrites `center` each frame, so it is
30        // unaffected.
31        let z = self.zoom.max(1e-4);
32        let top_left = [
33            self.center[0] - self.viewport[0] * 0.5 / z,
34            self.center[1] - self.viewport[1] * 0.5 / z,
35        ];
36        self.viewport = [w, h];
37        self.center = [top_left[0] + w * 0.5 / z, top_left[1] + h * 0.5 / z];
38    }
39
40    /// World-space rectangle currently visible, as `(min, max)` corners. Useful for culling.
41    pub fn visible_rect(&self) -> ([f32; 2], [f32; 2]) {
42        let z = self.zoom.max(1e-4);
43        let hw = self.viewport[0] * 0.5 / z;
44        let hh = self.viewport[1] * 0.5 / z;
45        (
46            [self.center[0] - hw, self.center[1] - hh],
47            [self.center[0] + hw, self.center[1] + hh],
48        )
49    }
50
51    pub fn view_proj(&self) -> [[f32; 4]; 4] {
52        let z = self.zoom.max(1e-4);
53        let hw = self.viewport[0] * 0.5 / z;
54        let hh = self.viewport[1] * 0.5 / z;
55        let l = self.center[0] - hw;
56        let r = self.center[0] + hw;
57        let (b, t) = if self.y_down {
58            (self.center[1] + hh, self.center[1] - hh)
59        } else {
60            (self.center[1] - hh, self.center[1] + hh)
61        };
62        ortho(l, r, b, t, -1.0, 1.0)
63    }
64}
65
66fn ortho(l: f32, r: f32, b: f32, t: f32, n: f32, f: f32) -> [[f32; 4]; 4] {
67    let rml = r - l;
68    let tmb = t - b;
69    let fmn = f - n;
70    [
71        [2.0 / rml, 0.0, 0.0, 0.0],
72        [0.0, 2.0 / tmb, 0.0, 0.0],
73        [0.0, 0.0, -1.0 / fmn, 0.0],
74        [-(r + l) / rml, -(t + b) / tmb, -n / fmn, 1.0],
75    ]
76}
77
78#[repr(C)]
79#[derive(Copy, Clone, Pod, Zeroable)]
80pub(crate) struct CameraUniform {
81    pub view_proj: [[f32; 4]; 4],
82}