mittens-engine 0.6.0

A Vulkan and OpenXR scene engine with ECS, reactive signals, and Meow Meow scripting
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
use crate::engine::ecs::ComponentId;
use crate::engine::ecs::World;
use crate::engine::ecs::component::Camera3DComponent;
use crate::engine::ecs::component::CameraXRComponent;
use crate::engine::ecs::system::System;
use crate::engine::ecs::system::TransformSystem;
use crate::engine::graphics::VisualWorld;
use crate::engine::graphics::primitives::Transform;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CameraHandle(pub u32);

#[derive(Debug, Clone, Copy)]
pub struct Camera3D {
    pub view: [[f32; 4]; 4],
    pub proj: [[f32; 4]; 4],
    pub transform: Transform,
}

#[derive(Debug, Clone, Copy)]
pub struct Camera2D {
    pub view: [[f32; 4]; 4],
    pub proj: [[f32; 4]; 4],
    pub transform: Transform,
}

#[derive(Debug, Clone, Copy)]
enum AnyCamera {
    Camera3D(Camera3D),
    Camera2D(Camera2D),
}

impl Camera3D {
    pub fn identity() -> Self {
        Self {
            view: [
                [1.0, 0.0, 0.0, 0.0],
                [0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 1.0, 0.0],
                [0.0, 0.0, 0.0, 1.0],
            ],
            proj: [
                [1.0, 0.0, 0.0, 0.0],
                [0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 1.0, 0.0],
                [0.0, 0.0, 0.0, 1.0],
            ],
            transform: Transform::default(),
        }
    }

    /// Right-handed perspective projection matrix.
    ///
    /// Assumptions:
    /// - Column-major mat4 (matches how we pack instance matrices / GLSL default).
    /// - NDC depth range is Vulkan-style: z in [0, 1].
    pub fn perspective_rh_zo(
        fov_y_radians: f32,
        aspect: f32,
        z_near: f32,
        z_far: f32,
    ) -> [[f32; 4]; 4] {
        // Based on the standard RH, zero-to-one depth projection.
        // Maps camera forward -Z.
        let f = 1.0 / (0.5 * fov_y_radians).tan();
        let nf = 1.0 / (z_near - z_far);

        // Column-major:
        // [ f/aspect, 0,  0,                      0 ]
        // [ 0,        f,  0,                      0 ]
        // [ 0,        0,  z_far*nf,               -1 ]
        // [ 0,        0,  z_near*z_far*nf,         0 ]
        [
            [f / aspect, 0.0, 0.0, 0.0],
            [0.0, f, 0.0, 0.0],
            [0.0, 0.0, z_far * nf, -1.0],
            [0.0, 0.0, (z_near * z_far) * nf, 0.0],
        ]
    }
}

impl Camera2D {
    pub fn identity() -> Self {
        Self {
            view: [
                [1.0, 0.0, 0.0, 0.0],
                [0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 1.0, 0.0],
                [0.0, 0.0, 0.0, 1.0],
            ],
            proj: [
                [1.0, 0.0, 0.0, 0.0],
                [0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 1.0, 0.0],
                [0.0, 0.0, 0.0, 1.0],
            ],
            transform: Transform::default(),
        }
    }
}

#[derive(Debug, Default)]
pub struct CameraSystem {
    next_handle: u32,
    cameras: Vec<(CameraHandle, AnyCamera)>,
    camera2d_components: std::collections::HashMap<CameraHandle, ComponentId>,
    camera3d_components: std::collections::HashMap<CameraHandle, ComponentId>,
    pub active_window_camera: Option<CameraHandle>,
    pub active_xr_camera: Option<ComponentId>,

    // Track viewport changes for the no-camera fallback projection.
    last_viewport: Option<[f32; 2]>,
}

impl CameraSystem {
    pub fn new() -> Self {
        Self::default()
    }

