Skip to main content

ling_graphics/
camera.rs

1use glam::{Vec3, Vec4, Mat4, Quat};
2use crate::math::{Vec4H, Mat5, Ray3};
3
4// ── 3D Camera ─────────────────────────────────────────────────────────────────
5
6#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
7pub enum Projection {
8    Perspective { fov_y: f32, near: f32, far: f32 },
9    Orthographic { half_width: f32, near: f32, far: f32 },
10}
11
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub struct Camera3D {
14    pub position: Vec3,
15    pub rotation: Quat,
16    pub projection: Projection,
17    pub aspect: f32,
18}
19
20impl Camera3D {
21    pub fn perspective(fov_y_deg: f32, aspect: f32, near: f32, far: f32) -> Self {
22        Self {
23            position: Vec3::ZERO,
24            rotation: Quat::IDENTITY,
25            projection: Projection::Perspective {
26                fov_y: fov_y_deg.to_radians(),
27                near,
28                far,
29            },
30            aspect,
31        }
32    }
33
34    pub fn orthographic(half_width: f32, aspect: f32, near: f32, far: f32) -> Self {
35        Self {
36            position: Vec3::ZERO,
37            rotation: Quat::IDENTITY,
38            projection: Projection::Orthographic { half_width, near, far },
39            aspect,
40        }
41    }
42
43    pub fn forward(&self) -> Vec3 { self.rotation * -Vec3::Z }
44    pub fn right(&self) -> Vec3   { self.rotation *  Vec3::X }
45    pub fn up(&self) -> Vec3      { self.rotation *  Vec3::Y }
46
47    pub fn look_at(&mut self, target: Vec3, world_up: Vec3) {
48        let dir = (target - self.position).normalize();
49        if dir.length_squared() < 1e-8 { return; }
50        let mat = Mat4::look_at_rh(self.position, target, world_up);
51        let (_, rot, _) = mat.inverse().to_scale_rotation_translation();
52        self.rotation = rot;
53    }
54
55    pub fn view_matrix(&self) -> Mat4 {
56        Mat4::from_rotation_translation(self.rotation, self.position).inverse()
57    }
58
59    pub fn projection_matrix(&self) -> Mat4 {
60        match self.projection {
61            Projection::Perspective { fov_y, near, far } =>
62                Mat4::perspective_rh(fov_y, self.aspect, near, far),
63            Projection::Orthographic { half_width, near, far } => {
64                let h = half_width / self.aspect;
65                Mat4::orthographic_rh(-half_width, half_width, -h, h, near, far)
66            }
67        }
68    }
69
70    pub fn view_proj(&self) -> Mat4 {
71        self.projection_matrix() * self.view_matrix()
72    }
73
74    /// Unproject a screen-space point [−1,1]×[−1,1] into a world-space ray.
75    pub fn unproject_ray(&self, ndc_x: f32, ndc_y: f32) -> Ray3 {
76        let inv_vp = self.view_proj().inverse();
77        let near = inv_vp * Vec4::new(ndc_x, ndc_y, -1.0, 1.0);
78        let far  = inv_vp * Vec4::new(ndc_x, ndc_y,  1.0, 1.0);
79        let near = near.truncate() / near.w;
80        let far  = far.truncate()  / far.w;
81        Ray3::new(near, (far - near).normalize())
82    }
83
84    pub fn move_forward(&mut self, dist: f32) { self.position += self.forward() * dist; }
85    pub fn move_right(&mut self, dist: f32)   { self.position += self.right()   * dist; }
86    pub fn move_up(&mut self, dist: f32)      { self.position += self.up()      * dist; }
87
88    pub fn orbit(&mut self, target: Vec3, yaw: f32, pitch: f32) {
89        let rot = Quat::from_rotation_y(yaw) * Quat::from_rotation_x(pitch);
90        let offset = self.position - target;
91        self.position = target + rot * offset;
92        self.look_at(target, Vec3::Y);
93    }
94}
95
96impl Default for Camera3D {
97    fn default() -> Self { Self::perspective(60.0, 16.0 / 9.0, 0.1, 1000.0) }
98}
99
100// ── 4D Hyperbolic Camera ──────────────────────────────────────────────────────
101
102/// How the 4D hyperbolic scene is projected to 3D for rendering.
103#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
104pub enum HyperModel {
105    /// Klein (Beltrami-Klein) gnomonic projection: geodesics appear as straight lines.
106    Klein,
107    /// Poincaré ball model: angles are preserved, geodesics are circular arcs.
108    Poincare,
109    /// Cross-section: slice 4D scene at a fixed w-value, render the 3D slice.
110    CrossSection { w_slice: f32 },
111}
112
113/// A point in the hyperboloid model of ℍ⁴.
114/// Satisfies: x₀² − x₁² − x₂² − x₃² − x₄² = 1, x₀ > 0.
115/// Components: (x0=time, x1..x4=space).
116#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
117pub struct HyperPoint4D {
118    pub x0: f32,
119    pub x1: f32,
120    pub x2: f32,
121    pub x3: f32,
122    pub x4: f32,
123}
124
125impl HyperPoint4D {
126    /// The origin of ℍ⁴: (1, 0, 0, 0, 0).
127    pub const ORIGIN: Self = Self { x0: 1.0, x1: 0.0, x2: 0.0, x3: 0.0, x4: 0.0 };
128
129    pub fn new(x0: f32, x1: f32, x2: f32, x3: f32, x4: f32) -> Self {
130        Self { x0, x1, x2, x3, x4 }
131    }
132
133    pub fn minkowski_dot(&self, other: &Self) -> f32 {
134        -self.x0 * other.x0
135            + self.x1 * other.x1
136            + self.x2 * other.x2
137            + self.x3 * other.x3
138            + self.x4 * other.x4
139    }
140
141    /// Hyperbolic distance from self to other.
142    pub fn distance(&self, other: &Self) -> f32 {
143        (-self.minkowski_dot(other)).max(1.0).acosh()
144    }
145
146    /// Normalize back onto the hyperboloid after floating-point drift.
147    pub fn normalize(&self) -> Self {
148        let sq = self.x0 * self.x0
149            - self.x1 * self.x1
150            - self.x2 * self.x2
151            - self.x3 * self.x3
152            - self.x4 * self.x4;
153        if sq <= 0.0 { return *self; }
154        let s = sq.sqrt();
155        Self::new(self.x0 / s, self.x1 / s, self.x2 / s, self.x3 / s, self.x4 / s)
156    }
157
158    pub fn to_klein(&self) -> Vec4H {
159        Vec4H::new(
160            self.x1 / self.x0,
161            self.x2 / self.x0,
162            self.x3 / self.x0,
163            self.x4 / self.x0,
164        )
165    }
166
167    pub fn to_poincare(&self) -> Vec4H {
168        let d = 1.0 + self.x0;
169        Vec4H::new(
170            self.x1 / d,
171            self.x2 / d,
172            self.x3 / d,
173            self.x4 / d,
174        )
175    }
176}
177
178/// Camera in 4D hyperbolic space.
179/// The camera sits at a point in ℍ⁴ and projects scenes to 3D for final rendering.
180#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
181pub struct Camera4D {
182    /// Camera position in ℍ⁴ (hyperboloid model).
183    pub position: HyperPoint4D,
184    /// Local reference frame as a 5×5 Lorentz matrix (columns = local axes).
185    pub frame: Mat5,
186    pub model: HyperModel,
187    /// 3D camera used for the final perspective pass after 4D→3D projection.
188    pub cam3d: Camera3D,
189}
190
191impl Camera4D {
192    pub fn new(model: HyperModel) -> Self {
193        Self {
194            position: HyperPoint4D::ORIGIN,
195            frame: Mat5::identity(),
196            model,
197            cam3d: Camera3D::perspective(60.0, 16.0 / 9.0, 0.01, 100.0),
198        }
199    }
200
201    /// Project a 4D hyperbolic point to 3D Euclidean for rasterization.
202    pub fn project(&self, point: &HyperPoint4D) -> Option<Vec3> {
203        match self.model {
204            HyperModel::Klein => {
205                let k = point.to_klein();
206                // Drop one spatial axis (x4 → w, render xyz)
207                Some(Vec3::new(k.x, k.y, k.z))
208            }
209            HyperModel::Poincare => {
210                let p = point.to_poincare();
211                Some(Vec3::new(p.x, p.y, p.z))
212            }
213            HyperModel::CrossSection { w_slice } => {
214                // Keep only points near the slice w_slice
215                let k = point.to_klein();
216                if (k.w - w_slice).abs() > 0.5 { return None; }
217                Some(Vec3::new(k.x, k.y, k.z))
218            }
219        }
220    }
221
222    /// Move the camera along a hyperbolic geodesic (Lorentz boost).
223    /// `direction` is a 4D spatial direction vector; `dist` is hyperbolic distance.
224    pub fn move_by(&mut self, direction: Vec4H, dist: f32) {
225        let len = direction.length();
226        if len < 1e-8 { return; }
227        let d = direction * (1.0 / len);
228        let ch = dist.cosh();
229        let sh = dist.sinh();
230        // Boost the origin point along `d`
231        let p = &self.position;
232        self.position = HyperPoint4D::new(
233            ch * p.x0 + sh * (d.x * p.x1 + d.y * p.x2 + d.z * p.x3 + d.w * p.x4),
234            p.x1 + (sh * p.x0 + (ch - 1.0) * (d.x * p.x1 + d.y * p.x2 + d.z * p.x3 + d.w * p.x4)) * d.x,
235            p.x2 + (sh * p.x0 + (ch - 1.0) * (d.x * p.x1 + d.y * p.x2 + d.z * p.x3 + d.w * p.x4)) * d.y,
236            p.x3 + (sh * p.x0 + (ch - 1.0) * (d.x * p.x1 + d.y * p.x2 + d.z * p.x3 + d.w * p.x4)) * d.z,
237            p.x4 + (sh * p.x0 + (ch - 1.0) * (d.x * p.x1 + d.y * p.x2 + d.z * p.x3 + d.w * p.x4)) * d.w,
238        ).normalize();
239    }
240}
241
242impl Default for Camera4D {
243    fn default() -> Self { Self::new(HyperModel::Klein) }
244}