khora_data/ecs/components/
camera.rs1use khora_core::math::Mat4;
16use khora_macros::Component;
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum ProjectionType {
21 Perspective {
23 fov_y_radians: f32,
25 },
26 Orthographic {
28 width: f32,
30 height: f32,
32 },
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Component)]
40pub struct Camera {
41 pub projection: ProjectionType,
43
44 pub aspect_ratio: f32,
47
48 pub z_near: f32,
52
53 pub z_far: f32,
57
58 pub is_active: bool,
61}
62
63impl Camera {
64 pub fn new_perspective(fov_y_radians: f32, aspect_ratio: f32, z_near: f32, z_far: f32) -> Self {
66 Self {
67 projection: ProjectionType::Perspective { fov_y_radians },
68 aspect_ratio,
69 z_near,
70 z_far,
71 is_active: true,
72 }
73 }
74
75 pub fn new_orthographic(width: f32, height: f32, z_near: f32, z_far: f32) -> Self {
77 let aspect_ratio = if height > 0.0 { width / height } else { 1.0 };
78 Self {
79 projection: ProjectionType::Orthographic { width, height },
80 aspect_ratio,
81 z_near,
82 z_far,
83 is_active: true,
84 }
85 }
86
87 pub fn default_perspective() -> Self {
94 Self::new_perspective(60.0_f32.to_radians(), 16.0 / 9.0, 0.1, 1000.0)
95 }
96
97 pub fn default_orthographic() -> Self {
104 Self::new_orthographic(1920.0, 1080.0, -1.0, 1000.0)
105 }
106
107 pub fn projection_matrix(&self) -> Mat4 {
112 match self.projection {
113 ProjectionType::Perspective { fov_y_radians } => {
114 Mat4::perspective_rh_zo(fov_y_radians, self.aspect_ratio, self.z_near, self.z_far)
115 }
116 ProjectionType::Orthographic { width, height } => {
117 let half_width = width / 2.0;
118 let half_height = height / 2.0;
119 Mat4::orthographic_rh_zo(
120 -half_width,
121 half_width,
122 -half_height,
123 half_height,
124 self.z_near,
125 self.z_far,
126 )
127 }
128 }
129 }
130
131 pub fn set_aspect_ratio(&mut self, width: u32, height: u32) {
133 if height > 0 {
134 self.aspect_ratio = width as f32 / height as f32;
135 }
136 }
137}
138
139impl Default for Camera {
140 fn default() -> Self {
141 Self::default_perspective()
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use std::f32::consts::PI;
149
150 #[test]
151 fn test_camera_default() {
152 let camera = Camera::default();
153 match camera.projection {
154 ProjectionType::Perspective { fov_y_radians } => {
155 assert_eq!(fov_y_radians, 60.0_f32.to_radians());
156 }
157 _ => panic!("Expected perspective projection"),
158 }
159 assert_eq!(camera.aspect_ratio, 16.0 / 9.0);
160 assert_eq!(camera.z_near, 0.1);
161 assert_eq!(camera.z_far, 1000.0);
162 assert!(camera.is_active);
163 }
164
165 #[test]
166 fn test_camera_new_perspective() {
167 let camera = Camera::new_perspective(PI / 3.0, 4.0 / 3.0, 0.5, 500.0);
168 match camera.projection {
169 ProjectionType::Perspective { fov_y_radians } => {
170 assert_eq!(fov_y_radians, PI / 3.0); }
172 _ => panic!("Expected perspective projection"),
173 }
174 assert_eq!(camera.aspect_ratio, 4.0 / 3.0);
175 assert_eq!(camera.z_near, 0.5);
176 assert_eq!(camera.z_far, 500.0);
177 assert!(camera.is_active);
178 }
179
180 #[test]
181 fn test_camera_new_orthographic() {
182 let camera = Camera::new_orthographic(1920.0, 1080.0, -1.0, 1000.0);
183 match camera.projection {
184 ProjectionType::Orthographic { width, height } => {
185 assert_eq!(width, 1920.0);
186 assert_eq!(height, 1080.0);
187 }
188 _ => panic!("Expected orthographic projection"),
189 }
190 assert_eq!(camera.z_near, -1.0);
191 assert_eq!(camera.z_far, 1000.0);
192 assert!(camera.is_active);
193 }
194
195 #[test]
196 fn test_camera_projection_matrix() {
197 let camera = Camera::new_perspective(PI / 2.0, 1.0, 1.0, 10.0);
198 let proj = camera.projection_matrix();
199
200 assert_ne!(proj, Mat4::IDENTITY);
202
203 let det = proj.determinant();
205 assert!(det.abs() > 0.0001, "Projection matrix is degenerate");
206 }
207
208 #[test]
209 fn test_camera_orthographic_projection_matrix() {
210 let camera = Camera::new_orthographic(100.0, 100.0, 0.1, 100.0);
211 let proj = camera.projection_matrix();
212
213 assert_ne!(proj, Mat4::IDENTITY);
215
216 }
219
220 #[test]
221 fn test_camera_aspect_ratio_update() {
222 let mut camera = Camera::default();
223 camera.set_aspect_ratio(2560, 1080); assert!((camera.aspect_ratio - 2560.0 / 1080.0).abs() < 0.001);
226
227 let proj = camera.projection_matrix();
228 assert_ne!(proj, Mat4::IDENTITY);
229 }
230
231 #[test]
232 fn test_camera_aspect_ratio_zero_height() {
233 let mut camera = Camera::default();
234 let old_aspect = camera.aspect_ratio;
235
236 camera.set_aspect_ratio(1920, 0);
238 assert_eq!(camera.aspect_ratio, old_aspect);
239 }
240
241 #[test]
242 fn test_camera_active_flag() {
243 let mut camera = Camera::default();
244 assert!(camera.is_active);
245
246 camera.is_active = false;
247 assert!(!camera.is_active);
248 }
249
250 #[test]
251 fn test_camera_fov_limits() {
252 let narrow_camera = Camera::new_perspective(0.1, 16.0 / 9.0, 0.1, 100.0);
254 let narrow_proj = narrow_camera.projection_matrix();
255 assert_ne!(narrow_proj, Mat4::IDENTITY);
256
257 let wide_camera = Camera::new_perspective(PI * 0.9, 16.0 / 9.0, 0.1, 100.0);
259 let wide_proj = wide_camera.projection_matrix();
260 assert_ne!(wide_proj, Mat4::IDENTITY);
261 }
262
263 #[test]
264 fn test_camera_near_far_planes() {
265 let camera = Camera::new_perspective(PI / 4.0, 16.0 / 9.0, 0.01, 10000.0);
266 assert_eq!(camera.z_near, 0.01);
267 assert_eq!(camera.z_far, 10000.0);
268 assert!(camera.z_near < camera.z_far);
269 }
270
271 #[test]
272 fn test_camera_default_perspective() {
273 let camera1 = Camera::default();
274 let camera2 = Camera::default_perspective();
275
276 assert_eq!(camera1.projection, camera2.projection);
277 assert_eq!(camera1.aspect_ratio, camera2.aspect_ratio);
278 assert_eq!(camera1.z_near, camera2.z_near);
279 assert_eq!(camera1.z_far, camera2.z_far);
280 assert_eq!(camera1.is_active, camera2.is_active);
281 }
282
283 #[test]
284 fn test_camera_default_orthographic() {
285 let camera = Camera::default_orthographic();
286 match camera.projection {
287 ProjectionType::Orthographic { width, height } => {
288 assert_eq!(width, 1920.0);
289 assert_eq!(height, 1080.0);
290 }
291 _ => panic!("Expected orthographic projection"),
292 }
293 assert!(camera.is_active);
294 }
295}