    /// Registers a camera derived from the component tree.
    ///
    /// The newest registered camera becomes active.
    pub fn register_camera(
        &mut self,
        world: &mut World,
        visuals: &mut VisualWorld,
        component: ComponentId,
    ) -> CameraHandle {
        // Default 3D camera parameters.
        let (w, h) = {
            let vp = visuals.viewport();
            (vp[0], vp[1])
        };
        let aspect = if h > 0.0 { w / h } else { 1.0 };

        let (fov_y_deg, z_near, z_far) = world
            .get_component_by_id_as::<Camera3DComponent>(component)
            .map(|c| (c.fov_y_degrees, c.z_near, c.z_far))
            .unwrap_or((
                Camera3DComponent::DEFAULT_FOV_Y_DEGREES,
                Camera3DComponent::DEFAULT_Z_NEAR,
                Camera3DComponent::DEFAULT_Z_FAR,
            ));
        let proj = Camera3D::perspective_rh_zo(fov_y_deg.to_radians(), aspect, z_near, z_far);

        // If the camera is parented under a TransformComponent, use that transform as the camera pose.
        // Otherwise default to identity view.
        let (view, transform) = if let Some(model) = TransformSystem::world_model(world, component)
        {
            (
                invert_affine_transform(&model),
                transform_from_matrix_world(model),
            )
        } else {
            let ident = Camera3D::identity();
            (ident.view, ident.transform)
        };

        let cam = Camera3D {
            view,
            proj,
            transform,
        };

        let h = CameraHandle(self.next_handle);
        self.next_handle = self.next_handle.wrapping_add(1);

        self.cameras.push((h, AnyCamera::Camera3D(cam)));
        self.camera3d_components.insert(h, component);

        // Newest becomes active (window target).
        self.active_window_camera = Some(h);
        visuals.set_camera_mono_for_target_with_transform(
            crate::engine::graphics::CameraTarget::Window,
            cam.view,
            cam.proj,
            cam.transform,
        );

        h
    }

    pub fn set_active_window_camera(&mut self, visuals: &mut VisualWorld, h: CameraHandle) {
        if self.active_window_camera == Some(h) {
            return;
        }

        if let Some((_, cam)) = self.cameras.iter().find(|(ch, _)| *ch == h) {
            self.active_window_camera = Some(h);
            match *cam {
                AnyCamera::Camera3D(cam3d) => {
                    visuals.set_camera_mono_for_target_with_transform(
                        crate::engine::graphics::CameraTarget::Window,
                        cam3d.view,
                        cam3d.proj,
                        cam3d.transform,
                    );
                }
                AnyCamera::Camera2D(cam2d) => {
                    visuals.set_camera_mono_for_target_with_transform(
                        crate::engine::graphics::CameraTarget::Window,
                        cam2d.view,
                        cam2d.proj,
                        cam2d.transform,
                    );
                }
            }
        }
    }

    fn window_camera_component_enabled(&self, world: &World, handle: CameraHandle) -> bool {
        if let Some(cid) = self.camera3d_components.get(&handle) {
            return world
                .get_component_by_id_as::<Camera3DComponent>(*cid)
                .is_some_and(|c| {
                    c.enabled && matches!(c.target, crate::engine::graphics::CameraTarget::Window)
                });
        }
        if let Some(cid) = self.camera2d_components.get(&handle) {
            return world
                .get_component_by_id_as::<crate::engine::ecs::component::Camera2DComponent>(*cid)
                .is_some_and(|c| {
                    matches!(c.target, crate::engine::graphics::CameraTarget::Window)
                });
        }
        false
    }

    fn update_active_window_camera(&mut self, world: &World, visuals: &mut VisualWorld) {
        if let Some(active) = self.active_window_camera {
            if self.window_camera_component_enabled(world, active) {
                return;
            }
        }

        let next = self
            .cameras
            .iter()
            .map(|(handle, _)| *handle)
            .rev()
            .find(|&handle| self.window_camera_component_enabled(world, handle));

        self.active_window_camera = next;

        if let Some(handle) = next {
            if let Some((_, cam)) = self.cameras.iter().find(|(ch, _)| *ch == handle) {
                match *cam {
                    AnyCamera::Camera3D(cam3d) => visuals
                        .set_camera_mono_for_target_with_transform(
                            crate::engine::graphics::CameraTarget::Window,
                            cam3d.view,
                            cam3d.proj,
                            cam3d.transform,
                        ),
                    AnyCamera::Camera2D(cam2d) => visuals
                        .set_camera_mono_for_target_with_transform(
                            crate::engine::graphics::CameraTarget::Window,
                            cam2d.view,
                            cam2d.proj,
                            cam2d.transform,
                        ),
                }
            }
        }
    }

