1use crate::math::{Mat5, Ray3, Vec4H};
2use glam::{Mat4, Quat, Vec3, Vec4};
3
4#[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 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#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
121pub enum HyperModel {
122 Klein,
124 Poincare,
126 CrossSection { w_slice: f32 },
128}
129
130#[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 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 pub fn distance(&self, other: &Self) -> f32 {
160 (-self.minkowski_dot(other)).max(1.0).acosh()
161 }
162
163 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#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
203pub struct Camera4D {
204 pub position: HyperPoint4D,
206 pub frame: Mat5,
208 pub model: HyperModel,
209 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 pub fn project(&self, point: &HyperPoint4D) -> Option<Vec3> {
225 match self.model {
226 HyperModel::Klein => {
227 let k = point.to_klein();
228 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 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 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 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}