Skip to main content

mittens_engine/engine/ecs/system/
mirror_system.rs

1use crate::engine::ecs::component::{
2    BoundsComponent, MirrorComponent, RenderableComponent, TextureComponent, TransformComponent,
3};
4use crate::engine::ecs::system::System;
5use crate::engine::ecs::{ComponentId, World};
6use crate::engine::graphics::primitives::{InstanceHandle, MaterialHandle};
7use crate::engine::graphics::visual_world::{
8    MirrorCaptureRequest, MirrorViewerFamily, VisualMirror,
9};
10use crate::engine::graphics::{CameraData, CameraTarget, VisualWorld};
11use crate::engine::user_input::InputState;
12use crate::utils::math;
13use std::collections::HashSet;
14use uuid::Uuid;
15use winit::event::MouseButton;
16
17const MIRROR_CLIP_BIAS_WORLD_UNITS: f32 = 0.01;
18const MIRROR_ENABLE_OBLIQUE_CLIP_PLANE: bool = false;
19
20#[derive(Debug, Default)]
21pub struct MirrorSystem {
22    pending_texture_registrations: Vec<ComponentId>,
23    logged_debug_sample: bool,
24}
25
26impl MirrorSystem {
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    pub fn take_pending_texture_registrations(&mut self) -> Vec<ComponentId> {
32        std::mem::take(&mut self.pending_texture_registrations)
33    }
34
35    fn normalize(v: [f32; 3]) -> Option<[f32; 3]> {
36        let len2 = math::vec3_dot(v, v);
37        if len2 <= 1e-12 {
38            return None;
39        }
40        let inv_len = len2.sqrt().recip();
41        Some([v[0] * inv_len, v[1] * inv_len, v[2] * inv_len])
42    }
43
44    fn mat4_mul_vec4(m: [[f32; 4]; 4], v: [f32; 4]) -> [f32; 4] {
45        [
46            m[0][0] * v[0] + m[1][0] * v[1] + m[2][0] * v[2] + m[3][0] * v[3],
47            m[0][1] * v[0] + m[1][1] * v[1] + m[2][1] * v[2] + m[3][1] * v[3],
48            m[0][2] * v[0] + m[1][2] * v[1] + m[2][2] * v[2] + m[3][2] * v[3],
49            m[0][3] * v[0] + m[1][3] * v[1] + m[2][3] * v[2] + m[3][3] * v[3],
50        ]
51    }
52
53    fn transform_plane_world_to_camera(
54        view: [[f32; 4]; 4],
55        plane_origin: [f32; 3],
56        plane_normal_toward_camera: [f32; 3],
57    ) -> Option<[f32; 4]> {
58        let origin4 = Self::mat4_mul_vec4(
59            view,
60            [plane_origin[0], plane_origin[1], plane_origin[2], 1.0],
61        );
62        let normal4 = Self::mat4_mul_vec4(
63            view,
64            [
65                plane_normal_toward_camera[0],
66                plane_normal_toward_camera[1],
67                plane_normal_toward_camera[2],
68                0.0,
69            ],
70        );
71        let normal = Self::normalize([normal4[0], normal4[1], normal4[2]])?;
72        let origin = [origin4[0], origin4[1], origin4[2]];
73        Some([
74            normal[0],
75            normal[1],
76            normal[2],
77            -math::vec3_dot(normal, origin),
78        ])
79    }
80
81    fn projection_inverse_corner(proj: [[f32; 4]; 4], clip: [f32; 4]) -> Option<[f32; 4]> {
82        let inv_proj = math::mat4_inverse(proj)?;
83        Some(Self::mat4_mul_vec4(inv_proj, clip))
84    }
85
86    fn apply_oblique_near_plane_projection(
87        proj: [[f32; 4]; 4],
88        plane_camera: [f32; 4],
89    ) -> Option<[[f32; 4]; 4]> {
90        let clip_corner = [
91            if plane_camera[0] >= 0.0 { 1.0 } else { -1.0 },
92            if plane_camera[1] >= 0.0 { 1.0 } else { -1.0 },
93            1.0,
94            1.0,
95        ];
96        let q = Self::projection_inverse_corner(proj, clip_corner)?;
97        let dot = plane_camera[0] * q[0]
98            + plane_camera[1] * q[1]
99            + plane_camera[2] * q[2]
100            + plane_camera[3] * q[3];
101        if dot.abs() <= 1e-6 {
102            return None;
103        }
104
105        let scale = 1.0 / dot;
106        let mut out = proj;
107        out[0][2] = plane_camera[0] * scale;
108        out[1][2] = plane_camera[1] * scale;
109        out[2][2] = plane_camera[2] * scale;
110        out[3][2] = plane_camera[3] * scale;
111        Some(out)
112    }
113
114    fn mirror_local_basis(world_matrix: [[f32; 4]; 4]) -> Option<([f32; 3], [f32; 3], [f32; 3])> {
115        let x = Self::normalize([world_matrix[0][0], world_matrix[0][1], world_matrix[0][2]])?;
116        let y = Self::normalize([world_matrix[1][0], world_matrix[1][1], world_matrix[1][2]])?;
117        let z = Self::normalize([world_matrix[2][0], world_matrix[2][1], world_matrix[2][2]])?;
118        Some((x, y, z))
119    }
120
121    fn build_camera_world_from_forward_up(
122        position: [f32; 3],
123        forward: [f32; 3],
124        up_hint: [f32; 3],
125    ) -> Option<[[f32; 4]; 4]> {
126        let forward = Self::normalize(forward)?;
127        let up_hint = Self::normalize(up_hint)?;
128        let right = Self::normalize(math::vec3_cross(forward, up_hint)).or_else(|| {
129            let fallback_up = if forward[1].abs() < 0.999 {
130                [0.0, 1.0, 0.0]
131            } else {
132                [1.0, 0.0, 0.0]
133            };
134            Self::normalize(math::vec3_cross(forward, fallback_up))
135        })?;
136        let up = Self::normalize(math::vec3_cross(right, forward))?;
137        let back = math::vec3_negate(forward);
138        Some([
139            [right[0], right[1], right[2], 0.0],
140            [up[0], up[1], up[2], 0.0],
141            [back[0], back[1], back[2], 0.0],
142            [position[0], position[1], position[2], 1.0],
143        ])
144    }
145
146    fn camera_space_off_axis_projection(
147        left: f32,
148        right: f32,
149        bottom: f32,
150        top: f32,
151        z_near: f32,
152        z_far: f32,
153    ) -> Option<[[f32; 4]; 4]> {
154        if !(z_near.is_finite() && z_far.is_finite()) || z_near <= 0.0 || z_far <= z_near {
155            return None;
156        }
157        let width = right - left;
158        let height = top - bottom;
159        if width.abs() <= 1e-6 || height.abs() <= 1e-6 {
160            return None;
161        }
162        let two_near = 2.0 * z_near;
163        let nf = 1.0 / (z_near - z_far);
164        Some([
165            [two_near / width, 0.0, 0.0, 0.0],
166            [0.0, two_near / height, 0.0, 0.0],
167            [
168                (right + left) / width,
169                (top + bottom) / height,
170                z_far * nf,
171                -1.0,
172            ],
173            [0.0, 0.0, (z_near * z_far) * nf, 0.0],
174        ])
175    }
176
177    fn log_debug_sample_once(
178        &mut self,
179        force: bool,
180        family: MirrorViewerFamily,
181        view_index: usize,
182        mirror_guid: &Uuid,
183        plane_pos: [f32; 3],
184        world_matrix: [[f32; 4]; 4],
185        cam_pos: [f32; 3],
186        ref_pos: [f32; 3],
187        cam_forward: [f32; 3],
188        ref_forward: [f32; 3],
189        cam_up: [f32; 3],
190        ref_up: [f32; 3],
191    ) {
192        if self.logged_debug_sample && !force {
193            return;
194        }
195        let Some((mirror_x, mirror_y, mirror_z)) = Self::mirror_local_basis(world_matrix) else {
196            return;
197        };
198
199        let source_from_plane = math::vec3_sub(cam_pos, plane_pos);
200        let reflected_from_plane = math::vec3_sub(ref_pos, plane_pos);
201        let source_local = [
202            math::vec3_dot(source_from_plane, mirror_x),
203            math::vec3_dot(source_from_plane, mirror_y),
204            math::vec3_dot(source_from_plane, mirror_z),
205        ];
206        let reflected_local = [
207            math::vec3_dot(reflected_from_plane, mirror_x),
208            math::vec3_dot(reflected_from_plane, mirror_y),
209            math::vec3_dot(reflected_from_plane, mirror_z),
210        ];
211
212        let _ = (
213            mirror_guid,
214            family,
215            view_index,
216            plane_pos,
217            mirror_x,
218            mirror_y,
219            mirror_z,
220            cam_pos,
221            source_local,
222            ref_pos,
223            reflected_local,
224            cam_forward,
225            ref_forward,
226            cam_up,
227            ref_up,
228        );
229
230        if !force {
231            self.logged_debug_sample = true;
232        }
233    }
234}
235
236impl System for MirrorSystem {
237    fn tick(
238        &mut self,
239        world: &mut World,
240        visuals: &mut VisualWorld,
241        input: &InputState,
242        _dt_sec: f32,
243    ) {
244        visuals.clear_mirrors();
245        let mut pending_texture_registrations = HashSet::new();
246        let force_debug_dump = input.mouse_pressed.contains(&MouseButton::Left);
247
248        let mirror_cids: Vec<ComponentId> = world
249            .all_components()
250            .filter(|&id| {
251                world
252                    .get_component_by_id_as::<MirrorComponent>(id)
253                    .is_some()
254            })
255            .collect();
256
257        for cid in mirror_cids {
258            let quality = world
259                .get_component_by_id_as::<MirrorComponent>(cid)
260                .map(|m| m.quality)
261                .unwrap_or(512);
262
263            // 1. Find transform (nearest ancestor).
264            let mut transform_cid = None;
265            let mut cur = cid;
266            while let Some(p) = world.parent_of(cur) {
267                if world
268                    .get_component_by_id_as::<TransformComponent>(p)
269                    .is_some()
270                {
271                    transform_cid = Some(p);
272                    break;
273                }
274                cur = p;
275            }
276            let Some(transform_cid) = transform_cid else {
277                continue;
278            };
279            let transform = world
280                .get_component_by_id_as::<TransformComponent>(transform_cid)
281                .unwrap();
282            let world_matrix = transform.transform.matrix_world;
283
284            // 2. Find parent renderable (the surface that should be reflective).
285            let mut renderable_cid = None;
286            if let Some(p) = world.parent_of(cid) {
287                if world
288                    .get_component_by_id_as::<RenderableComponent>(p)
289                    .is_some()
290                {
291                    renderable_cid = Some(p);
292                }
293            }
294            let Some(renderable_cid) = renderable_cid else {
295                continue;
296            };
297
298            // 3. Find bounds (for aspect ratio).
299            let bounds = world
300                .get_component_by_id_as::<BoundsComponent>(renderable_cid)
301                .map(|b| b.local)
302                .or_else(|| {
303                    // Try children of renderable.
304                    world.children_of(renderable_cid).iter().find_map(|&ch| {
305                        world
306                            .get_component_by_id_as::<BoundsComponent>(ch)
307                            .map(|b| b.local)
308                    })
309                });
310
311            // Mirror surface world-space dimensions (used for mirror-sized frustum).
312            let (mirror_world_w, mirror_world_h) = if let Some(b) = bounds {
313                let sx =
314                    math::vec3_len([world_matrix[0][0], world_matrix[0][1], world_matrix[0][2]]);
315                let sy =
316                    math::vec3_len([world_matrix[1][0], world_matrix[1][1], world_matrix[1][2]]);
317                (
318                    (b.max[0] - b.min[0]).abs() * sx,
319                    (b.max[1] - b.min[1]).abs() * sy,
320                )
321            } else {
322                (1.0, 1.0)
323            };
324            let mirror_aspect = if mirror_world_h > 1e-6 {
325                mirror_world_w / mirror_world_h
326            } else {
327                1.0
328            };
329
330            // Mirror plane in world space.
331            // Start from the transform origin, then move the plane onto the renderable's
332            // visible +Z face so thick mirror slabs reflect from the surface you actually see,
333            // not from the center of the volume.
334            let mut plane_pos = [world_matrix[3][0], world_matrix[3][1], world_matrix[3][2]];
335            let plane_normal = {
336                let n = [world_matrix[2][0], world_matrix[2][1], world_matrix[2][2]];
337                let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
338                if len > 1e-6 {
339                    [n[0] / len, n[1] / len, n[2] / len]
340                } else {
341                    [0.0, 0.0, 1.0]
342                }
343            };
344            if let Some(b) = bounds {
345                let z_axis_len = {
346                    let z = [world_matrix[2][0], world_matrix[2][1], world_matrix[2][2]];
347                    (z[0] * z[0] + z[1] * z[1] + z[2] * z[2]).sqrt()
348                };
349                if z_axis_len > 1e-6 {
350                    plane_pos = math::vec3_add(
351                        plane_pos,
352                        math::vec3_scale(plane_normal, b.max[2] * z_axis_len),
353                    );
354                }
355            }
356
357            // 4. Derive reflected camera views for each active viewer family.
358            let mut captures = Vec::new();
359            let guid = world
360                .get_component_node(cid)
361                .map(|n| n.guid)
362                .unwrap_or_default();
363            let source_families = [
364                (CameraTarget::Window, MirrorViewerFamily::Monoscopic),
365                (CameraTarget::Xr, MirrorViewerFamily::Stereoscopic),
366            ];
367
368            for (source_target, family) in source_families {
369                let Some(source_cam) = visuals.visual_camera(source_target) else {
370                    continue;
371                };
372                if source_cam.eyes.is_empty() {
373                    continue;
374                }
375
376                for (view_index, eye_data) in source_cam.eyes.iter().enumerate() {
377                    let camera_world = eye_data.transform.matrix_world;
378                    let cam_pos = [camera_world[3][0], camera_world[3][1], camera_world[3][2]];
379                    let cam_up = [camera_world[1][0], camera_world[1][1], camera_world[1][2]];
380                    let cam_back = [camera_world[2][0], camera_world[2][1], camera_world[2][2]];
381                    let Some(cam_forward) = Self::normalize(math::vec3_negate(cam_back)) else {
382                        continue;
383                    };
384                    let Some(cam_up) = Self::normalize(cam_up) else {
385                        continue;
386                    };
387
388                    let ref_pos = math::vec3_reflect_point(cam_pos, plane_pos, plane_normal);
389                    let Some((mirror_x, mirror_y, _mirror_z)) =
390                        Self::mirror_local_basis(world_matrix)
391                    else {
392                        continue;
393                    };
394                    // For a mirror rendered onto a flat surface, the capture basis should stay
395                    // aligned to the mirror plane while the eye position moves the off-axis
396                    // frustum window. Coupling the capture basis to viewer yaw/pitch causes
397                    // edge-touching reflected geometry to shrink away from the side/corner the
398                    // viewer turns toward.
399                    let Some(ref_world) =
400                        Self::build_camera_world_from_forward_up(ref_pos, plane_normal, mirror_y)
401                    else {
402                        continue;
403                    };
404                    let ref_forward = plane_normal;
405                    let ref_up = [ref_world[1][0], ref_world[1][1], ref_world[1][2]];
406
407                    self.log_debug_sample_once(
408                        force_debug_dump,
409                        family,
410                        view_index,
411                        &guid,
412                        plane_pos,
413                        world_matrix,
414                        cam_pos,
415                        ref_pos,
416                        cam_forward,
417                        ref_forward,
418                        cam_up,
419                        ref_up,
420                    );
421
422                    let ref_view =
423                        math::mat4_inverse(ref_world).unwrap_or_else(math::mat4_identity);
424
425                    let mut reflected_transform = eye_data.transform;
426                    reflected_transform.matrix_world = ref_world;
427                    reflected_transform.model = ref_world;
428                    reflected_transform.translation = ref_pos;
429
430                    let z_near = eye_data.proj[3][2] / eye_data.proj[2][2];
431                    let z_far = eye_data.proj[3][2] / (1.0 + eye_data.proj[2][2]);
432                    let half_w = mirror_world_w * 0.5;
433                    let half_h = mirror_world_h * 0.5;
434                    let corners_world = [
435                        math::vec3_add(
436                            plane_pos,
437                            math::vec3_add(
438                                math::vec3_scale(mirror_x, -half_w),
439                                math::vec3_scale(mirror_y, -half_h),
440                            ),
441                        ),
442                        math::vec3_add(
443                            plane_pos,
444                            math::vec3_add(
445                                math::vec3_scale(mirror_x, half_w),
446                                math::vec3_scale(mirror_y, -half_h),
447                            ),
448                        ),
449                        math::vec3_add(
450                            plane_pos,
451                            math::vec3_add(
452                                math::vec3_scale(mirror_x, -half_w),
453                                math::vec3_scale(mirror_y, half_h),
454                            ),
455                        ),
456                        math::vec3_add(
457                            plane_pos,
458                            math::vec3_add(
459                                math::vec3_scale(mirror_x, half_w),
460                                math::vec3_scale(mirror_y, half_h),
461                            ),
462                        ),
463                    ];
464                    let mut left = f32::INFINITY;
465                    let mut right = f32::NEG_INFINITY;
466                    let mut bottom = f32::INFINITY;
467                    let mut top = f32::NEG_INFINITY;
468                    let mut projection_valid = true;
469                    for corner_world in corners_world {
470                        let corner_camera = Self::mat4_mul_vec4(
471                            ref_view,
472                            [corner_world[0], corner_world[1], corner_world[2], 1.0],
473                        );
474                        let corner_z = corner_camera[2];
475                        if corner_z >= -1e-5 {
476                            projection_valid = false;
477                            break;
478                        }
479                        let scale = z_near / -corner_z;
480                        let x = corner_camera[0] * scale;
481                        let y = corner_camera[1] * scale;
482                        left = left.min(x);
483                        right = right.max(x);
484                        bottom = bottom.min(y);
485                        top = top.max(y);
486                    }
487                    if !projection_valid {
488                        continue;
489                    }
490                    let mut ref_proj = Self::camera_space_off_axis_projection(
491                        left, right, bottom, top, z_near, z_far,
492                    )
493                    .unwrap_or(eye_data.proj);
494
495                    if MIRROR_ENABLE_OBLIQUE_CLIP_PLANE {
496                        let plane_normal_toward_camera = if math::vec3_dot(plane_normal, ref_pos)
497                            - math::vec3_dot(plane_normal, plane_pos)
498                            < 0.0
499                        {
500                            plane_normal
501                        } else {
502                            math::vec3_negate(plane_normal)
503                        };
504                        let biased_plane_origin = math::vec3_add(
505                            plane_pos,
506                            math::vec3_scale(
507                                plane_normal_toward_camera,
508                                MIRROR_CLIP_BIAS_WORLD_UNITS,
509                            ),
510                        );
511                        let plane_camera = Self::transform_plane_world_to_camera(
512                            ref_view,
513                            biased_plane_origin,
514                            plane_normal_toward_camera,
515                        );
516                        if let Some(plane_camera) = plane_camera {
517                            if let Some(oblique_proj) =
518                                Self::apply_oblique_near_plane_projection(ref_proj, plane_camera)
519                            {
520                                ref_proj = oblique_proj;
521                            }
522                        }
523                    }
524
525                    captures.push(MirrorCaptureRequest {
526                        family,
527                        view_index,
528                        camera: CameraData {
529                            view: ref_view,
530                            proj: ref_proj,
531                            transform: reflected_transform,
532                        },
533                        target_key: format!(
534                            "capture.mirror.{}.{}.{}.color",
535                            guid,
536                            family.key_segment(),
537                            view_index
538                        ),
539                    });
540                }
541            }
542
543            if captures.is_empty() {
544                continue;
545            }
546
547            // 5. Register with VisualWorld.
548            let source_instance = world
549                .get_component_by_id_as::<RenderableComponent>(renderable_cid)
550                .and_then(|r| r.handle)
551                .unwrap_or(InstanceHandle(u32::MAX)); // renderer will handle null
552
553            visuals.register_mirror(VisualMirror {
554                mirror_component: cid,
555                captures: captures.clone(),
556                plane_origin: plane_pos,
557                plane_normal,
558                aspect_ratio: mirror_aspect,
559                source_instance,
560                resolution_scale: quality as f32 / 1024.0, // normalized scale? renderer decides
561            });
562
563            // 6. Override parent renderable's material and texture.
564            if let Some(renderable) =
565                world.get_component_by_id_as_mut::<RenderableComponent>(renderable_cid)
566            {
567                renderable.renderable.material = MaterialHandle::MIRROR;
568                if let Some(handle) = renderable.handle {
569                    let _ = visuals.update_material(handle, MaterialHandle::MIRROR);
570                }
571
572                // Ensure TextureComponent exists and points to mirror_key.
573                let mut texture_cid = None;
574                for &ch in world.children_of(renderable_cid) {
575                    if world
576                        .get_component_by_id_as::<TextureComponent>(ch)
577                        .is_some()
578                    {
579                        texture_cid = Some(ch);
580                        break;
581                    }
582                }
583
584                if let Some(t_cid) = texture_cid {
585                    let tex = world
586                        .get_component_by_id_as_mut::<TextureComponent>(t_cid)
587                        .unwrap();
588                    tex.render_image = Some(format!("capture.mirror.{}.mono.0.color", guid));
589                    pending_texture_registrations.insert(t_cid);
590                } else {
591                    let new_tex_id = world.add_component(TextureComponent::render_image(format!(
592                        "capture.mirror.{}.mono.0.color",
593                        guid
594                    )));
595                    let _ = world.add_child(renderable_cid, new_tex_id);
596                    pending_texture_registrations.insert(new_tex_id);
597                }
598            }
599        }
600
601        self.pending_texture_registrations
602            .extend(pending_texture_registrations);
603    }
604}
605
606#[cfg(test)]
607mod tests {
608    use super::MirrorSystem;
609
610    fn approx_eq(a: f32, b: f32) {
611        assert!(
612            (a - b).abs() <= 1e-4,
613            "expected {a} ~= {b}, diff={}",
614            (a - b).abs()
615        );
616    }
617
618    fn clip_z(proj: [[f32; 4]; 4], v: [f32; 4]) -> f32 {
619        MirrorSystem::mat4_mul_vec4(proj, v)[2]
620    }
621
622    #[test]
623    fn transforms_world_plane_into_camera_space() {
624        let view = [
625            [1.0, 0.0, 0.0, 0.0],
626            [0.0, 1.0, 0.0, 0.0],
627            [0.0, 0.0, 1.0, 0.0],
628            [0.0, 0.0, -5.0, 1.0],
629        ];
630
631        let plane =
632            MirrorSystem::transform_plane_world_to_camera(view, [0.0, 0.0, 0.0], [0.0, 0.0, 1.0])
633                .expect("plane");
634
635        approx_eq(plane[0], 0.0);
636        approx_eq(plane[1], 0.0);
637        approx_eq(plane[2], 1.0);
638        approx_eq(plane[3], 5.0);
639    }
640
641    #[test]
642    fn oblique_projection_maps_clip_plane_to_near_plane() {
643        let proj = crate::engine::ecs::system::camera_system::Camera3D::perspective_rh_zo(
644            60.0f32.to_radians(),
645            1.0,
646            0.1,
647            100.0,
648        );
649        let plane_camera = [0.0, 0.0, 1.0, 5.0];
650        let oblique = MirrorSystem::apply_oblique_near_plane_projection(proj, plane_camera)
651            .expect("oblique projection");
652
653        approx_eq(clip_z(oblique, [0.0, 0.0, -5.0, 1.0]), 0.0);
654        assert!(clip_z(oblique, [0.0, 0.0, -6.0, 1.0]) > 0.0);
655        assert!(clip_z(oblique, [0.0, 0.0, -4.0, 1.0]) < 0.0);
656    }
657
658    #[test]
659    fn oblique_projection_handles_flipped_plane_orientation() {
660        let proj = crate::engine::ecs::system::camera_system::Camera3D::perspective_rh_zo(
661            60.0f32.to_radians(),
662            1.0,
663            0.1,
664            100.0,
665        );
666        let plane_camera = [0.0, 0.0, -1.0, -5.0];
667        let oblique = MirrorSystem::apply_oblique_near_plane_projection(proj, plane_camera)
668            .expect("oblique projection");
669
670        approx_eq(clip_z(oblique, [0.0, 0.0, -5.0, 1.0]), 0.0);
671        assert!(clip_z(oblique, [0.0, 0.0, -6.0, 1.0]) > 0.0);
672        assert!(clip_z(oblique, [0.0, 0.0, -4.0, 1.0]) < 0.0);
673    }
674}