    pub fn set_active_xr_camera(
        &mut self,
        world: &World,
        visuals: &mut VisualWorld,
        component: ComponentId,
    ) {
        // Only allow selecting an enabled XR camera; otherwise keep existing selection.
        if world
            .get_component_by_id_as::<CameraXRComponent>(component)
            .is_some_and(|c| c.enabled)
        {
            self.active_xr_camera = Some(component);
            visuals.set_active_xr_camera(Some(component));
        }
    }

    /// Update Camera2D view/proj from the component tree.
    ///
    /// `camera2d_component_id` should be the Camera2DComponent, whose parent is typically a TransformComponent.
    pub fn update_camera_2d_from_parent_transform(
        &mut self,
        world: &World,
        visuals: &mut VisualWorld,
        camera2d_component_id: ComponentId,
        transform_component_id: ComponentId,
    ) {
        let Some(camera2d_comp) = world
            .get_component_by_id_as::<crate::engine::ecs::component::Camera2DComponent>(
                camera2d_component_id,
            )
        else {
            return;
        };

        if let Some(handle) = camera2d_comp.handle {
            if self.active_window_camera == Some(handle) {
                // Maintain the old call sites' contract: ensure the provided parent really is a Transform.
                if world
                    .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(
                        transform_component_id,
                    )
                    .is_none()
                {
                    return;
                }

                // Use the full accumulated world model for the camera component so nested transforms work.
                let model = TransformSystem::world_model(world, camera2d_component_id)
                    .unwrap_or_else(|| Camera3D::identity().view);

                let view = invert_affine_transform(&model);

                // Match the previous shader-side aspect correction: scale X by (height/width).
                let vp = visuals.viewport();
                let inv_aspect = if vp[0] > 0.0 { vp[1] / vp[0] } else { 1.0 };
                let proj = [
                    [inv_aspect, 0.0, 0.0, 0.0],
                    [0.0, 1.0, 0.0, 0.0],
                    [0.0, 0.0, 1.0, 0.0],
                    [0.0, 0.0, 0.0, 1.0],
                ];

                // Persist into the camera registry so switching cameras updates visuals immediately.
                if let Some((_, AnyCamera::Camera2D(cam2d))) =
                    self.cameras.iter_mut().find(|(ch, _)| *ch == handle)
                {
                    cam2d.view = view;
                    cam2d.proj = proj;
                    cam2d.transform = transform_from_matrix_world(model);
                }

                visuals.set_camera_mono_for_target_with_transform(
                    crate::engine::graphics::CameraTarget::Window,
                    view,
                    proj,
                    transform_from_matrix_world(model),
                );
            }
        }
    }

    /// Register a Camera2D component.
    pub fn register_camera2d(
        &mut self,
        _world: &mut World,
        _visuals: &mut VisualWorld,
        component: ComponentId,
    ) -> CameraHandle {
        let h = CameraHandle(self.next_handle);
        self.next_handle = self.next_handle.wrapping_add(1);

        self.cameras
            .push((h, AnyCamera::Camera2D(Camera2D::identity())));
        self.camera2d_components.insert(h, component);

        // Newest becomes active (window target).
        self.active_window_camera = Some(h);

        h
    }

    pub fn active_window_camera_matrices(&self) -> Option<([[f32; 4]; 4], [[f32; 4]; 4])> {
        let h = self.active_window_camera?;
        let (_, cam) = self.cameras.iter().find(|(ch, _)| *ch == h)?;
        match *cam {
            AnyCamera::Camera3D(cam3d) => Some((cam3d.view, cam3d.proj)),
            AnyCamera::Camera2D(cam2d) => Some((cam2d.view, cam2d.proj)),
        }
    }

    pub fn has_active_window_camera(&self) -> bool {
        self.active_window_camera_matrices().is_some()
    }

