1#![deny(missing_docs)]
2use collide_ray::Ray;
27use ga3::{Rotor, Vector};
28use inner_space::InnerSpace;
29
30pub trait Clip {
36 fn raycast(&self, ray: &Ray<Vector<f32>>, max_distance: f32) -> Option<f32>;
39}
40
41#[cfg(feature = "collide-mesh")]
42impl Clip for collide_mesh::CollisionWorld {
43 fn raycast(&self, ray: &Ray<Vector<f32>>, max_distance: f32) -> Option<f32> {
44 collide_mesh::CollisionWorld::raycast(self, ray, max_distance)
45 }
46}
47
48#[derive(Copy, Clone, Debug, PartialEq)]
53pub struct CameraConfig {
54 pub min_pitch: f32,
56 pub max_pitch: f32,
58 pub low_distance: f32,
60 pub high_distance: f32,
62 pub min_distance: f32,
64 pub min_zoom: f32,
66 pub max_zoom: f32,
68 pub zoom_step: f32,
70 pub focus_height: f32,
72 pub focus_strength: f32,
74 pub distance_strength: f32,
76 pub clip_start: f32,
78 pub clip_margin: f32,
80 pub default_pitch: f32,
82 pub field_of_view: f32,
84 pub near_plane: f32,
86 pub far_plane: f32,
88}
89
90impl Default for CameraConfig {
91 fn default() -> Self {
92 Self {
93 min_pitch: -0.524,
94 max_pitch: 1.047,
95 low_distance: 4.0,
96 high_distance: 9.0,
97 min_distance: 1.0,
98 min_zoom: 0.5,
99 max_zoom: 2.0,
100 zoom_step: 0.2,
101 focus_height: 1.5,
102 focus_strength: 4.0,
103 distance_strength: 8.0,
104 clip_start: 0.5,
105 clip_margin: 0.2,
106 default_pitch: 0.4,
107 field_of_view: std::f32::consts::FRAC_PI_3,
108 near_plane: 0.1,
109 far_plane: 200.0,
110 }
111 }
112}
113
114#[derive(Copy, Clone, Debug)]
119pub struct MovementBasis {
120 pub forward: Vector<f32>,
122 pub right: Vector<f32>,
124}
125
126#[derive(Copy, Clone, Debug)]
128pub struct ViewParameters {
129 pub eye: Vector<f32>,
131 pub focus: Vector<f32>,
133 pub up: Vector<f32>,
136 pub field_of_view: f32,
138 pub near_plane: f32,
140 pub far_plane: f32,
142}
143
144#[derive(Copy, Clone, Debug)]
146pub struct OrbitCamera {
147 yaw: f32,
148 pitch: f32,
149 zoom_factor: f32,
150 distance: f32,
151 focus: Vector<f32>,
152 rotation: Rotor<f32>,
153 config: CameraConfig,
154}
155
156const IDENTITY: Rotor<f32> = Rotor {
157 scalar: 1.0,
158 xy: 0.0,
159 xz: 0.0,
160 yz: 0.0,
161};
162
163fn rotation_between(from: Vector<f32>, to: Vector<f32>) -> Rotor<f32> {
164 (from + to) * from
165}
166
167impl OrbitCamera {
168 pub fn new(target: Vector<f32>) -> Self {
170 Self::facing(target, 0.0, CameraConfig::default())
171 }
172
173 pub fn facing(target: Vector<f32>, yaw: f32, config: CameraConfig) -> Self {
175 let mut camera = Self {
176 yaw,
177 pitch: config.default_pitch,
178 zoom_factor: 1.0,
179 distance: 0.0,
180 focus: target + Vector::y(config.focus_height),
181 rotation: IDENTITY,
182 config,
183 };
184 camera.distance = camera.goal_distance();
185 camera
186 }
187
188 pub fn config(&self) -> &CameraConfig {
190 &self.config
191 }
192
193 pub fn set_config(&mut self, config: CameraConfig) {
196 self.config = config;
197 }
198
199 pub fn rotate(&mut self, delta: [f32; 2]) {
202 let [delta_yaw, delta_pitch] = delta;
203 self.yaw -= delta_yaw;
204 self.pitch = (self.pitch - delta_pitch).clamp(self.config.min_pitch, self.config.max_pitch);
205 }
206
207 pub fn yaw(&self) -> f32 {
209 self.yaw
210 }
211
212 pub fn set_yaw(&mut self, yaw: f32) {
216 self.yaw = yaw;
217 }
218
219 pub fn pitch(&self) -> f32 {
221 self.pitch
222 }
223
224 pub fn set_pitch(&mut self, pitch: f32) {
226 self.pitch = pitch.clamp(self.config.min_pitch, self.config.max_pitch);
227 }
228
229 pub fn focus(&self) -> Vector<f32> {
231 self.focus
232 }
233
234 pub fn set_focus(&mut self, focus: Vector<f32>) {
238 self.focus = focus;
239 }
240
241 pub fn distance(&self) -> f32 {
243 self.distance
244 }
245
246 pub fn set_distance(&mut self, distance: f32) {
250 self.distance = distance;
251 }
252
253 pub fn up(&self) -> Vector<f32> {
256 self.rotation.rotate(Vector::y(1.0))
257 }
258
259 pub fn rotation(&self) -> Rotor<f32> {
262 self.rotation
263 }
264
265 pub fn set_rotation(&mut self, rotation: Rotor<f32>) {
268 self.rotation = rotation;
269 }
270
271 pub fn align_up(&mut self, target_up: Vector<f32>) {
277 if target_up.magnitude2() < 1e-12 {
278 return;
279 }
280 let current = self.up();
281 let target = target_up.normalize();
282 if (current + target).magnitude2() < 1e-6 {
283 return;
284 }
285 self.rotation = (self.rotation * rotation_between(current, target)).normalize();
286 }
287
288 pub fn zoom(&mut self, amount: f32) {
290 self.zoom_factor = (self.zoom_factor * (-amount * self.config.zoom_step).exp2())
291 .clamp(self.config.min_zoom, self.config.max_zoom);
292 }
293
294 pub fn follow(&mut self, target: Vector<f32>, timestep: f32) {
297 let goal_focus = target + self.rotation.rotate(Vector::y(self.config.focus_height));
298 self.focus +=
299 (goal_focus - self.focus) * timed_friction(self.config.focus_strength, timestep);
300
301 let goal_distance = self.goal_distance();
302 self.distance += (goal_distance - self.distance)
303 * timed_friction(self.config.distance_strength, timestep);
304 }
305
306 pub fn clip<C: Clip>(&mut self, world: &C) {
309 if self.distance <= self.config.clip_start {
310 return;
311 }
312 let direction = (self.eye() - self.focus) / self.distance;
313 let ray = Ray::new(self.focus + direction * self.config.clip_start, direction);
314 if let Some(hit) = world.raycast(&ray, self.distance - self.config.clip_start) {
315 let clipped = (self.config.clip_start + hit - self.config.clip_margin)
316 .max(self.config.min_distance);
317 if clipped < self.distance {
318 self.distance = clipped;
319 }
320 }
321 }
322
323 pub fn eye(&self) -> Vector<f32> {
325 self.eye_at(self.distance)
326 }
327
328 pub fn eye_at(&self, distance: f32) -> Vector<f32> {
333 let (sin_pitch, cos_pitch) = self.pitch.sin_cos();
334 let (sin_yaw, cos_yaw) = self.yaw.sin_cos();
335 let local = Vector::new(cos_pitch * sin_yaw, sin_pitch, cos_pitch * cos_yaw);
336 self.focus + self.rotation.rotate(local) * distance
337 }
338
339 pub fn steer_toward(&mut self, look_direction: Vector<f32>, strength: f32, timestep: f32) {
343 use std::f32::consts::{PI, TAU};
344 let length = look_direction.x.hypot(look_direction.z);
345 if length < 1e-4 {
346 return;
347 }
348 let target_yaw = (-look_direction.x).atan2(-look_direction.z);
349 let difference = (target_yaw - self.yaw + PI).rem_euclid(TAU) - PI;
350 self.yaw += difference * timed_friction(strength, timestep);
351 }
352
353 pub fn forward_xz(&self) -> Vector<f32> {
356 self.rotation
357 .rotate(Vector::new(-self.yaw.sin(), 0.0, -self.yaw.cos()))
358 }
359
360 pub fn right_xz(&self) -> Vector<f32> {
363 self.rotation
364 .rotate(Vector::new(self.yaw.cos(), 0.0, -self.yaw.sin()))
365 }
366
367 pub fn basis(&self) -> MovementBasis {
369 MovementBasis {
370 forward: self.forward_xz(),
371 right: self.right_xz(),
372 }
373 }
374
375 pub fn view(&self) -> ViewParameters {
377 ViewParameters {
378 eye: self.eye(),
379 focus: self.focus,
380 up: self.up(),
381 field_of_view: self.config.field_of_view,
382 near_plane: self.config.near_plane,
383 far_plane: self.config.far_plane,
384 }
385 }
386
387 fn goal_distance(&self) -> f32 {
388 let pitch_ratio =
389 (self.pitch - self.config.min_pitch) / (self.config.max_pitch - self.config.min_pitch);
390 (self.config.low_distance
391 + (self.config.high_distance - self.config.low_distance) * pitch_ratio)
392 * self.zoom_factor
393 }
394}
395
396fn timed_friction(strength: f32, timestep: f32) -> f32 {
397 1.0 - (-strength * timestep).exp2()
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn eye_sits_above_and_behind_focus() {
406 let camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
407 let offset = camera.eye() - camera.focus();
408 let length = (offset.x * offset.x + offset.y * offset.y + offset.z * offset.z).sqrt();
409 assert!(length > 0.0);
410 assert!(offset.y > 0.0);
411 }
412
413 #[test]
414 fn rotate_clamps_pitch() {
415 let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
416 camera.rotate([0.0, 100.0]);
417 assert!((camera.pitch() - camera.config().min_pitch).abs() < 1e-5);
418 camera.rotate([0.0, -100.0]);
419 assert!((camera.pitch() - camera.config().max_pitch).abs() < 1e-5);
420 }
421
422 #[test]
423 fn follow_moves_focus_toward_target() {
424 let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
425 let start = camera.focus();
426 camera.follow(Vector::new(10.0, 0.0, 0.0), 1.0 / 60.0);
427 assert!(camera.focus().x > start.x);
428 assert!(camera.focus().x < 10.0);
429 }
430
431 #[test]
432 fn set_focus_and_distance_bypass_easing() {
433 let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
434 let focus = Vector::new(3.0, 1.0, -2.0);
435 camera.set_focus(focus);
436 camera.set_distance(5.0);
437 assert_eq!(camera.focus(), focus);
438 assert_eq!(camera.distance(), 5.0);
439 }
440
441 #[test]
442 fn set_yaw_is_absolute_and_set_pitch_clamps() {
443 let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
444 camera.set_yaw(1.25);
445 assert_eq!(camera.yaw(), 1.25);
446 camera.set_pitch(100.0);
447 assert!((camera.pitch() - camera.config().max_pitch).abs() < 1e-6);
448 }
449
450 #[test]
451 fn eye_at_matches_eye_for_current_distance() {
452 let camera = OrbitCamera::new(Vector::new(1.0, 2.0, 3.0));
453 let eye = camera.eye();
454 let probed = camera.eye_at(camera.distance());
455 assert!((eye - probed).x.abs() < 1e-6);
456 assert!((eye - probed).y.abs() < 1e-6);
457 assert!((eye - probed).z.abs() < 1e-6);
458 }
459
460 #[test]
461 fn default_camera_has_world_up() {
462 let up = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0)).up();
463 assert!(up.x.abs() < 1e-6);
464 assert!((up.y - 1.0).abs() < 1e-6);
465 assert!(up.z.abs() < 1e-6);
466 }
467
468 #[test]
469 fn rotation_between_maps_from_onto_to() {
470 let from = Vector::new(0.0, 1.0, 0.0);
471 let to = Vector::new(1.0, 0.0, 0.0);
472 let mapped = rotation_between(from, to).rotate(from);
473 assert!((to - mapped).x.abs() < 1e-5);
474 assert!((to - mapped).y.abs() < 1e-5);
475 assert!((to - mapped).z.abs() < 1e-5);
476 }
477
478 #[test]
479 fn align_up_matches_target_normal() {
480 let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
481 let normal = Vector::new(1.0, 1.0, 0.0);
482 camera.align_up(normal);
483 let up = camera.up();
484 let scale = 1.0 / normal.magnitude();
485 assert!((up.x - normal.x * scale).abs() < 1e-4);
486 assert!((up.y - normal.y * scale).abs() < 1e-4);
487 assert!((up.z - normal.z * scale).abs() < 1e-4);
488 }
489
490 #[test]
491 fn incremental_align_tracks_up_in_every_plane() {
492 for (axis_a, axis_b, name) in [(0, 1, "xy"), (1, 2, "yz"), (0, 2, "xz")] {
493 let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
494 for step in 1..=200 {
495 let angle = step as f32 / 200.0 * 2.0;
496 let mut components = [0.0f32; 3];
497 components[axis_b] = angle.cos();
498 components[axis_a] = angle.sin();
499 let target = Vector::new(components[0], components[1], components[2]);
500 camera.align_up(target);
501 let up = camera.up();
502 let alignment = up.x * target.x + up.y * target.y + up.z * target.z;
503 assert!(
504 alignment > 0.999,
505 "plane {name}: up diverged at step {step}: dot {alignment}, up ({}, {}, {}) target ({}, {}, {})",
506 up.x,
507 up.y,
508 up.z,
509 target.x,
510 target.y,
511 target.z,
512 );
513 }
514 }
515 }
516
517 #[test]
518 fn eye_keeps_orbit_distance_after_align() {
519 let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
520 camera.align_up(Vector::new(0.3, 0.5, 0.8));
521 let offset = camera.eye() - camera.focus();
522 let length = (offset.x * offset.x + offset.y * offset.y + offset.z * offset.z).sqrt();
523 assert!((length - camera.distance()).abs() < 1e-4);
524 }
525}