Skip to main content

ling_graphics/
camera.rs

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