Skip to main content

agpu/scene3d/
camera.rs

1//! Camera — perspective and orthographic projection.
2
3/// Projection type.
4#[derive(Debug, Clone, Copy)]
5pub enum Projection {
6    Perspective {
7        fov_y: f32,
8        aspect: f32,
9        near: f32,
10        far: f32,
11    },
12    Orthographic {
13        width: f32,
14        height: f32,
15        near: f32,
16        far: f32,
17    },
18}
19
20/// A 3D camera with position, target, and projection.
21#[derive(Debug, Clone)]
22pub struct Camera {
23    eye: [f32; 3],
24    target: [f32; 3],
25    up: [f32; 3],
26    projection: Projection,
27}
28
29impl Camera {
30    pub fn perspective(fov_y: f32, aspect: f32, near: f32, far: f32) -> Self {
31        Self {
32            eye: [0.0, 0.0, 5.0],
33            target: [0.0, 0.0, 0.0],
34            up: [0.0, 1.0, 0.0],
35            projection: Projection::Perspective {
36                fov_y,
37                aspect,
38                near,
39                far,
40            },
41        }
42    }
43
44    pub fn orthographic(width: f32, height: f32, near: f32, far: f32) -> Self {
45        Self {
46            eye: [0.0, 0.0, 5.0],
47            target: [0.0, 0.0, 0.0],
48            up: [0.0, 1.0, 0.0],
49            projection: Projection::Orthographic {
50                width,
51                height,
52                near,
53                far,
54            },
55        }
56    }
57
58    pub fn position(mut self, eye: [f32; 3]) -> Self {
59        self.eye = eye;
60        self
61    }
62
63    pub fn look_at(mut self, target: [f32; 3]) -> Self {
64        self.target = target;
65        self
66    }
67
68    pub fn up(mut self, up: [f32; 3]) -> Self {
69        self.up = up;
70        self
71    }
72
73    pub fn eye(&self) -> [f32; 3] {
74        self.eye
75    }
76
77    pub fn target(&self) -> [f32; 3] {
78        self.target
79    }
80
81    pub fn projection(&self) -> Projection {
82        self.projection
83    }
84
85    pub fn fov_y(&self) -> f32 {
86        match self.projection {
87            Projection::Perspective { fov_y, .. } => fov_y,
88            Projection::Orthographic { .. } => 0.0,
89        }
90    }
91
92    /// Compute a 4×4 view matrix (column-major) using a look-at transform.
93    pub fn view_matrix(&self) -> [f32; 16] {
94        let f = normalize(sub(self.target, self.eye));
95        let s = normalize(cross(f, self.up));
96        let u = cross(s, f);
97
98        [
99            s[0],
100            u[0],
101            -f[0],
102            0.0,
103            s[1],
104            u[1],
105            -f[1],
106            0.0,
107            s[2],
108            u[2],
109            -f[2],
110            0.0,
111            -dot(s, self.eye),
112            -dot(u, self.eye),
113            dot(f, self.eye),
114            1.0,
115        ]
116    }
117
118    /// Compute a 4×4 projection matrix (column-major).
119    pub fn projection_matrix(&self) -> [f32; 16] {
120        match self.projection {
121            Projection::Perspective {
122                fov_y,
123                aspect,
124                near,
125                far,
126            } => {
127                let f = 1.0 / (fov_y.to_radians() / 2.0).tan();
128                let nf = 1.0 / (near - far);
129                [
130                    f / aspect,
131                    0.0,
132                    0.0,
133                    0.0,
134                    0.0,
135                    f,
136                    0.0,
137                    0.0,
138                    0.0,
139                    0.0,
140                    (far + near) * nf,
141                    -1.0,
142                    0.0,
143                    0.0,
144                    2.0 * far * near * nf,
145                    0.0,
146                ]
147            }
148            Projection::Orthographic {
149                width,
150                height,
151                near,
152                far,
153            } => {
154                let nf = 1.0 / (near - far);
155                [
156                    2.0 / width,
157                    0.0,
158                    0.0,
159                    0.0,
160                    0.0,
161                    2.0 / height,
162                    0.0,
163                    0.0,
164                    0.0,
165                    0.0,
166                    2.0 * nf,
167                    0.0,
168                    0.0,
169                    0.0,
170                    (far + near) * nf,
171                    1.0,
172                ]
173            }
174        }
175    }
176}
177
178fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
179    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
180}
181
182fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
183    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
184}
185
186fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
187    [
188        a[1] * b[2] - a[2] * b[1],
189        a[2] * b[0] - a[0] * b[2],
190        a[0] * b[1] - a[1] * b[0],
191    ]
192}
193
194fn normalize(v: [f32; 3]) -> [f32; 3] {
195    let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
196    if len < 1e-10 {
197        return [0.0, 0.0, 0.0];
198    }
199    [v[0] / len, v[1] / len, v[2] / len]
200}