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 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 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 let bounds = world
300 .get_component_by_id_as::<BoundsComponent>(renderable_cid)
301 .map(|b| b.local)
302 .or_else(|| {
303 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 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 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 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 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 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)); 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, });
562
563 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 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}