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