1use serde::{Deserialize, Serialize};
45
46#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
48pub struct Vec3 {
49 pub x: f32,
51 pub y: f32,
53 pub z: f32,
55}
56
57impl Vec3 {
58 #[must_use]
60 pub const fn new(x: f32, y: f32, z: f32) -> Self {
61 Self { x, y, z }
62 }
63
64 #[must_use]
66 pub const fn zero() -> Self {
67 Self::new(0.0, 0.0, 0.0)
68 }
69
70 #[must_use]
72 pub const fn up() -> Self {
73 Self::new(0.0, 1.0, 0.0)
74 }
75
76 #[must_use]
78 pub const fn forward() -> Self {
79 Self::new(0.0, 0.0, -1.0)
80 }
81
82 #[must_use]
84 pub fn length(&self) -> f32 {
85 (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
86 }
87
88 #[must_use]
90 pub fn normalize(&self) -> Self {
91 let len = self.length();
92 if len > 0.0 {
93 Self::new(self.x / len, self.y / len, self.z / len)
94 } else {
95 *self
96 }
97 }
98
99 #[must_use]
101 pub fn cross(&self, other: &Self) -> Self {
102 Self::new(
103 self.y * other.z - self.z * other.y,
104 self.z * other.x - self.x * other.z,
105 self.x * other.y - self.y * other.x,
106 )
107 }
108
109 #[must_use]
111 pub fn dot(&self, other: &Self) -> f32 {
112 self.x * other.x + self.y * other.y + self.z * other.z
113 }
114
115 #[must_use]
117 pub fn sub(&self, other: &Self) -> Self {
118 Self::new(self.x - other.x, self.y - other.y, self.z - other.z)
119 }
120
121 #[must_use]
123 pub fn add(&self, other: &Self) -> Self {
124 Self::new(self.x + other.x, self.y + other.y, self.z + other.z)
125 }
126
127 #[must_use]
129 pub fn scale(&self, s: f32) -> Self {
130 Self::new(self.x * s, self.y * s, self.z * s)
131 }
132}
133
134impl Default for Vec3 {
135 fn default() -> Self {
136 Self::zero()
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq)]
142pub struct Mat4 {
143 pub data: [f32; 16],
145}
146
147impl Mat4 {
148 #[must_use]
150 pub fn identity() -> Self {
151 #[rustfmt::skip]
152 let data = [
153 1.0, 0.0, 0.0, 0.0,
154 0.0, 1.0, 0.0, 0.0,
155 0.0, 0.0, 1.0, 0.0,
156 0.0, 0.0, 0.0, 1.0,
157 ];
158 Self { data }
159 }
160
161 #[must_use]
163 pub fn look_at(eye: Vec3, target: Vec3, up: Vec3) -> Self {
164 let f = target.sub(&eye).normalize();
165 let s = f.cross(&up).normalize();
166 let u = s.cross(&f);
167
168 #[rustfmt::skip]
169 let data = [
170 s.x, u.x, -f.x, 0.0,
171 s.y, u.y, -f.y, 0.0,
172 s.z, u.z, -f.z, 0.0,
173 -s.dot(&eye), -u.dot(&eye), f.dot(&eye), 1.0,
174 ];
175 Self { data }
176 }
177
178 #[must_use]
180 pub fn perspective(fov_y_radians: f32, aspect: f32, near: f32, far: f32) -> Self {
181 let f = 1.0 / (fov_y_radians / 2.0).tan();
182 let nf = 1.0 / (near - far);
183
184 #[rustfmt::skip]
185 let data = [
186 f / aspect, 0.0, 0.0, 0.0,
187 0.0, f, 0.0, 0.0,
188 0.0, 0.0, (far + near) * nf, -1.0,
189 0.0, 0.0, 2.0 * far * near * nf, 0.0,
190 ];
191 Self { data }
192 }
193
194 #[must_use]
196 pub fn mul(&self, other: &Self) -> Self {
197 let mut result = [0.0f32; 16];
198
199 for row in 0..4 {
200 for col in 0..4 {
201 for k in 0..4 {
202 result[col * 4 + row] += self.data[k * 4 + row] * other.data[col * 4 + k];
203 }
204 }
205 }
206
207 Self { data: result }
208 }
209}
210
211impl Default for Mat4 {
212 fn default() -> Self {
213 Self::identity()
214 }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct Camera {
220 pub position: Vec3,
222 pub target: Vec3,
224 pub up: Vec3,
226 pub fov: f32,
228 pub near: f32,
230 pub far: f32,
232}
233
234impl Camera {
235 #[must_use]
237 pub fn new() -> Self {
238 Self {
239 position: Vec3::new(0.0, 0.0, 5.0),
240 target: Vec3::zero(),
241 up: Vec3::up(),
242 fov: std::f32::consts::FRAC_PI_4, near: 0.1,
244 far: 100.0,
245 }
246 }
247
248 #[must_use]
250 pub fn view_matrix(&self) -> Mat4 {
251 Mat4::look_at(self.position, self.target, self.up)
252 }
253
254 #[must_use]
256 pub fn projection_matrix(&self, aspect: f32) -> Mat4 {
257 Mat4::perspective(self.fov, aspect, self.near, self.far)
258 }
259}
260
261impl Default for Camera {
262 fn default() -> Self {
263 Self::new()
264 }
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct HolographicConfig {
270 pub num_views: u32,
272 pub quilt_columns: u32,
274 pub quilt_rows: u32,
276 pub view_width: u32,
278 pub view_height: u32,
280 pub view_cone: f32,
282 pub focal_distance: f32,
284}
285
286impl HolographicConfig {
287 #[must_use]
289 pub fn looking_glass_portrait() -> Self {
290 Self {
291 num_views: 45,
292 quilt_columns: 5,
293 quilt_rows: 9,
294 view_width: 420,
295 view_height: 560,
296 view_cone: 40.0_f32.to_radians(), focal_distance: 2.0,
298 }
299 }
300
301 #[must_use]
303 pub fn looking_glass_4k() -> Self {
304 Self {
305 num_views: 45,
306 quilt_columns: 5,
307 quilt_rows: 9,
308 view_width: 819,
309 view_height: 455,
310 view_cone: 40.0_f32.to_radians(),
311 focal_distance: 2.0,
312 }
313 }
314
315 #[must_use]
317 pub fn quilt_width(&self) -> u32 {
318 self.quilt_columns * self.view_width
319 }
320
321 #[must_use]
323 pub fn quilt_height(&self) -> u32 {
324 self.quilt_rows * self.view_height
325 }
326
327 #[must_use]
329 pub fn view_to_grid(&self, view_index: u32) -> (u32, u32) {
330 let col = view_index % self.quilt_columns;
331 let row = view_index / self.quilt_columns;
332 (col, row)
333 }
334
335 #[must_use]
337 pub fn view_offset(&self, view_index: u32) -> (u32, u32) {
338 let (col, row) = self.view_to_grid(view_index);
339 (col * self.view_width, row * self.view_height)
340 }
341
342 #[must_use]
346 #[allow(clippy::cast_precision_loss)] pub fn camera_for_view(&self, base_camera: &Camera, view_index: u32) -> Camera {
348 if self.num_views <= 1 {
349 return base_camera.clone();
350 }
351
352 let t = view_index as f32 / (self.num_views - 1) as f32;
355 let angle = (t - 0.5) * self.view_cone;
356
357 let dir = base_camera.position.sub(&base_camera.target);
359 let distance = dir.length();
360
361 let cos_a = angle.cos();
363 let sin_a = angle.sin();
364 let new_dir = Vec3::new(
365 dir.x * cos_a + dir.z * sin_a,
366 dir.y,
367 -dir.x * sin_a + dir.z * cos_a,
368 );
369
370 Camera {
371 position: base_camera.target.add(&new_dir.normalize().scale(distance)),
372 target: base_camera.target,
373 up: base_camera.up,
374 fov: base_camera.fov,
375 near: base_camera.near,
376 far: base_camera.far,
377 }
378 }
379}
380
381impl Default for HolographicConfig {
382 fn default() -> Self {
383 Self::looking_glass_portrait()
384 }
385}
386
387#[derive(Debug, Clone)]
389pub struct QuiltRenderInfo {
390 pub width: u32,
392 pub height: u32,
394 pub num_views: u32,
396 pub columns: u32,
398 pub rows: u32,
400}
401
402impl QuiltRenderInfo {
403 #[must_use]
405 pub fn from_config(config: &HolographicConfig) -> Self {
406 Self {
407 width: config.quilt_width(),
408 height: config.quilt_height(),
409 num_views: config.num_views,
410 columns: config.quilt_columns,
411 rows: config.quilt_rows,
412 }
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 const EPSILON: f32 = 1e-5;
421
422 fn approx_eq(a: f32, b: f32) -> bool {
423 (a - b).abs() < EPSILON
424 }
425
426 #[test]
431 fn test_vec3_new() {
432 let v = Vec3::new(1.0, 2.0, 3.0);
433 assert!(approx_eq(v.x, 1.0));
434 assert!(approx_eq(v.y, 2.0));
435 assert!(approx_eq(v.z, 3.0));
436 }
437
438 #[test]
439 fn test_vec3_zero() {
440 let v = Vec3::zero();
441 assert!(approx_eq(v.x, 0.0));
442 assert!(approx_eq(v.y, 0.0));
443 assert!(approx_eq(v.z, 0.0));
444 }
445
446 #[test]
447 fn test_vec3_length() {
448 let v = Vec3::new(3.0, 4.0, 0.0);
449 assert!(approx_eq(v.length(), 5.0));
450 }
451
452 #[test]
453 fn test_vec3_normalize() {
454 let v = Vec3::new(3.0, 0.0, 0.0);
455 let n = v.normalize();
456 assert!(approx_eq(n.x, 1.0));
457 assert!(approx_eq(n.y, 0.0));
458 assert!(approx_eq(n.z, 0.0));
459 }
460
461 #[test]
462 fn test_vec3_normalize_zero() {
463 let v = Vec3::zero();
464 let n = v.normalize();
465 assert!(approx_eq(n.length(), 0.0));
467 }
468
469 #[test]
470 fn test_vec3_cross() {
471 let x = Vec3::new(1.0, 0.0, 0.0);
472 let y = Vec3::new(0.0, 1.0, 0.0);
473 let z = x.cross(&y);
474 assert!(approx_eq(z.x, 0.0));
475 assert!(approx_eq(z.y, 0.0));
476 assert!(approx_eq(z.z, 1.0));
477 }
478
479 #[test]
480 fn test_vec3_dot() {
481 let a = Vec3::new(1.0, 2.0, 3.0);
482 let b = Vec3::new(4.0, 5.0, 6.0);
483 assert!(approx_eq(a.dot(&b), 32.0)); }
485
486 #[test]
487 fn test_vec3_add_sub() {
488 let a = Vec3::new(1.0, 2.0, 3.0);
489 let b = Vec3::new(4.0, 5.0, 6.0);
490 let sum = a.add(&b);
491 let diff = a.sub(&b);
492
493 assert!(approx_eq(sum.x, 5.0));
494 assert!(approx_eq(sum.y, 7.0));
495 assert!(approx_eq(sum.z, 9.0));
496
497 assert!(approx_eq(diff.x, -3.0));
498 assert!(approx_eq(diff.y, -3.0));
499 assert!(approx_eq(diff.z, -3.0));
500 }
501
502 #[test]
503 fn test_vec3_scale() {
504 let v = Vec3::new(1.0, 2.0, 3.0);
505 let scaled = v.scale(2.0);
506 assert!(approx_eq(scaled.x, 2.0));
507 assert!(approx_eq(scaled.y, 4.0));
508 assert!(approx_eq(scaled.z, 6.0));
509 }
510
511 #[test]
516 fn test_mat4_identity() {
517 let m = Mat4::identity();
518 assert!(approx_eq(m.data[0], 1.0));
520 assert!(approx_eq(m.data[5], 1.0));
521 assert!(approx_eq(m.data[10], 1.0));
522 assert!(approx_eq(m.data[15], 1.0));
523 assert!(approx_eq(m.data[1], 0.0));
525 assert!(approx_eq(m.data[4], 0.0));
526 }
527
528 #[test]
529 fn test_mat4_mul_identity() {
530 let m = Mat4::identity();
531 let result = m.mul(&m);
532 assert!(approx_eq(result.data[0], 1.0));
534 assert!(approx_eq(result.data[5], 1.0));
535 assert!(approx_eq(result.data[10], 1.0));
536 assert!(approx_eq(result.data[15], 1.0));
537 }
538
539 #[test]
540 fn test_mat4_look_at() {
541 let eye = Vec3::new(0.0, 0.0, 5.0);
542 let target = Vec3::zero();
543 let up = Vec3::up();
544 let view = Mat4::look_at(eye, target, up);
545
546 assert!(view.data[15].abs() - 1.0 < EPSILON);
549 }
550
551 #[test]
552 fn test_mat4_perspective() {
553 let proj = Mat4::perspective(
554 std::f32::consts::FRAC_PI_4, 1.0, 0.1,
557 100.0,
558 );
559 assert!(approx_eq(proj.data[11], -1.0));
561 }
562
563 #[test]
568 fn test_camera_default() {
569 let cam = Camera::new();
570 assert!(approx_eq(cam.position.z, 5.0));
571 assert!(approx_eq(cam.target.x, 0.0));
572 assert!(approx_eq(cam.target.y, 0.0));
573 assert!(approx_eq(cam.target.z, 0.0));
574 }
575
576 #[test]
577 fn test_camera_view_matrix() {
578 let cam = Camera::new();
579 let view = cam.view_matrix();
580 assert_eq!(view.data.len(), 16);
582 }
583
584 #[test]
585 fn test_camera_projection_matrix() {
586 let cam = Camera::new();
587 let proj = cam.projection_matrix(16.0 / 9.0);
588 assert!(approx_eq(proj.data[11], -1.0));
590 }
591
592 #[test]
597 fn test_holographic_config_portrait() {
598 let config = HolographicConfig::looking_glass_portrait();
599 assert_eq!(config.num_views, 45);
600 assert_eq!(config.quilt_columns, 5);
601 assert_eq!(config.quilt_rows, 9);
602 assert_eq!(config.num_views, config.quilt_columns * config.quilt_rows);
603 }
604
605 #[test]
606 fn test_holographic_config_4k() {
607 let config = HolographicConfig::looking_glass_4k();
608 assert_eq!(config.num_views, 45);
609 assert_eq!(config.quilt_columns, 5);
610 assert_eq!(config.quilt_rows, 9);
611 }
612
613 #[test]
614 fn test_quilt_dimensions() {
615 let config = HolographicConfig::looking_glass_portrait();
616 let width = config.quilt_width();
617 let height = config.quilt_height();
618
619 assert_eq!(width, 5 * 420); assert_eq!(height, 9 * 560); }
622
623 #[test]
624 fn test_view_to_grid() {
625 let config = HolographicConfig::looking_glass_portrait();
626
627 let (col, row) = config.view_to_grid(0);
629 assert_eq!(col, 0);
630 assert_eq!(row, 0);
631
632 let (col, row) = config.view_to_grid(4);
634 assert_eq!(col, 4);
635 assert_eq!(row, 0);
636
637 let (col, row) = config.view_to_grid(5);
639 assert_eq!(col, 0);
640 assert_eq!(row, 1);
641
642 let (col, row) = config.view_to_grid(44);
644 assert_eq!(col, 4);
645 assert_eq!(row, 8);
646 }
647
648 #[test]
649 fn test_view_offset() {
650 let config = HolographicConfig::looking_glass_portrait();
651
652 let (x, y) = config.view_offset(0);
654 assert_eq!(x, 0);
655 assert_eq!(y, 0);
656
657 let (x, y) = config.view_offset(1);
659 assert_eq!(x, 420);
660 assert_eq!(y, 0);
661
662 let (x, y) = config.view_offset(5);
664 assert_eq!(x, 0);
665 assert_eq!(y, 560);
666 }
667
668 #[test]
669 fn test_camera_for_view_single() {
670 let config = HolographicConfig {
671 num_views: 1,
672 ..Default::default()
673 };
674 let base = Camera::new();
675 let view_cam = config.camera_for_view(&base, 0);
676
677 assert!(approx_eq(view_cam.position.x, base.position.x));
679 assert!(approx_eq(view_cam.position.y, base.position.y));
680 assert!(approx_eq(view_cam.position.z, base.position.z));
681 }
682
683 #[test]
684 fn test_camera_for_view_center() {
685 let config = HolographicConfig::looking_glass_portrait();
686 let base = Camera::new();
687
688 let center_cam = config.camera_for_view(&base, 22);
690
691 let distance_from_base = center_cam.position.sub(&base.position).length();
694 assert!(distance_from_base < 0.5);
695 }
696
697 #[test]
698 fn test_camera_for_view_symmetry() {
699 let config = HolographicConfig::looking_glass_portrait();
700 let base = Camera::new();
701
702 let left_cam = config.camera_for_view(&base, 0);
704 let right_cam = config.camera_for_view(&base, 44);
705
706 assert!(approx_eq(left_cam.position.x, -right_cam.position.x));
708 assert!(approx_eq(left_cam.position.y, right_cam.position.y));
710 }
711
712 #[test]
713 fn test_camera_for_view_maintains_distance() {
714 let config = HolographicConfig::looking_glass_portrait();
715 let base = Camera::new();
716 let base_distance = base.position.sub(&base.target).length();
717
718 for i in 0..config.num_views {
720 let view_cam = config.camera_for_view(&base, i);
721 let view_distance = view_cam.position.sub(&view_cam.target).length();
722 assert!(
723 approx_eq(view_distance, base_distance),
724 "View {i} distance {view_distance} != base {base_distance}"
725 );
726 }
727 }
728
729 #[test]
730 fn test_camera_for_view_progression() {
731 let config = HolographicConfig::looking_glass_portrait();
732 let base = Camera::new();
733
734 let mut prev_x = f32::NEG_INFINITY;
736 for i in 0..config.num_views {
737 let view_cam = config.camera_for_view(&base, i);
738 assert!(
739 view_cam.position.x > prev_x,
740 "View {} x {} should be > {}",
741 i,
742 view_cam.position.x,
743 prev_x
744 );
745 prev_x = view_cam.position.x;
746 }
747 }
748
749 #[test]
750 fn test_camera_for_view_edge_angles() {
751 let config = HolographicConfig::looking_glass_portrait();
752 let base = Camera::new();
753
754 let left_cam = config.camera_for_view(&base, 0);
757 let right_cam = config.camera_for_view(&base, 44);
758
759 let left_angle = left_cam.position.x.atan2(left_cam.position.z);
761 let right_angle = right_cam.position.x.atan2(right_cam.position.z);
762
763 let total_span = right_angle - left_angle;
765 let expected_span = config.view_cone;
766 assert!(
767 (total_span - expected_span).abs() < 0.01,
768 "View cone span {total_span:.4} should match config {expected_span:.4}"
769 );
770 }
771
772 #[test]
773 fn test_camera_for_view_all_point_to_target() {
774 let config = HolographicConfig::looking_glass_portrait();
775 let base = Camera::new();
776
777 for i in 0..config.num_views {
779 let view_cam = config.camera_for_view(&base, i);
780 assert!(
781 approx_eq(view_cam.target.x, base.target.x),
782 "View {} target.x {} should equal base {}",
783 i,
784 view_cam.target.x,
785 base.target.x
786 );
787 assert!(
788 approx_eq(view_cam.target.y, base.target.y),
789 "View {} target.y {} should equal base {}",
790 i,
791 view_cam.target.y,
792 base.target.y
793 );
794 assert!(
795 approx_eq(view_cam.target.z, base.target.z),
796 "View {} target.z {} should equal base {}",
797 i,
798 view_cam.target.z,
799 base.target.z
800 );
801 }
802 }
803
804 #[test]
805 fn test_camera_for_view_preserves_camera_params() {
806 let config = HolographicConfig::looking_glass_portrait();
807 let base = Camera {
808 position: Vec3::new(0.0, 0.0, 10.0),
809 target: Vec3::new(0.0, 0.0, 0.0),
810 up: Vec3::up(),
811 fov: 0.8, near: 0.5, far: 50.0, };
815
816 for i in 0..config.num_views {
818 let view_cam = config.camera_for_view(&base, i);
819 assert!(approx_eq(view_cam.fov, base.fov), "View {i} FOV mismatch");
820 assert!(
821 approx_eq(view_cam.near, base.near),
822 "View {i} near mismatch"
823 );
824 assert!(approx_eq(view_cam.far, base.far), "View {i} far mismatch");
825 assert!(
826 approx_eq(view_cam.up.x, base.up.x)
827 && approx_eq(view_cam.up.y, base.up.y)
828 && approx_eq(view_cam.up.z, base.up.z),
829 "View {i} up vector mismatch"
830 );
831 }
832 }
833
834 #[test]
835 fn test_camera_for_view_4k_config() {
836 let config = HolographicConfig::looking_glass_4k();
837 let base = Camera::new();
838
839 assert_eq!(config.num_views, 45);
841
842 let center_cam = config.camera_for_view(&base, 22);
844 let distance_from_base = center_cam.position.sub(&base.position).length();
845 assert!(
846 distance_from_base < 0.5,
847 "4K center view should be near base"
848 );
849
850 let left_cam = config.camera_for_view(&base, 0);
852 let right_cam = config.camera_for_view(&base, 44);
853 assert!(
854 approx_eq(left_cam.position.x, -right_cam.position.x),
855 "4K edge views should be symmetric"
856 );
857
858 let left_angle = left_cam.position.x.atan2(left_cam.position.z);
860 let right_angle = right_cam.position.x.atan2(right_cam.position.z);
861 let total_span = right_angle - left_angle;
862 assert!(
863 (total_span - config.view_cone).abs() < 0.01,
864 "4K view cone should match config"
865 );
866 }
867
868 #[test]
869 fn test_camera_for_view_custom_target() {
870 let config = HolographicConfig::looking_glass_portrait();
871 let base = Camera {
872 position: Vec3::new(5.0, 2.0, 8.0),
873 target: Vec3::new(5.0, 2.0, 0.0), up: Vec3::up(),
875 fov: std::f32::consts::FRAC_PI_4,
876 near: 0.1,
877 far: 100.0,
878 };
879
880 let base_distance = base.position.sub(&base.target).length();
881
882 for i in 0..config.num_views {
884 let view_cam = config.camera_for_view(&base, i);
885 let view_distance = view_cam.position.sub(&view_cam.target).length();
886 assert!(
887 approx_eq(view_distance, base_distance),
888 "View {i} distance {view_distance} should match base {base_distance}"
889 );
890 assert!(approx_eq(view_cam.target.x, base.target.x));
892 assert!(approx_eq(view_cam.target.y, base.target.y));
893 assert!(approx_eq(view_cam.target.z, base.target.z));
894 }
895 }
896
897 #[test]
898 fn test_camera_for_view_zero_views() {
899 let config = HolographicConfig {
901 num_views: 0,
902 ..Default::default()
903 };
904 let base = Camera::new();
905 let view_cam = config.camera_for_view(&base, 0);
906
907 assert!(approx_eq(view_cam.position.x, base.position.x));
909 assert!(approx_eq(view_cam.position.y, base.position.y));
910 assert!(approx_eq(view_cam.position.z, base.position.z));
911 }
912
913 #[test]
918 fn test_quilt_render_info_from_config() {
919 let config = HolographicConfig::looking_glass_portrait();
920 let info = QuiltRenderInfo::from_config(&config);
921
922 assert_eq!(info.width, 2100);
923 assert_eq!(info.height, 5040);
924 assert_eq!(info.num_views, 45);
925 assert_eq!(info.columns, 5);
926 assert_eq!(info.rows, 9);
927 }
928}