Skip to main content

mittens_engine/engine/ecs/system/
camera_system.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::World;
3use crate::engine::ecs::component::Camera3DComponent;
4use crate::engine::ecs::component::CameraXRComponent;
5use crate::engine::ecs::system::System;
6use crate::engine::ecs::system::TransformSystem;
7use crate::engine::graphics::VisualWorld;
8use crate::engine::graphics::primitives::Transform;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct CameraHandle(pub u32);
12
13#[derive(Debug, Clone, Copy)]
14pub struct Camera3D {
15    pub view: [[f32; 4]; 4],
16    pub proj: [[f32; 4]; 4],
17    pub transform: Transform,
18}
19
20#[derive(Debug, Clone, Copy)]
21pub struct Camera2D {
22    pub view: [[f32; 4]; 4],
23    pub proj: [[f32; 4]; 4],
24    pub transform: Transform,
25}
26
27#[derive(Debug, Clone, Copy)]
28enum AnyCamera {
29    Camera3D(Camera3D),
30    Camera2D(Camera2D),
31}
32
33impl Camera3D {
34    pub fn identity() -> Self {
35        Self {
36            view: [
37                [1.0, 0.0, 0.0, 0.0],
38                [0.0, 1.0, 0.0, 0.0],
39                [0.0, 0.0, 1.0, 0.0],
40                [0.0, 0.0, 0.0, 1.0],
41            ],
42            proj: [
43                [1.0, 0.0, 0.0, 0.0],
44                [0.0, 1.0, 0.0, 0.0],
45                [0.0, 0.0, 1.0, 0.0],
46                [0.0, 0.0, 0.0, 1.0],
47            ],
48            transform: Transform::default(),
49        }
50    }
51
52    /// Right-handed perspective projection matrix.
53    ///
54    /// Assumptions:
55    /// - Column-major mat4 (matches how we pack instance matrices / GLSL default).
56    /// - NDC depth range is Vulkan-style: z in [0, 1].
57    pub fn perspective_rh_zo(
58        fov_y_radians: f32,
59        aspect: f32,
60        z_near: f32,
61        z_far: f32,
62    ) -> [[f32; 4]; 4] {
63        // Based on the standard RH, zero-to-one depth projection.
64        // Maps camera forward -Z.
65        let f = 1.0 / (0.5 * fov_y_radians).tan();
66        let nf = 1.0 / (z_near - z_far);
67
68        // Column-major:
69        // [ f/aspect, 0,  0,                      0 ]
70        // [ 0,        f,  0,                      0 ]
71        // [ 0,        0,  z_far*nf,               -1 ]
72        // [ 0,        0,  z_near*z_far*nf,         0 ]
73        [
74            [f / aspect, 0.0, 0.0, 0.0],
75            [0.0, f, 0.0, 0.0],
76            [0.0, 0.0, z_far * nf, -1.0],
77            [0.0, 0.0, (z_near * z_far) * nf, 0.0],
78        ]
79    }
80}
81
82impl Camera2D {
83    pub fn identity() -> Self {
84        Self {
85            view: [
86                [1.0, 0.0, 0.0, 0.0],
87                [0.0, 1.0, 0.0, 0.0],
88                [0.0, 0.0, 1.0, 0.0],
89                [0.0, 0.0, 0.0, 1.0],
90            ],
91            proj: [
92                [1.0, 0.0, 0.0, 0.0],
93                [0.0, 1.0, 0.0, 0.0],
94                [0.0, 0.0, 1.0, 0.0],
95                [0.0, 0.0, 0.0, 1.0],
96            ],
97            transform: Transform::default(),
98        }
99    }
100}
101
102#[derive(Debug, Default)]
103pub struct CameraSystem {
104    next_handle: u32,
105    cameras: Vec<(CameraHandle, AnyCamera)>,
106    camera2d_components: std::collections::HashMap<CameraHandle, ComponentId>,
107    camera3d_components: std::collections::HashMap<CameraHandle, ComponentId>,
108    pub active_window_camera: Option<CameraHandle>,
109    pub active_xr_camera: Option<ComponentId>,
110
111    // Track viewport changes for the no-camera fallback projection.
112    last_viewport: Option<[f32; 2]>,
113}
114
115impl CameraSystem {
116    pub fn new() -> Self {
117        Self::default()
118    }
119
120    /// Registers a camera derived from the component tree.
121    ///
122    /// The newest registered camera becomes active.
123    pub fn register_camera(
124        &mut self,
125        world: &mut World,
126        visuals: &mut VisualWorld,
127        component: ComponentId,
128    ) -> CameraHandle {
129        // Default 3D camera parameters.
130        let (w, h) = {
131            let vp = visuals.viewport();
132            (vp[0], vp[1])
133        };
134        let aspect = if h > 0.0 { w / h } else { 1.0 };
135
136        let (fov_y_deg, z_near, z_far) = world
137            .get_component_by_id_as::<Camera3DComponent>(component)
138            .map(|c| (c.fov_y_degrees, c.z_near, c.z_far))
139            .unwrap_or((
140                Camera3DComponent::DEFAULT_FOV_Y_DEGREES,
141                Camera3DComponent::DEFAULT_Z_NEAR,
142                Camera3DComponent::DEFAULT_Z_FAR,
143            ));
144        let proj = Camera3D::perspective_rh_zo(fov_y_deg.to_radians(), aspect, z_near, z_far);
145
146        // If the camera is parented under a TransformComponent, use that transform as the camera pose.
147        // Otherwise default to identity view.
148        let (view, transform) = if let Some(model) = TransformSystem::world_model(world, component)
149        {
150            (
151                invert_affine_transform(&model),
152                transform_from_matrix_world(model),
153            )
154        } else {
155            let ident = Camera3D::identity();
156            (ident.view, ident.transform)
157        };
158
159        let cam = Camera3D {
160            view,
161            proj,
162            transform,
163        };
164
165        let h = CameraHandle(self.next_handle);
166        self.next_handle = self.next_handle.wrapping_add(1);
167
168        self.cameras.push((h, AnyCamera::Camera3D(cam)));
169        self.camera3d_components.insert(h, component);
170
171        // Newest becomes active (window target).
172        self.active_window_camera = Some(h);
173        visuals.set_camera_mono_for_target_with_transform(
174            crate::engine::graphics::CameraTarget::Window,
175            cam.view,
176            cam.proj,
177            cam.transform,
178        );
179
180        h
181    }
182
183    pub fn set_active_window_camera(&mut self, visuals: &mut VisualWorld, h: CameraHandle) {
184        if self.active_window_camera == Some(h) {
185            return;
186        }
187
188        if let Some((_, cam)) = self.cameras.iter().find(|(ch, _)| *ch == h) {
189            self.active_window_camera = Some(h);
190            match *cam {
191                AnyCamera::Camera3D(cam3d) => {
192                    visuals.set_camera_mono_for_target_with_transform(
193                        crate::engine::graphics::CameraTarget::Window,
194                        cam3d.view,
195                        cam3d.proj,
196                        cam3d.transform,
197                    );
198                }
199                AnyCamera::Camera2D(cam2d) => {
200                    visuals.set_camera_mono_for_target_with_transform(
201                        crate::engine::graphics::CameraTarget::Window,
202                        cam2d.view,
203                        cam2d.proj,
204                        cam2d.transform,
205                    );
206                }
207            }
208        }
209    }
210
211    fn window_camera_component_enabled(&self, world: &World, handle: CameraHandle) -> bool {
212        if let Some(cid) = self.camera3d_components.get(&handle) {
213            return world
214                .get_component_by_id_as::<Camera3DComponent>(*cid)
215                .is_some_and(|c| {
216                    c.enabled && matches!(c.target, crate::engine::graphics::CameraTarget::Window)
217                });
218        }
219        if let Some(cid) = self.camera2d_components.get(&handle) {
220            return world
221                .get_component_by_id_as::<crate::engine::ecs::component::Camera2DComponent>(*cid)
222                .is_some_and(|c| {
223                    matches!(c.target, crate::engine::graphics::CameraTarget::Window)
224                });
225        }
226        false
227    }
228
229    fn update_active_window_camera(&mut self, world: &World, visuals: &mut VisualWorld) {
230        if let Some(active) = self.active_window_camera {
231            if self.window_camera_component_enabled(world, active) {
232                return;
233            }
234        }
235
236        let next = self
237            .cameras
238            .iter()
239            .map(|(handle, _)| *handle)
240            .rev()
241            .find(|&handle| self.window_camera_component_enabled(world, handle));
242
243        self.active_window_camera = next;
244
245        if let Some(handle) = next {
246            if let Some((_, cam)) = self.cameras.iter().find(|(ch, _)| *ch == handle) {
247                match *cam {
248                    AnyCamera::Camera3D(cam3d) => visuals
249                        .set_camera_mono_for_target_with_transform(
250                            crate::engine::graphics::CameraTarget::Window,
251                            cam3d.view,
252                            cam3d.proj,
253                            cam3d.transform,
254                        ),
255                    AnyCamera::Camera2D(cam2d) => visuals
256                        .set_camera_mono_for_target_with_transform(
257                            crate::engine::graphics::CameraTarget::Window,
258                            cam2d.view,
259                            cam2d.proj,
260                            cam2d.transform,
261                        ),
262                }
263            }
264        }
265    }
266
267    pub fn set_active_xr_camera(
268        &mut self,
269        world: &World,
270        visuals: &mut VisualWorld,
271        component: ComponentId,
272    ) {
273        // Only allow selecting an enabled XR camera; otherwise keep existing selection.
274        if world
275            .get_component_by_id_as::<CameraXRComponent>(component)
276            .is_some_and(|c| c.enabled)
277        {
278            self.active_xr_camera = Some(component);
279            visuals.set_active_xr_camera(Some(component));
280        }
281    }
282
283    /// Update Camera2D view/proj from the component tree.
284    ///
285    /// `camera2d_component_id` should be the Camera2DComponent, whose parent is typically a TransformComponent.
286    pub fn update_camera_2d_from_parent_transform(
287        &mut self,
288        world: &World,
289        visuals: &mut VisualWorld,
290        camera2d_component_id: ComponentId,
291        transform_component_id: ComponentId,
292    ) {
293        let Some(camera2d_comp) = world
294            .get_component_by_id_as::<crate::engine::ecs::component::Camera2DComponent>(
295                camera2d_component_id,
296            )
297        else {
298            return;
299        };
300
301        if let Some(handle) = camera2d_comp.handle {
302            if self.active_window_camera == Some(handle) {
303                // Maintain the old call sites' contract: ensure the provided parent really is a Transform.
304                if world
305                    .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(
306                        transform_component_id,
307                    )
308                    .is_none()
309                {
310                    return;
311                }
312
313                // Use the full accumulated world model for the camera component so nested transforms work.
314                let model = TransformSystem::world_model(world, camera2d_component_id)
315                    .unwrap_or_else(|| Camera3D::identity().view);
316
317                let view = invert_affine_transform(&model);
318
319                // Match the previous shader-side aspect correction: scale X by (height/width).
320                let vp = visuals.viewport();
321                let inv_aspect = if vp[0] > 0.0 { vp[1] / vp[0] } else { 1.0 };
322                let proj = [
323                    [inv_aspect, 0.0, 0.0, 0.0],
324                    [0.0, 1.0, 0.0, 0.0],
325                    [0.0, 0.0, 1.0, 0.0],
326                    [0.0, 0.0, 0.0, 1.0],
327                ];
328
329                // Persist into the camera registry so switching cameras updates visuals immediately.
330                if let Some((_, AnyCamera::Camera2D(cam2d))) =
331                    self.cameras.iter_mut().find(|(ch, _)| *ch == handle)
332                {
333                    cam2d.view = view;
334                    cam2d.proj = proj;
335                    cam2d.transform = transform_from_matrix_world(model);
336                }
337
338                visuals.set_camera_mono_for_target_with_transform(
339                    crate::engine::graphics::CameraTarget::Window,
340                    view,
341                    proj,
342                    transform_from_matrix_world(model),
343                );
344            }
345        }
346    }
347
348    /// Register a Camera2D component.
349    pub fn register_camera2d(
350        &mut self,
351        _world: &mut World,
352        _visuals: &mut VisualWorld,
353        component: ComponentId,
354    ) -> CameraHandle {
355        let h = CameraHandle(self.next_handle);
356        self.next_handle = self.next_handle.wrapping_add(1);
357
358        self.cameras
359            .push((h, AnyCamera::Camera2D(Camera2D::identity())));
360        self.camera2d_components.insert(h, component);
361
362        // Newest becomes active (window target).
363        self.active_window_camera = Some(h);
364
365        h
366    }
367
368    pub fn active_window_camera_matrices(&self) -> Option<([[f32; 4]; 4], [[f32; 4]; 4])> {
369        let h = self.active_window_camera?;
370        let (_, cam) = self.cameras.iter().find(|(ch, _)| *ch == h)?;
371        match *cam {
372            AnyCamera::Camera3D(cam3d) => Some((cam3d.view, cam3d.proj)),
373            AnyCamera::Camera2D(cam2d) => Some((cam2d.view, cam2d.proj)),
374        }
375    }
376
377    pub fn has_active_window_camera(&self) -> bool {
378        self.active_window_camera_matrices().is_some()
379    }
380
381    /// Update Camera3D view/proj from the component tree.
382    ///
383    /// `camera3d_component_id` should be the Camera3DComponent, whose parent is typically a TransformComponent.
384    pub fn update_camera_3d_from_parent_transform(
385        &mut self,
386        world: &World,
387        visuals: &mut VisualWorld,
388        camera3d_component_id: ComponentId,
389        transform_component_id: ComponentId,
390    ) {
391        let Some(camera3d_comp) = world
392            .get_component_by_id_as::<crate::engine::ecs::component::Camera3DComponent>(
393                camera3d_component_id,
394            )
395        else {
396            return;
397        };
398
399        if let Some(handle) = camera3d_comp.handle {
400            if self.active_window_camera == Some(handle) {
401                // Maintain the old call sites' contract: ensure the provided parent really is a Transform.
402                if world
403                    .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(
404                        transform_component_id,
405                    )
406                    .is_none()
407                {
408                    return;
409                }
410
411                // Use the full accumulated world model for the camera component so nested transforms work.
412                let model = TransformSystem::world_model(world, camera3d_component_id)
413                    .unwrap_or_else(|| Camera3D::identity().view);
414                let view = invert_affine_transform(&model);
415
416                let vp = visuals.viewport();
417                let aspect = if vp[1] > 0.0 { vp[0] / vp[1] } else { 1.0 };
418                let proj = Camera3D::perspective_rh_zo(
419                    camera3d_comp.fov_y_degrees.to_radians(),
420                    aspect,
421                    camera3d_comp.z_near,
422                    camera3d_comp.z_far,
423                );
424
425                // Persist into the camera registry so switching cameras updates visuals immediately.
426                if let Some((_, AnyCamera::Camera3D(cam3d))) =
427                    self.cameras.iter_mut().find(|(ch, _)| *ch == handle)
428                {
429                    cam3d.view = view;
430                    cam3d.proj = proj;
431                    cam3d.transform = transform_from_matrix_world(model);
432                }
433
434                visuals.set_camera_mono_for_target_with_transform(
435                    crate::engine::graphics::CameraTarget::Window,
436                    view,
437                    proj,
438                    transform_from_matrix_world(model),
439                );
440            }
441        }
442    }
443
444    fn update_active_xr_camera(&mut self, world: &World, visuals: &mut VisualWorld) {
445        // If the current active XR camera is still enabled, keep it.
446        if let Some(active) = self.active_xr_camera {
447            if let Some(c) = world.get_component_by_id_as::<CameraXRComponent>(active) {
448                if c.enabled {
449                    visuals.set_active_xr_camera(Some(active));
450                    return;
451                }
452            }
453        }
454
455        // Otherwise, pick the first enabled XR camera component (if any).
456        let next = world.all_components().find(|&id| {
457            world
458                .get_component_by_id_as::<CameraXRComponent>(id)
459                .is_some_and(|c| {
460                    c.enabled && matches!(c.target, crate::engine::graphics::CameraTarget::Xr)
461                })
462        });
463
464        self.active_xr_camera = next;
465        visuals.set_active_xr_camera(next);
466    }
467}
468
469/// Invert an affine 4x4 transform matrix (upper 3x3 + translation).
470///
471/// Assumes the bottom row is `[0, 0, 0, 1]` (which matches `Transform::recompute_model`).
472/// Returns identity if the 3x3 part is singular.
473fn invert_affine_transform(m: &[[f32; 4]; 4]) -> [[f32; 4]; 4] {
474    // Upper-left 3x3 in column-major.
475    let c0 = [m[0][0], m[0][1], m[0][2]];
476    let c1 = [m[1][0], m[1][1], m[1][2]];
477    let c2 = [m[2][0], m[2][1], m[2][2]];
478
479    // Row-major elements for determinant/cofactors.
480    let a00 = c0[0];
481    let a10 = c0[1];
482    let a20 = c0[2];
483    let a01 = c1[0];
484    let a11 = c1[1];
485    let a21 = c1[2];
486    let a02 = c2[0];
487    let a12 = c2[1];
488    let a22 = c2[2];
489
490    let det = a00 * (a11 * a22 - a12 * a21) - a01 * (a10 * a22 - a12 * a20)
491        + a02 * (a10 * a21 - a11 * a20);
492
493    if det.abs() < 1e-8 {
494        return Camera3D::identity().view;
495    }
496    let inv_det = 1.0 / det;
497
498    // Inverse in row-major.
499    let inv00 = (a11 * a22 - a12 * a21) * inv_det;
500    let inv01 = (a02 * a21 - a01 * a22) * inv_det;
501    let inv02 = (a01 * a12 - a02 * a11) * inv_det;
502
503    let inv10 = (a12 * a20 - a10 * a22) * inv_det;
504    let inv11 = (a00 * a22 - a02 * a20) * inv_det;
505    let inv12 = (a02 * a10 - a00 * a12) * inv_det;
506
507    let inv20 = (a10 * a21 - a11 * a20) * inv_det;
508    let inv21 = (a01 * a20 - a00 * a21) * inv_det;
509    let inv22 = (a00 * a11 - a01 * a10) * inv_det;
510
511    // Translation.
512    let tx = m[3][0];
513    let ty = m[3][1];
514    let tz = m[3][2];
515
516    let itx = -(inv00 * tx + inv01 * ty + inv02 * tz);
517    let ity = -(inv10 * tx + inv11 * ty + inv12 * tz);
518    let itz = -(inv20 * tx + inv21 * ty + inv22 * tz);
519
520    [
521        [inv00, inv10, inv20, 0.0],
522        [inv01, inv11, inv21, 0.0],
523        [inv02, inv12, inv22, 0.0],
524        [itx, ity, itz, 1.0],
525    ]
526}
527
528impl System for CameraSystem {
529    fn tick(
530        &mut self,
531        world: &mut World,
532        visuals: &mut VisualWorld,
533        _input: &crate::engine::user_input::InputState,
534        _dt_sec: f32,
535    ) {
536        self.update_active_window_camera(world, visuals);
537        // Maintain which XR rig is active so the OpenXR system can apply the correct world transform.
538        self.update_active_xr_camera(world, visuals);
539
540        let vp = visuals.viewport();
541        let prev_vp = self.last_viewport;
542        self.last_viewport = Some(vp);
543
544        // If the viewport changed, projections that depend on aspect ratio may need updating.
545        // View matrices are updated event-driven via TransformSystem::transform_changed.
546        let viewport_changed = prev_vp != Some(vp);
547
548        if !viewport_changed {
549            return;
550        }
551
552        let Some(active_handle) = self.active_window_camera else {
553            // No camera in the scene: keep the legacy behavior where 2D content is aspect-correct.
554            // Previously this was done in the vertex shader via `ubo.viewport`.
555            let inv_aspect = if vp[0] > 0.0 { vp[1] / vp[0] } else { 1.0 };
556            let view = Camera3D::identity().view;
557            let proj = [
558                [inv_aspect, 0.0, 0.0, 0.0],
559                [0.0, 1.0, 0.0, 0.0],
560                [0.0, 0.0, 1.0, 0.0],
561                [0.0, 0.0, 0.0, 1.0],
562            ];
563            visuals.set_camera_mono_for_target_with_transform(
564                crate::engine::graphics::CameraTarget::Window,
565                view,
566                proj,
567                Transform::default(),
568            );
569            return;
570        };
571
572        // View is already up-to-date via TransformSystem; on resize we only need to refresh proj.
573        let Some((_, cam)) = self.cameras.iter_mut().find(|(ch, _)| *ch == active_handle) else {
574            return;
575        };
576
577        match cam {
578            AnyCamera::Camera2D(cam2d) => {
579                let inv_aspect = if vp[0] > 0.0 { vp[1] / vp[0] } else { 1.0 };
580                cam2d.proj = [
581                    [inv_aspect, 0.0, 0.0, 0.0],
582                    [0.0, 1.0, 0.0, 0.0],
583                    [0.0, 0.0, 1.0, 0.0],
584                    [0.0, 0.0, 0.0, 1.0],
585                ];
586                visuals.set_camera_mono_for_target_with_transform(
587                    crate::engine::graphics::CameraTarget::Window,
588                    cam2d.view,
589                    cam2d.proj,
590                    cam2d.transform,
591                );
592            }
593            AnyCamera::Camera3D(cam3d) => {
594                let aspect = if vp[1] > 0.0 { vp[0] / vp[1] } else { 1.0 };
595
596                let (fov_y_deg, z_near, z_far) = self
597                    .camera3d_components
598                    .get(&active_handle)
599                    .and_then(|cid| world.get_component_by_id_as::<Camera3DComponent>(*cid))
600                    .map(|c| (c.fov_y_degrees, c.z_near, c.z_far))
601                    .unwrap_or((
602                        Camera3DComponent::DEFAULT_FOV_Y_DEGREES,
603                        Camera3DComponent::DEFAULT_Z_NEAR,
604                        Camera3DComponent::DEFAULT_Z_FAR,
605                    ));
606
607                cam3d.proj =
608                    Camera3D::perspective_rh_zo(fov_y_deg.to_radians(), aspect, z_near, z_far);
609                visuals.set_camera_mono_for_target_with_transform(
610                    crate::engine::graphics::CameraTarget::Window,
611                    cam3d.view,
612                    cam3d.proj,
613                    cam3d.transform,
614                );
615            }
616        }
617    }
618}
619
620fn transform_from_matrix_world(m: [[f32; 4]; 4]) -> Transform {
621    let mut t = Transform::default();
622    t.model = m;
623    t.matrix_world = m;
624    t.translation = [m[3][0], m[3][1], m[3][2]];
625    t
626}