1use glam::{Mat4, Vec3, Vec4};
12use crate::math::springs::{SpringDamper, Spring3D as SpringDamper3};
13use crate::config::EngineConfig;
14
15#[derive(Clone, Debug)]
19pub struct CameraState {
20 pub view: Mat4,
21 pub projection: Mat4,
22 pub position: Vec3,
23 pub target: Vec3,
24 pub fov_degrees: f32,
25 pub aspect: f32,
26}
27
28impl CameraState {
29 pub fn unproject_ray(&self, ndc_x: f32, ndc_y: f32) -> Vec3 {
31 let inv_proj = self.projection.inverse();
32 let inv_view = self.view.inverse();
33 let clip = Vec4::new(ndc_x, ndc_y, -1.0, 1.0);
34 let view_space = inv_proj * clip;
35 let view_dir = Vec4::new(view_space.x, view_space.y, -1.0, 0.0);
36 let world_dir = inv_view * view_dir;
37 Vec3::new(world_dir.x, world_dir.y, world_dir.z).normalize_or_zero()
38 }
39
40 pub fn unproject_point(&self, ndc_x: f32, ndc_y: f32, depth: f32) -> Vec3 {
42 let inv = (self.projection * self.view).inverse();
43 let clip = Vec4::new(ndc_x, ndc_y, depth * 2.0 - 1.0, 1.0);
44 let world = inv * clip;
45 Vec3::new(world.x / world.w, world.y / world.w, world.z / world.w)
46 }
47
48 pub fn project(&self, world: Vec3) -> Vec3 {
50 let clip = self.projection * self.view * Vec4::new(world.x, world.y, world.z, 1.0);
51 if clip.w.abs() < 0.0001 { return Vec3::ZERO; }
52 Vec3::new(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w)
53 }
54
55 pub fn is_visible(&self, world: Vec3) -> bool {
57 let ndc = self.project(world);
58 ndc.x >= -1.0 && ndc.x <= 1.0 && ndc.y >= -1.0 && ndc.y <= 1.0 && ndc.z >= 0.0
59 }
60}
61
62#[derive(Debug, Clone)]
66pub struct TraumaShake {
67 pub trauma: f32,
69 pub decay_rate: f32,
71 pub max_translation: f32,
73 pub max_rotation: f32,
75 time: f32,
77}
78
79impl Default for TraumaShake {
80 fn default() -> Self {
81 Self {
82 trauma: 0.0,
83 decay_rate: 0.8,
84 max_translation: 0.3,
85 max_rotation: 3.0,
86 time: 0.0,
87 }
88 }
89}
90
91impl TraumaShake {
92 pub fn add(&mut self, amount: f32) {
93 self.trauma = (self.trauma + amount).min(1.0);
94 }
95
96 pub fn tick(&mut self, dt: f32) -> (Vec3, f32) {
97 self.trauma = (self.trauma - self.decay_rate * dt).max(0.0);
98 self.time += dt;
99 let shake_sq = self.trauma * self.trauma; let t = self.time;
101 let tx = (t * 47.3).sin() * shake_sq * self.max_translation;
102 let ty = (t * 31.7).cos() * shake_sq * self.max_translation;
103 let rot = (t * 23.1).sin() * shake_sq * self.max_rotation;
104 (Vec3::new(tx, ty, 0.0), rot)
105 }
106
107 pub fn is_idle(&self) -> bool { self.trauma < 0.001 }
108}
109
110#[derive(Debug, Clone)]
119pub struct OrbitalCamera {
120 pub target: Vec3,
121 pub azimuth: f32, pub elevation: f32, pub distance: f32,
124 target_spring: SpringDamper3,
126 dist_spring: SpringDamper,
128 pub dist_min: f32,
130 pub dist_max: f32,
131 pub elev_min: f32,
133 pub elev_max: f32,
134}
135
136impl OrbitalCamera {
137 pub fn new(target: Vec3, distance: f32) -> Self {
138 Self {
139 target,
140 azimuth: 0.0,
141 elevation: 0.4, distance,
143 target_spring: SpringDamper3::from_vec3(target, 10.0, 6.0),
144 dist_spring: SpringDamper::new(distance, 8.0, 5.0),
145 dist_min: 2.0,
146 dist_max: 200.0,
147 elev_min: -1.4,
148 elev_max: 1.4,
149 }
150 }
151
152 pub fn set_target(&mut self, pos: Vec3) {
154 self.target_spring.set_target(pos);
155 }
156
157 pub fn rotate(&mut self, delta_azimuth: f32, delta_elevation: f32) {
159 self.azimuth += delta_azimuth;
160 self.elevation = (self.elevation + delta_elevation)
161 .clamp(self.elev_min, self.elev_max);
162 }
163
164 pub fn zoom(&mut self, delta: f32) {
166 let new_dist = (self.distance + delta).clamp(self.dist_min, self.dist_max);
167 self.dist_spring.set_target(new_dist);
168 }
169
170 pub fn tick(&mut self, dt: f32) -> (Vec3, Vec3) {
172 let target = self.target_spring.tick(dt);
173 self.distance = self.dist_spring.tick_get(dt)
174 .clamp(self.dist_min, self.dist_max);
175
176 let eye = target + Vec3::new(
177 self.elevation.cos() * self.azimuth.sin() * self.distance,
178 self.elevation.sin() * self.distance,
179 self.elevation.cos() * self.azimuth.cos() * self.distance,
180 );
181
182 (eye, target)
183 }
184
185 pub fn view_matrix(&mut self, dt: f32) -> Mat4 {
187 let (eye, target) = self.tick(dt);
188 Mat4::look_at_rh(eye, target, Vec3::Y)
189 }
190}
191
192#[derive(Debug, Clone)]
196pub struct PathPoint {
197 pub position: Vec3,
198 pub target: Vec3,
199 pub fov: f32,
200 pub duration: f32,
202 pub ease: PathEasing,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq)]
208pub enum PathEasing {
209 Linear,
210 EaseInOut,
211 EaseIn,
212 EaseOut,
213 Instant, }
215
216impl PathEasing {
217 pub fn apply(&self, t: f32) -> f32 {
218 let t = t.clamp(0.0, 1.0);
219 match self {
220 PathEasing::Linear => t,
221 PathEasing::EaseIn => t * t,
222 PathEasing::EaseOut => t * (2.0 - t),
223 PathEasing::EaseInOut => t * t * (3.0 - 2.0 * t),
224 PathEasing::Instant => 1.0,
225 }
226 }
227}
228
229impl PathPoint {
230 pub fn new(position: Vec3, target: Vec3, fov: f32, duration: f32) -> Self {
231 Self { position, target, fov, duration, ease: PathEasing::EaseInOut }
232 }
233
234 pub fn instant(position: Vec3, target: Vec3, fov: f32) -> Self {
235 Self { position, target, fov, duration: 0.0, ease: PathEasing::Instant }
236 }
237}
238
239#[derive(Debug, Clone)]
241pub struct CinematicPath {
242 pub points: Vec<PathPoint>,
243 pub looping: bool,
244 elapsed: f32,
245 done: bool,
246}
247
248impl CinematicPath {
249 pub fn new(points: Vec<PathPoint>, looping: bool) -> Self {
250 Self { points, looping, elapsed: 0.0, done: false }
251 }
252
253 pub fn is_done(&self) -> bool { self.done }
254 pub fn is_playing(&self) -> bool { !self.points.is_empty() && !self.done }
255
256 pub fn reset(&mut self) {
257 self.elapsed = 0.0;
258 self.done = false;
259 }
260
261 pub fn tick(&mut self, dt: f32) -> (Vec3, Vec3, f32) {
263 if self.points.is_empty() || self.done {
264 return (Vec3::ZERO, Vec3::ZERO, 60.0);
265 }
266
267 self.elapsed += dt;
268
269 let mut t_accum = 0.0f32;
271 for i in 0..self.points.len() {
272 let next_i = (i + 1) % self.points.len();
273 if next_i == 0 && !self.looping { break; }
274 let seg_dur = self.points[next_i].duration.max(f32::EPSILON);
275 let seg_end = t_accum + seg_dur;
276
277 if self.elapsed <= seg_end {
278 let local_t = (self.elapsed - t_accum) / seg_dur;
279 let eased = self.points[next_i].ease.apply(local_t);
280 let a = &self.points[i];
281 let b = &self.points[next_i];
282 let pos = a.position.lerp(b.position, eased);
283 let tgt = a.target.lerp(b.target, eased);
284 let fov = a.fov + (b.fov - a.fov) * eased;
285 return (pos, tgt, fov);
286 }
287
288 t_accum = seg_end;
289 }
290
291 if self.looping {
293 let total = self.total_duration();
294 if total > 0.0 { self.elapsed %= total; }
295 return self.tick(0.0);
296 }
297
298 self.done = true;
299 let last = self.points.last().unwrap();
300 (last.position, last.target, last.fov)
301 }
302
303 pub fn total_duration(&self) -> f32 {
304 self.points.iter().map(|p| p.duration).sum()
305 }
306}
307
308pub struct ProofCamera {
312 pub position: SpringDamper3,
314 pub target: SpringDamper3,
315 pub fov: SpringDamper,
316
317 pub shake: TraumaShake,
319
320 pub orbital: Option<OrbitalCamera>,
322
323 pub path: Option<CinematicPath>,
325
326 pub aspect: f32,
328 pub near: f32,
329 pub far: f32,
330
331 total_time: f32,
333}
334
335impl ProofCamera {
336 pub fn new(config: &EngineConfig) -> Self {
337 let aspect = config.window_width as f32 / config.window_height.max(1) as f32;
338 Self {
339 position: SpringDamper3::from_vec3(Vec3::new(0.0, 0.0, 10.0), 12.0, 6.0),
340 target: SpringDamper3::from_vec3(Vec3::ZERO, 14.0, 7.0),
341 fov: SpringDamper::new(60.0, 8.0, 5.0),
342 shake: TraumaShake::default(),
343 orbital: None,
344 path: None,
345 aspect,
346 near: 0.1,
347 far: 1000.0,
348 total_time: 0.0,
349 }
350 }
351
352 pub fn add_trauma(&mut self, amount: f32) {
355 self.shake.add(amount);
356 }
357
358 pub fn move_to(&mut self, pos: Vec3) {
362 self.position.set_target(pos);
363 }
364
365 pub fn look_at(&mut self, target: Vec3) {
367 self.target.set_target(target);
368 }
369
370 pub fn zoom_to(&mut self, fov_degrees: f32) {
372 self.fov.set_target(fov_degrees);
373 }
374
375 pub fn set_position_instant(&mut self, pos: Vec3) {
377 self.position.x.position = pos.x;
378 self.position.y.position = pos.y;
379 self.position.z.position = pos.z;
380 self.position.set_target(pos);
381 }
382
383 pub fn begin_orbital(&mut self, target: Vec3, distance: f32) {
387 self.orbital = Some(OrbitalCamera::new(target, distance));
388 }
389
390 pub fn end_orbital(&mut self) { self.orbital = None; }
392
393 pub fn is_orbital(&self) -> bool { self.orbital.is_some() }
394
395 pub fn orbital_rotate(&mut self, delta_az: f32, delta_el: f32) {
397 if let Some(ref mut orb) = self.orbital {
398 orb.rotate(delta_az, delta_el);
399 }
400 }
401
402 pub fn orbital_zoom(&mut self, delta: f32) {
404 if let Some(ref mut orb) = self.orbital {
405 orb.zoom(delta);
406 }
407 }
408
409 pub fn begin_path(&mut self, points: Vec<PathPoint>, looping: bool) {
413 self.path = Some(CinematicPath::new(points, looping));
414 }
415
416 pub fn end_path(&mut self) { self.path = None; }
418
419 pub fn is_on_path(&self) -> bool {
420 self.path.as_ref().map(|p| p.is_playing()).unwrap_or(false)
421 }
422
423 pub fn tick(&mut self, dt: f32) -> CameraState {
427 self.total_time += dt;
428 let (shake_offset, _shake_rot) = self.shake.tick(dt);
429
430 let (pos, tgt, fov_deg) = if let Some(ref mut path) = self.path {
431 if path.is_playing() {
433 path.tick(dt)
434 } else {
435 self.path = None;
436 (
437 self.position.tick(dt),
438 self.target.tick(dt),
439 self.fov.tick_get(dt),
440 )
441 }
442 } else if let Some(ref mut orb) = self.orbital {
443 let (eye, target) = orb.tick(dt);
445 let fov = self.fov.tick_get(dt);
446 (eye, target, fov)
447 } else {
448 (
450 self.position.tick(dt),
451 self.target.tick(dt),
452 self.fov.tick_get(dt),
453 )
454 };
455
456 let final_pos = pos + shake_offset;
457 let view = Mat4::look_at_rh(final_pos, tgt, Vec3::Y);
458 let projection = Mat4::perspective_rh(
459 fov_deg.to_radians(), self.aspect, self.near, self.far,
460 );
461
462 CameraState { view, projection, position: final_pos, target: tgt,
463 fov_degrees: fov_deg, aspect: self.aspect }
464 }
465
466 pub fn on_resize(&mut self, width: u32, height: u32) {
467 self.aspect = width as f32 / height.max(1) as f32;
468 }
469}
470
471impl Default for ProofCamera {
472 fn default() -> Self { Self::new(&EngineConfig::default()) }
473}
474
475#[cfg(test)]
478mod tests {
479 use super::*;
480 use crate::config::EngineConfig;
481
482 #[test]
483 fn camera_tick_produces_finite_matrices() {
484 let config = EngineConfig::default();
485 let mut cam = ProofCamera::new(&config);
486 let state = cam.tick(0.016);
487 assert!(state.view.is_finite());
488 assert!(state.projection.is_finite());
489 }
490
491 #[test]
492 fn shake_decays_to_zero() {
493 let mut shake = TraumaShake::default();
494 shake.add(1.0);
495 for _ in 0..100 { shake.tick(0.016); }
496 assert!(shake.trauma < 0.01);
497 }
498
499 #[test]
500 fn orbital_camera_moves() {
501 let mut orb = OrbitalCamera::new(Vec3::ZERO, 10.0);
502 orb.rotate(0.5, 0.2);
503 let (eye, _) = orb.tick(0.016);
504 assert!(eye.length() > 5.0);
505 }
506
507 #[test]
508 fn cinematic_path_reaches_end() {
509 let points = vec![
510 PathPoint::new(Vec3::ZERO, Vec3::Z, 60.0, 1.0),
511 PathPoint::new(Vec3::X * 5.0, Vec3::Z, 60.0, 1.0),
512 ];
513 let mut path = CinematicPath::new(points, false);
514 let (start, _, _) = path.tick(0.01);
515 assert!(start.x < 0.5);
516 let (end, _, _) = path.tick(2.0);
517 assert!((end.x - 5.0).abs() < 0.1, "Expected near 5.0, got {}", end.x);
518 }
519
520 #[test]
521 fn unproject_ray_is_normalized() {
522 let config = EngineConfig::default();
523 let mut cam = ProofCamera::new(&config);
524 let state = cam.tick(0.016);
525 let ray = state.unproject_ray(0.0, 0.0);
526 assert!((ray.length() - 1.0).abs() < 0.001);
527 }
528
529 #[test]
530 fn on_resize_updates_aspect() {
531 let mut cam = ProofCamera::default();
532 cam.on_resize(1920, 1080);
533 let expected = 1920.0 / 1080.0;
534 assert!((cam.aspect - expected).abs() < 0.001);
535 }
536}