viewport_lib/camera/
camera.rs1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3#[non_exhaustive]
4pub enum Projection {
5 Perspective,
7 Orthographic,
9}
10
11#[derive(Clone)]
20pub struct Camera {
21 pub projection: Projection,
23 pub center: glam::Vec3,
25 pub distance: f32,
27 pub orientation: glam::Quat,
31 pub fov_y: f32,
33 pub aspect: f32,
35 pub znear: f32,
37 pub zfar: f32,
39}
40
41impl Default for Camera {
42 fn default() -> Self {
43 Self {
44 projection: Projection::Perspective,
45 center: glam::Vec3::ZERO,
46 distance: 5.0,
47 orientation: glam::Quat::from_rotation_y(0.3) * glam::Quat::from_rotation_x(-0.3),
49 fov_y: std::f32::consts::FRAC_PI_4,
50 aspect: 1.5,
51 znear: 0.01,
52 zfar: 1000.0,
53 }
54 }
55}
56
57impl Camera {
58 fn eye_offset(&self) -> glam::Vec3 {
60 self.orientation * (glam::Vec3::Z * self.distance)
61 }
62
63 pub fn eye_position(&self) -> glam::Vec3 {
65 self.center + self.eye_offset()
66 }
67
68 pub fn view_matrix(&self) -> glam::Mat4 {
70 let eye = self.eye_position();
71 let up = self.orientation * glam::Vec3::Y;
72 glam::Mat4::look_at_rh(eye, self.center, up)
73 }
74
75 pub fn proj_matrix(&self) -> glam::Mat4 {
81 let effective_zfar = self.zfar.max(self.distance * 3.0);
85 match self.projection {
86 Projection::Perspective => {
87 glam::Mat4::perspective_rh(self.fov_y, self.aspect, self.znear, effective_zfar)
88 }
89 Projection::Orthographic => {
90 let half_h = self.distance * (self.fov_y / 2.0).tan();
91 let half_w = half_h * self.aspect;
92 glam::Mat4::orthographic_rh(
93 -half_w,
94 half_w,
95 -half_h,
96 half_h,
97 self.znear,
98 effective_zfar,
99 )
100 }
101 }
102 }
103
104 pub fn view_proj_matrix(&self) -> glam::Mat4 {
107 self.proj_matrix() * self.view_matrix()
108 }
109
110 pub fn right(&self) -> glam::Vec3 {
112 self.orientation * glam::Vec3::X
113 }
114
115 pub fn up(&self) -> glam::Vec3 {
117 self.orientation * glam::Vec3::Y
118 }
119
120 pub fn frustum(&self) -> crate::camera::frustum::Frustum {
122 crate::camera::frustum::Frustum::from_view_proj(&self.view_proj_matrix())
123 }
124
125 pub fn fit_sphere(&self, center: glam::Vec3, radius: f32) -> (glam::Vec3, f32) {
130 let distance = match self.projection {
131 Projection::Perspective => radius / (self.fov_y / 2.0).tan() * 1.2,
132 Projection::Orthographic => radius * 1.2,
133 };
134 (center, distance)
135 }
136
137 pub fn fit_aabb(&self, aabb: &crate::scene::aabb::Aabb) -> (glam::Vec3, f32) {
142 let center = aabb.center();
143 let radius = aabb.half_extents().length();
144 self.fit_sphere(center, radius)
145 }
146
147 pub fn center_on_domain(&mut self, nx: f32, ny: f32, nz: f32) {
150 self.center = glam::Vec3::new(nx / 2.0, ny / 2.0, nz / 2.0);
151 let diagonal = (nx * nx + ny * ny + nz * nz).sqrt();
152 self.distance = (diagonal / 2.0) / (self.fov_y / 2.0).tan() * 1.2;
153 self.znear = (diagonal * 0.0001).max(0.01);
154 self.zfar = (diagonal * 10.0).max(1000.0);
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_default_eye_position() {
164 let cam = Camera::default();
165 let expected = cam.center + cam.orientation * (glam::Vec3::Z * cam.distance);
166 let eye = cam.eye_position();
167 assert!(
168 (eye - expected).length() < 1e-5,
169 "eye={eye:?} expected={expected:?}"
170 );
171 }
172
173 #[test]
174 fn test_view_matrix_looks_at_center() {
175 let cam = Camera::default();
176 let view = cam.view_matrix();
177 let center_view = view.transform_point3(cam.center);
179 assert!(
181 center_view.x.abs() < 1e-4,
182 "center_view.x={}",
183 center_view.x
184 );
185 assert!(
186 center_view.y.abs() < 1e-4,
187 "center_view.y={}",
188 center_view.y
189 );
190 assert!(
191 center_view.z < 0.0,
192 "center should be in front of camera, z={}",
193 center_view.z
194 );
195 }
196
197 #[test]
198 fn test_view_proj_roundtrip() {
199 let cam = Camera::default();
200 let vp = cam.view_proj_matrix();
201 let vp_inv = vp.inverse();
202 let world_pt = vp_inv.project_point3(glam::Vec3::new(0.0, 0.0, 0.5));
204 let eye = cam.eye_position();
206 let to_center = (cam.center - eye).normalize();
207 let to_pt = (world_pt - eye).normalize();
208 let dot = to_center.dot(to_pt);
209 assert!(
210 dot > 0.99,
211 "dot={dot}, point should be along camera-to-center ray"
212 );
213 }
214
215 #[test]
216 fn test_center_on_domain() {
217 let mut cam = Camera::default();
218 cam.center_on_domain(10.0, 10.0, 10.0);
219 assert!((cam.center - glam::Vec3::splat(5.0)).length() < 1e-5);
220 assert!(cam.distance > 0.0);
221 }
222
223 #[test]
224 fn test_fit_sphere_perspective() {
225 let cam = Camera::default(); let (center, dist) = cam.fit_sphere(glam::Vec3::ZERO, 5.0);
227 assert!((center - glam::Vec3::ZERO).length() < 1e-5);
228 let expected = 5.0 / (cam.fov_y / 2.0).tan() * 1.2;
229 assert!(
230 (dist - expected).abs() < 1e-4,
231 "dist={dist}, expected={expected}"
232 );
233 }
234
235 #[test]
236 fn test_fit_sphere_orthographic() {
237 let mut cam = Camera::default();
238 cam.projection = Projection::Orthographic;
239 let (_, dist) = cam.fit_sphere(glam::Vec3::ZERO, 5.0);
240 let expected = 5.0 * 1.2;
241 assert!(
242 (dist - expected).abs() < 1e-4,
243 "dist={dist}, expected={expected}"
244 );
245 }
246
247 #[test]
248 fn test_fit_aabb_unit_cube() {
249 let cam = Camera::default();
250 let aabb = crate::scene::aabb::Aabb {
251 min: glam::Vec3::splat(-0.5),
252 max: glam::Vec3::splat(0.5),
253 };
254 let (center, dist) = cam.fit_aabb(&aabb);
255 assert!(center.length() < 1e-5, "center should be origin");
256 assert!(dist > 0.0, "distance should be positive");
257 let radius = aabb.half_extents().length();
259 let expected = radius / (cam.fov_y / 2.0).tan() * 1.2;
260 assert!(
261 (dist - expected).abs() < 1e-4,
262 "dist={dist}, expected={expected}"
263 );
264 }
265
266 #[test]
267 fn test_fit_aabb_preserves_padding() {
268 let cam = Camera::default();
269 let aabb = crate::scene::aabb::Aabb {
270 min: glam::Vec3::splat(-2.0),
271 max: glam::Vec3::splat(2.0),
272 };
273 let (_, dist) = cam.fit_aabb(&aabb);
274 let radius = aabb.half_extents().length();
276 let no_pad = radius / (cam.fov_y / 2.0).tan();
277 assert!(
278 dist > no_pad,
279 "padded distance ({dist}) should exceed unpadded ({no_pad})"
280 );
281 }
282
283 #[test]
284 fn test_right_up_orthogonal() {
285 let cam = Camera::default();
286 let dot = cam.right().dot(cam.up());
287 assert!(
288 dot.abs() < 1e-5,
289 "right and up should be orthogonal, dot={dot}"
290 );
291 }
292}