    /// Update Camera3D view/proj from the component tree.
    ///
    /// `camera3d_component_id` should be the Camera3DComponent, whose parent is typically a TransformComponent.
    pub fn update_camera_3d_from_parent_transform(
        &mut self,
        world: &World,
        visuals: &mut VisualWorld,
        camera3d_component_id: ComponentId,
        transform_component_id: ComponentId,
    ) {
        let Some(camera3d_comp) = world
            .get_component_by_id_as::<crate::engine::ecs::component::Camera3DComponent>(
                camera3d_component_id,
            )
        else {
            return;
        };

        if let Some(handle) = camera3d_comp.handle {
            if self.active_window_camera == Some(handle) {
                // Maintain the old call sites' contract: ensure the provided parent really is a Transform.
                if world
                    .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(
                        transform_component_id,
                    )
                    .is_none()
                {
                    return;
                }

                // Use the full accumulated world model for the camera component so nested transforms work.
                let model = TransformSystem::world_model(world, camera3d_component_id)
                    .unwrap_or_else(|| Camera3D::identity().view);
                let view = invert_affine_transform(&model);

                let vp = visuals.viewport();
                let aspect = if vp[1] > 0.0 { vp[0] / vp[1] } else { 1.0 };
                let proj = Camera3D::perspective_rh_zo(
                    camera3d_comp.fov_y_degrees.to_radians(),
                    aspect,
                    camera3d_comp.z_near,
                    camera3d_comp.z_far,
                );

                // Persist into the camera registry so switching cameras updates visuals immediately.
                if let Some((_, AnyCamera::Camera3D(cam3d))) =
                    self.cameras.iter_mut().find(|(ch, _)| *ch == handle)
                {
                    cam3d.view = view;
                    cam3d.proj = proj;
                    cam3d.transform = transform_from_matrix_world(model);
                }

                visuals.set_camera_mono_for_target_with_transform(
                    crate::engine::graphics::CameraTarget::Window,
                    view,
                    proj,
                    transform_from_matrix_world(model),
                );
            }
        }
    }

    fn update_active_xr_camera(&mut self, world: &World, visuals: &mut VisualWorld) {
        // If the current active XR camera is still enabled, keep it.
        if let Some(active) = self.active_xr_camera {
            if let Some(c) = world.get_component_by_id_as::<CameraXRComponent>(active) {
                if c.enabled {
                    visuals.set_active_xr_camera(Some(active));
                    return;
                }
            }
        }

        // Otherwise, pick the first enabled XR camera component (if any).
        let next = world.all_components().find(|&id| {
            world
                .get_component_by_id_as::<CameraXRComponent>(id)
                .is_some_and(|c| {
                    c.enabled && matches!(c.target, crate::engine::graphics::CameraTarget::Xr)
                })
        });

        self.active_xr_camera = next;
        visuals.set_active_xr_camera(next);
    }
}

/// Invert an affine 4x4 transform matrix (upper 3x3 + translation).
///
/// Assumes the bottom row is `[0, 0, 0, 1]` (which matches `Transform::recompute_model`).
/// Returns identity if the 3x3 part is singular.
fn invert_affine_transform(m: &[[f32; 4]; 4]) -> [[f32; 4]; 4] {
    // Upper-left 3x3 in column-major.
    let c0 = [m[0][0], m[0][1], m[0][2]];
    let c1 = [m[1][0], m[1][1], m[1][2]];
    let c2 = [m[2][0], m[2][1], m[2][2]];

    // Row-major elements for determinant/cofactors.
    let a00 = c0[0];
    let a10 = c0[1];
    let a20 = c0[2];
    let a01 = c1[0];
    let a11 = c1[1];
    let a21 = c1[2];
    let a02 = c2[0];
    let a12 = c2[1];
    let a22 = c2[2];

    let det = a00 * (a11 * a22 - a12 * a21) - a01 * (a10 * a22 - a12 * a20)
        + a02 * (a10 * a21 - a11 * a20);

    if det.abs() < 1e-8 {
        return Camera3D::identity().view;
    }
    let inv_det = 1.0 / det;

    // Inverse in row-major.
    let inv00 = (a11 * a22 - a12 * a21) * inv_det;
    let inv01 = (a02 * a21 - a01 * a22) * inv_det;
    let inv02 = (a01 * a12 - a02 * a11) * inv_det;

    let inv10 = (a12 * a20 - a10 * a22) * inv_det;
    let inv11 = (a00 * a22 - a02 * a20) * inv_det;
    let inv12 = (a02 * a10 - a00 * a12) * inv_det;

    let inv20 = (a10 * a21 - a11 * a20) * inv_det;
    let inv21 = (a01 * a20 - a00 * a21) * inv_det;
    let inv22 = (a00 * a11 - a01 * a10) * inv_det;

    // Translation.
    let tx = m[3][0];
    let ty = m[3][1];
    let tz = m[3][2];

    let itx = -(inv00 * tx + inv01 * ty + inv02 * tz);
    let ity = -(inv10 * tx + inv11 * ty + inv12 * tz);
    let itz = -(inv20 * tx + inv21 * ty + inv22 * tz);

    [
        [inv00, inv10, inv20, 0.0],
        [inv01, inv11, inv21, 0.0],
        [inv02, inv12, inv22, 0.0],
        [itx, ity, itz, 1.0],
    ]
}

