use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Vec3 {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Vec3 {
#[must_use]
pub const fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
#[must_use]
pub const fn zero() -> Self {
Self::new(0.0, 0.0, 0.0)
}
#[must_use]
pub const fn up() -> Self {
Self::new(0.0, 1.0, 0.0)
}
#[must_use]
pub const fn forward() -> Self {
Self::new(0.0, 0.0, -1.0)
}
#[must_use]
pub fn length(&self) -> f32 {
(self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
}
#[must_use]
pub fn normalize(&self) -> Self {
let len = self.length();
if len > 0.0 {
Self::new(self.x / len, self.y / len, self.z / len)
} else {
*self
}
}
#[must_use]
pub fn cross(&self, other: &Self) -> Self {
Self::new(
self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x,
)
}
#[must_use]
pub fn dot(&self, other: &Self) -> f32 {
self.x * other.x + self.y * other.y + self.z * other.z
}
#[must_use]
pub fn sub(&self, other: &Self) -> Self {
Self::new(self.x - other.x, self.y - other.y, self.z - other.z)
}
#[must_use]
pub fn add(&self, other: &Self) -> Self {
Self::new(self.x + other.x, self.y + other.y, self.z + other.z)
}
#[must_use]
pub fn scale(&self, s: f32) -> Self {
Self::new(self.x * s, self.y * s, self.z * s)
}
}
impl Default for Vec3 {
fn default() -> Self {
Self::zero()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Mat4 {
pub data: [f32; 16],
}
impl Mat4 {
#[must_use]
pub fn identity() -> Self {
#[rustfmt::skip]
let data = [
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,
];
Self { data }
}
#[must_use]
pub fn look_at(eye: Vec3, target: Vec3, up: Vec3) -> Self {
let f = target.sub(&eye).normalize();
let s = f.cross(&up).normalize();
let u = s.cross(&f);
#[rustfmt::skip]
let data = [
s.x, u.x, -f.x, 0.0,
s.y, u.y, -f.y, 0.0,
s.z, u.z, -f.z, 0.0,
-s.dot(&eye), -u.dot(&eye), f.dot(&eye), 1.0,
];
Self { data }
}
#[must_use]
pub fn perspective(fov_y_radians: f32, aspect: f32, near: f32, far: f32) -> Self {
let f = 1.0 / (fov_y_radians / 2.0).tan();
let nf = 1.0 / (near - far);
#[rustfmt::skip]
let data = [
f / aspect, 0.0, 0.0, 0.0,
0.0, f, 0.0, 0.0,
0.0, 0.0, (far + near) * nf, -1.0,
0.0, 0.0, 2.0 * far * near * nf, 0.0,
];
Self { data }
}
#[must_use]
pub fn mul(&self, other: &Self) -> Self {
let mut result = [0.0f32; 16];
for row in 0..4 {
for col in 0..4 {
for k in 0..4 {
result[col * 4 + row] += self.data[k * 4 + row] * other.data[col * 4 + k];
}
}
}
Self { data: result }
}
}
impl Default for Mat4 {
fn default() -> Self {
Self::identity()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Camera {
pub position: Vec3,
pub target: Vec3,
pub up: Vec3,
pub fov: f32,
pub near: f32,
pub far: f32,
}
impl Camera {
#[must_use]
pub fn new() -> Self {
Self {
position: Vec3::new(0.0, 0.0, 5.0),
target: Vec3::zero(),
up: Vec3::up(),
fov: std::f32::consts::FRAC_PI_4, near: 0.1,
far: 100.0,
}
}
#[must_use]
pub fn view_matrix(&self) -> Mat4 {
Mat4::look_at(self.position, self.target, self.up)
}
#[must_use]
pub fn projection_matrix(&self, aspect: f32) -> Mat4 {
Mat4::perspective(self.fov, aspect, self.near, self.far)
}
}
impl Default for Camera {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HolographicConfig {
pub num_views: u32,
pub quilt_columns: u32,
pub quilt_rows: u32,
pub view_width: u32,
pub view_height: u32,
pub view_cone: f32,
pub focal_distance: f32,
}
impl HolographicConfig {
#[must_use]
pub fn looking_glass_portrait() -> Self {
Self {
num_views: 45,
quilt_columns: 5,
quilt_rows: 9,
view_width: 420,
view_height: 560,
view_cone: 40.0_f32.to_radians(), focal_distance: 2.0,
}
}
#[must_use]
pub fn looking_glass_4k() -> Self {
Self {
num_views: 45,
quilt_columns: 5,
quilt_rows: 9,
view_width: 819,
view_height: 455,
view_cone: 40.0_f32.to_radians(),
focal_distance: 2.0,
}
}
#[must_use]
pub fn quilt_width(&self) -> u32 {
self.quilt_columns * self.view_width
}
#[must_use]
pub fn quilt_height(&self) -> u32 {
self.quilt_rows * self.view_height
}
#[must_use]
pub fn view_to_grid(&self, view_index: u32) -> (u32, u32) {
let col = view_index % self.quilt_columns;
let row = view_index / self.quilt_columns;
(col, row)
}
#[must_use]
pub fn view_offset(&self, view_index: u32) -> (u32, u32) {
let (col, row) = self.view_to_grid(view_index);
(col * self.view_width, row * self.view_height)
}
#[must_use]
#[allow(clippy::cast_precision_loss)] pub fn camera_for_view(&self, base_camera: &Camera, view_index: u32) -> Camera {
if self.num_views <= 1 {
return base_camera.clone();
}
let t = view_index as f32 / (self.num_views - 1) as f32;
let angle = (t - 0.5) * self.view_cone;
let dir = base_camera.position.sub(&base_camera.target);
let distance = dir.length();
let cos_a = angle.cos();
let sin_a = angle.sin();
let new_dir = Vec3::new(
dir.x * cos_a + dir.z * sin_a,
dir.y,
-dir.x * sin_a + dir.z * cos_a,
);
Camera {
position: base_camera.target.add(&new_dir.normalize().scale(distance)),
target: base_camera.target,
up: base_camera.up,
fov: base_camera.fov,
near: base_camera.near,
far: base_camera.far,
}
}
}
impl Default for HolographicConfig {
fn default() -> Self {
Self::looking_glass_portrait()
}
}
#[derive(Debug, Clone)]
pub struct QuiltRenderInfo {
pub width: u32,
pub height: u32,
pub num_views: u32,
pub columns: u32,
pub rows: u32,
}
impl QuiltRenderInfo {
#[must_use]
pub fn from_config(config: &HolographicConfig) -> Self {
Self {
width: config.quilt_width(),
height: config.quilt_height(),
num_views: config.num_views,
columns: config.quilt_columns,
rows: config.quilt_rows,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f32 = 1e-5;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn test_vec3_new() {
let v = Vec3::new(1.0, 2.0, 3.0);
assert!(approx_eq(v.x, 1.0));
assert!(approx_eq(v.y, 2.0));
assert!(approx_eq(v.z, 3.0));
}
#[test]
fn test_vec3_zero() {
let v = Vec3::zero();
assert!(approx_eq(v.x, 0.0));
assert!(approx_eq(v.y, 0.0));
assert!(approx_eq(v.z, 0.0));
}
#[test]
fn test_vec3_length() {
let v = Vec3::new(3.0, 4.0, 0.0);
assert!(approx_eq(v.length(), 5.0));
}
#[test]
fn test_vec3_normalize() {
let v = Vec3::new(3.0, 0.0, 0.0);
let n = v.normalize();
assert!(approx_eq(n.x, 1.0));
assert!(approx_eq(n.y, 0.0));
assert!(approx_eq(n.z, 0.0));
}
#[test]
fn test_vec3_normalize_zero() {
let v = Vec3::zero();
let n = v.normalize();
assert!(approx_eq(n.length(), 0.0));
}
#[test]
fn test_vec3_cross() {
let x = Vec3::new(1.0, 0.0, 0.0);
let y = Vec3::new(0.0, 1.0, 0.0);
let z = x.cross(&y);
assert!(approx_eq(z.x, 0.0));
assert!(approx_eq(z.y, 0.0));
assert!(approx_eq(z.z, 1.0));
}
#[test]
fn test_vec3_dot() {
let a = Vec3::new(1.0, 2.0, 3.0);
let b = Vec3::new(4.0, 5.0, 6.0);
assert!(approx_eq(a.dot(&b), 32.0)); }
#[test]
fn test_vec3_add_sub() {
let a = Vec3::new(1.0, 2.0, 3.0);
let b = Vec3::new(4.0, 5.0, 6.0);
let sum = a.add(&b);
let diff = a.sub(&b);
assert!(approx_eq(sum.x, 5.0));
assert!(approx_eq(sum.y, 7.0));
assert!(approx_eq(sum.z, 9.0));
assert!(approx_eq(diff.x, -3.0));
assert!(approx_eq(diff.y, -3.0));
assert!(approx_eq(diff.z, -3.0));
}
#[test]
fn test_vec3_scale() {
let v = Vec3::new(1.0, 2.0, 3.0);
let scaled = v.scale(2.0);
assert!(approx_eq(scaled.x, 2.0));
assert!(approx_eq(scaled.y, 4.0));
assert!(approx_eq(scaled.z, 6.0));
}
#[test]
fn test_mat4_identity() {
let m = Mat4::identity();
assert!(approx_eq(m.data[0], 1.0));
assert!(approx_eq(m.data[5], 1.0));
assert!(approx_eq(m.data[10], 1.0));
assert!(approx_eq(m.data[15], 1.0));
assert!(approx_eq(m.data[1], 0.0));
assert!(approx_eq(m.data[4], 0.0));
}
#[test]
fn test_mat4_mul_identity() {
let m = Mat4::identity();
let result = m.mul(&m);
assert!(approx_eq(result.data[0], 1.0));
assert!(approx_eq(result.data[5], 1.0));
assert!(approx_eq(result.data[10], 1.0));
assert!(approx_eq(result.data[15], 1.0));
}
#[test]
fn test_mat4_look_at() {
let eye = Vec3::new(0.0, 0.0, 5.0);
let target = Vec3::zero();
let up = Vec3::up();
let view = Mat4::look_at(eye, target, up);
assert!(view.data[15].abs() - 1.0 < EPSILON);
}
#[test]
fn test_mat4_perspective() {
let proj = Mat4::perspective(
std::f32::consts::FRAC_PI_4, 1.0, 0.1,
100.0,
);
assert!(approx_eq(proj.data[11], -1.0));
}
#[test]
fn test_camera_default() {
let cam = Camera::new();
assert!(approx_eq(cam.position.z, 5.0));
assert!(approx_eq(cam.target.x, 0.0));
assert!(approx_eq(cam.target.y, 0.0));
assert!(approx_eq(cam.target.z, 0.0));
}
#[test]
fn test_camera_view_matrix() {
let cam = Camera::new();
let view = cam.view_matrix();
assert_eq!(view.data.len(), 16);
}
#[test]
fn test_camera_projection_matrix() {
let cam = Camera::new();
let proj = cam.projection_matrix(16.0 / 9.0);
assert!(approx_eq(proj.data[11], -1.0));
}
#[test]
fn test_holographic_config_portrait() {
let config = HolographicConfig::looking_glass_portrait();
assert_eq!(config.num_views, 45);
assert_eq!(config.quilt_columns, 5);
assert_eq!(config.quilt_rows, 9);
assert_eq!(config.num_views, config.quilt_columns * config.quilt_rows);
}
#[test]
fn test_holographic_config_4k() {
let config = HolographicConfig::looking_glass_4k();
assert_eq!(config.num_views, 45);
assert_eq!(config.quilt_columns, 5);
assert_eq!(config.quilt_rows, 9);
}
#[test]
fn test_quilt_dimensions() {
let config = HolographicConfig::looking_glass_portrait();
let width = config.quilt_width();
let height = config.quilt_height();
assert_eq!(width, 5 * 420); assert_eq!(height, 9 * 560); }
#[test]
fn test_view_to_grid() {
let config = HolographicConfig::looking_glass_portrait();
let (col, row) = config.view_to_grid(0);
assert_eq!(col, 0);
assert_eq!(row, 0);
let (col, row) = config.view_to_grid(4);
assert_eq!(col, 4);
assert_eq!(row, 0);
let (col, row) = config.view_to_grid(5);
assert_eq!(col, 0);
assert_eq!(row, 1);
let (col, row) = config.view_to_grid(44);
assert_eq!(col, 4);
assert_eq!(row, 8);
}
#[test]
fn test_view_offset() {
let config = HolographicConfig::looking_glass_portrait();
let (x, y) = config.view_offset(0);
assert_eq!(x, 0);
assert_eq!(y, 0);
let (x, y) = config.view_offset(1);
assert_eq!(x, 420);
assert_eq!(y, 0);
let (x, y) = config.view_offset(5);
assert_eq!(x, 0);
assert_eq!(y, 560);
}
#[test]
fn test_camera_for_view_single() {
let config = HolographicConfig {
num_views: 1,
..Default::default()
};
let base = Camera::new();
let view_cam = config.camera_for_view(&base, 0);
assert!(approx_eq(view_cam.position.x, base.position.x));
assert!(approx_eq(view_cam.position.y, base.position.y));
assert!(approx_eq(view_cam.position.z, base.position.z));
}
#[test]
fn test_camera_for_view_center() {
let config = HolographicConfig::looking_glass_portrait();
let base = Camera::new();
let center_cam = config.camera_for_view(&base, 22);
let distance_from_base = center_cam.position.sub(&base.position).length();
assert!(distance_from_base < 0.5);
}
#[test]
fn test_camera_for_view_symmetry() {
let config = HolographicConfig::looking_glass_portrait();
let base = Camera::new();
let left_cam = config.camera_for_view(&base, 0);
let right_cam = config.camera_for_view(&base, 44);
assert!(approx_eq(left_cam.position.x, -right_cam.position.x));
assert!(approx_eq(left_cam.position.y, right_cam.position.y));
}
#[test]
fn test_camera_for_view_maintains_distance() {
let config = HolographicConfig::looking_glass_portrait();
let base = Camera::new();
let base_distance = base.position.sub(&base.target).length();
for i in 0..config.num_views {
let view_cam = config.camera_for_view(&base, i);
let view_distance = view_cam.position.sub(&view_cam.target).length();
assert!(
approx_eq(view_distance, base_distance),
"View {i} distance {view_distance} != base {base_distance}"
);
}
}
#[test]
fn test_camera_for_view_progression() {
let config = HolographicConfig::looking_glass_portrait();
let base = Camera::new();
let mut prev_x = f32::NEG_INFINITY;
for i in 0..config.num_views {
let view_cam = config.camera_for_view(&base, i);
assert!(
view_cam.position.x > prev_x,
"View {} x {} should be > {}",
i,
view_cam.position.x,
prev_x
);
prev_x = view_cam.position.x;
}
}
#[test]
fn test_camera_for_view_edge_angles() {
let config = HolographicConfig::looking_glass_portrait();
let base = Camera::new();
let left_cam = config.camera_for_view(&base, 0);
let right_cam = config.camera_for_view(&base, 44);
let left_angle = left_cam.position.x.atan2(left_cam.position.z);
let right_angle = right_cam.position.x.atan2(right_cam.position.z);
let total_span = right_angle - left_angle;
let expected_span = config.view_cone;
assert!(
(total_span - expected_span).abs() < 0.01,
"View cone span {total_span:.4} should match config {expected_span:.4}"
);
}
#[test]
fn test_camera_for_view_all_point_to_target() {
let config = HolographicConfig::looking_glass_portrait();
let base = Camera::new();
for i in 0..config.num_views {
let view_cam = config.camera_for_view(&base, i);
assert!(
approx_eq(view_cam.target.x, base.target.x),
"View {} target.x {} should equal base {}",
i,
view_cam.target.x,
base.target.x
);
assert!(
approx_eq(view_cam.target.y, base.target.y),
"View {} target.y {} should equal base {}",
i,
view_cam.target.y,
base.target.y
);
assert!(
approx_eq(view_cam.target.z, base.target.z),
"View {} target.z {} should equal base {}",
i,
view_cam.target.z,
base.target.z
);
}
}
#[test]
fn test_camera_for_view_preserves_camera_params() {
let config = HolographicConfig::looking_glass_portrait();
let base = Camera {
position: Vec3::new(0.0, 0.0, 10.0),
target: Vec3::new(0.0, 0.0, 0.0),
up: Vec3::up(),
fov: 0.8, near: 0.5, far: 50.0, };
for i in 0..config.num_views {
let view_cam = config.camera_for_view(&base, i);
assert!(approx_eq(view_cam.fov, base.fov), "View {i} FOV mismatch");
assert!(
approx_eq(view_cam.near, base.near),
"View {i} near mismatch"
);
assert!(approx_eq(view_cam.far, base.far), "View {i} far mismatch");
assert!(
approx_eq(view_cam.up.x, base.up.x)
&& approx_eq(view_cam.up.y, base.up.y)
&& approx_eq(view_cam.up.z, base.up.z),
"View {i} up vector mismatch"
);
}
}
#[test]
fn test_camera_for_view_4k_config() {
let config = HolographicConfig::looking_glass_4k();
let base = Camera::new();
assert_eq!(config.num_views, 45);
let center_cam = config.camera_for_view(&base, 22);
let distance_from_base = center_cam.position.sub(&base.position).length();
assert!(
distance_from_base < 0.5,
"4K center view should be near base"
);
let left_cam = config.camera_for_view(&base, 0);
let right_cam = config.camera_for_view(&base, 44);
assert!(
approx_eq(left_cam.position.x, -right_cam.position.x),
"4K edge views should be symmetric"
);
let left_angle = left_cam.position.x.atan2(left_cam.position.z);
let right_angle = right_cam.position.x.atan2(right_cam.position.z);
let total_span = right_angle - left_angle;
assert!(
(total_span - config.view_cone).abs() < 0.01,
"4K view cone should match config"
);
}
#[test]
fn test_camera_for_view_custom_target() {
let config = HolographicConfig::looking_glass_portrait();
let base = Camera {
position: Vec3::new(5.0, 2.0, 8.0),
target: Vec3::new(5.0, 2.0, 0.0), up: Vec3::up(),
fov: std::f32::consts::FRAC_PI_4,
near: 0.1,
far: 100.0,
};
let base_distance = base.position.sub(&base.target).length();
for i in 0..config.num_views {
let view_cam = config.camera_for_view(&base, i);
let view_distance = view_cam.position.sub(&view_cam.target).length();
assert!(
approx_eq(view_distance, base_distance),
"View {i} distance {view_distance} should match base {base_distance}"
);
assert!(approx_eq(view_cam.target.x, base.target.x));
assert!(approx_eq(view_cam.target.y, base.target.y));
assert!(approx_eq(view_cam.target.z, base.target.z));
}
}
#[test]
fn test_camera_for_view_zero_views() {
let config = HolographicConfig {
num_views: 0,
..Default::default()
};
let base = Camera::new();
let view_cam = config.camera_for_view(&base, 0);
assert!(approx_eq(view_cam.position.x, base.position.x));
assert!(approx_eq(view_cam.position.y, base.position.y));
assert!(approx_eq(view_cam.position.z, base.position.z));
}
#[test]
fn test_quilt_render_info_from_config() {
let config = HolographicConfig::looking_glass_portrait();
let info = QuiltRenderInfo::from_config(&config);
assert_eq!(info.width, 2100);
assert_eq!(info.height, 5040);
assert_eq!(info.num_views, 45);
assert_eq!(info.columns, 5);
assert_eq!(info.rows, 9);
}
}