impl System for CameraSystem {
    fn tick(
        &mut self,
        world: &mut World,
        visuals: &mut VisualWorld,
        _input: &crate::engine::user_input::InputState,
        _dt_sec: f32,
    ) {
        self.update_active_window_camera(world, visuals);
        // Maintain which XR rig is active so the OpenXR system can apply the correct world transform.
        self.update_active_xr_camera(world, visuals);

        let vp = visuals.viewport();
        let prev_vp = self.last_viewport;
        self.last_viewport = Some(vp);

        // If the viewport changed, projections that depend on aspect ratio may need updating.
        // View matrices are updated event-driven via TransformSystem::transform_changed.
        let viewport_changed = prev_vp != Some(vp);

        if !viewport_changed {
            return;
        }

        let Some(active_handle) = self.active_window_camera else {
            // No camera in the scene: keep the legacy behavior where 2D content is aspect-correct.
            // Previously this was done in the vertex shader via `ubo.viewport`.
            let inv_aspect = if vp[0] > 0.0 { vp[1] / vp[0] } else { 1.0 };
            let view = Camera3D::identity().view;
            let proj = [
                [inv_aspect, 0.0, 0.0, 0.0],
                [0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 1.0, 0.0],
                [0.0, 0.0, 0.0, 1.0],
            ];
            visuals.set_camera_mono_for_target_with_transform(
                crate::engine::graphics::CameraTarget::Window,
                view,
                proj,
                Transform::default(),
            );
            return;
        };

        // View is already up-to-date via TransformSystem; on resize we only need to refresh proj.
        let Some((_, cam)) = self.cameras.iter_mut().find(|(ch, _)| *ch == active_handle) else {
            return;
        };

        match cam {
            AnyCamera::Camera2D(cam2d) => {
                let inv_aspect = if vp[0] > 0.0 { vp[1] / vp[0] } else { 1.0 };
                cam2d.proj = [
                    [inv_aspect, 0.0, 0.0, 0.0],
                    [0.0, 1.0, 0.0, 0.0],
                    [0.0, 0.0, 1.0, 0.0],
                    [0.0, 0.0, 0.0, 1.0],
                ];
                visuals.set_camera_mono_for_target_with_transform(
                    crate::engine::graphics::CameraTarget::Window,
                    cam2d.view,
                    cam2d.proj,
                    cam2d.transform,
                );
            }
            AnyCamera::Camera3D(cam3d) => {
                let aspect = if vp[1] > 0.0 { vp[0] / vp[1] } else { 1.0 };

                let (fov_y_deg, z_near, z_far) = self
                    .camera3d_components
                    .get(&active_handle)
                    .and_then(|cid| world.get_component_by_id_as::<Camera3DComponent>(*cid))
                    .map(|c| (c.fov_y_degrees, c.z_near, c.z_far))
                    .unwrap_or((
                        Camera3DComponent::DEFAULT_FOV_Y_DEGREES,
                        Camera3DComponent::DEFAULT_Z_NEAR,
                        Camera3DComponent::DEFAULT_Z_FAR,
                    ));

                cam3d.proj =
                    Camera3D::perspective_rh_zo(fov_y_deg.to_radians(), aspect, z_near, z_far);
                visuals.set_camera_mono_for_target_with_transform(
                    crate::engine::graphics::CameraTarget::Window,
                    cam3d.view,
                    cam3d.proj,
                    cam3d.transform,
                );
            }
        }
    }
}

fn transform_from_matrix_world(m: [[f32; 4]; 4]) -> Transform {
    let mut t = Transform::default();
    t.model = m;
    t.matrix_world = m;
    t.translation = [m[3][0], m[3][1], m[3][2]];
    t
}