1#![deny(missing_docs)]
2use collide_ray::Ray;
27use ga3::Vector;
28
29pub trait Clip {
35 fn raycast(&self, ray: &Ray<Vector<f32>>, max_distance: f32) -> Option<f32>;
38}
39
40#[cfg(feature = "collide-mesh")]
41impl Clip for collide_mesh::CollisionWorld {
42 fn raycast(&self, ray: &Ray<Vector<f32>>, max_distance: f32) -> Option<f32> {
43 collide_mesh::CollisionWorld::raycast(self, ray, max_distance)
44 }
45}
46
47#[derive(Copy, Clone, Debug, PartialEq)]
52pub struct CameraConfig {
53 pub min_pitch: f32,
55 pub max_pitch: f32,
57 pub low_distance: f32,
59 pub high_distance: f32,
61 pub min_distance: f32,
63 pub min_zoom: f32,
65 pub max_zoom: f32,
67 pub zoom_step: f32,
69 pub focus_height: f32,
71 pub focus_strength: f32,
73 pub distance_strength: f32,
75 pub clip_start: f32,
77 pub clip_margin: f32,
79 pub default_pitch: f32,
81 pub field_of_view: f32,
83 pub near_plane: f32,
85 pub far_plane: f32,
87}
88
89impl Default for CameraConfig {
90 fn default() -> Self {
91 Self {
92 min_pitch: -0.524,
93 max_pitch: 1.047,
94 low_distance: 4.0,
95 high_distance: 9.0,
96 min_distance: 1.0,
97 min_zoom: 0.5,
98 max_zoom: 2.0,
99 zoom_step: 0.2,
100 focus_height: 1.5,
101 focus_strength: 4.0,
102 distance_strength: 8.0,
103 clip_start: 0.5,
104 clip_margin: 0.2,
105 default_pitch: 0.4,
106 field_of_view: std::f32::consts::FRAC_PI_3,
107 near_plane: 0.1,
108 far_plane: 200.0,
109 }
110 }
111}
112
113#[derive(Copy, Clone, Debug)]
118pub struct MovementBasis {
119 pub forward: Vector<f32>,
121 pub right: Vector<f32>,
123}
124
125#[derive(Copy, Clone, Debug)]
127pub struct ViewParameters {
128 pub eye: Vector<f32>,
130 pub focus: Vector<f32>,
132 pub up: Vector<f32>,
134 pub field_of_view: f32,
136 pub near_plane: f32,
138 pub far_plane: f32,
140}
141
142#[derive(Copy, Clone, Debug)]
144pub struct OrbitCamera {
145 yaw: f32,
146 pitch: f32,
147 zoom_factor: f32,
148 distance: f32,
149 focus: Vector<f32>,
150 config: CameraConfig,
151}
152
153impl OrbitCamera {
154 pub fn new(target: Vector<f32>) -> Self {
156 Self::facing(target, 0.0, CameraConfig::default())
157 }
158
159 pub fn facing(target: Vector<f32>, yaw: f32, config: CameraConfig) -> Self {
161 let mut camera = Self {
162 yaw,
163 pitch: config.default_pitch,
164 zoom_factor: 1.0,
165 distance: 0.0,
166 focus: target + Vector::y(config.focus_height),
167 config,
168 };
169 camera.distance = camera.goal_distance();
170 camera
171 }
172
173 pub fn config(&self) -> &CameraConfig {
175 &self.config
176 }
177
178 pub fn set_config(&mut self, config: CameraConfig) {
181 self.config = config;
182 }
183
184 pub fn rotate(&mut self, delta: [f32; 2]) {
187 let [delta_yaw, delta_pitch] = delta;
188 self.yaw -= delta_yaw;
189 self.pitch = (self.pitch - delta_pitch).clamp(self.config.min_pitch, self.config.max_pitch);
190 }
191
192 pub fn yaw(&self) -> f32 {
194 self.yaw
195 }
196
197 pub fn set_yaw(&mut self, yaw: f32) {
201 self.yaw = yaw;
202 }
203
204 pub fn pitch(&self) -> f32 {
206 self.pitch
207 }
208
209 pub fn set_pitch(&mut self, pitch: f32) {
211 self.pitch = pitch.clamp(self.config.min_pitch, self.config.max_pitch);
212 }
213
214 pub fn focus(&self) -> Vector<f32> {
216 self.focus
217 }
218
219 pub fn set_focus(&mut self, focus: Vector<f32>) {
223 self.focus = focus;
224 }
225
226 pub fn distance(&self) -> f32 {
228 self.distance
229 }
230
231 pub fn set_distance(&mut self, distance: f32) {
235 self.distance = distance;
236 }
237
238 pub fn zoom(&mut self, amount: f32) {
240 self.zoom_factor = (self.zoom_factor * (-amount * self.config.zoom_step).exp2())
241 .clamp(self.config.min_zoom, self.config.max_zoom);
242 }
243
244 pub fn follow(&mut self, target: Vector<f32>, timestep: f32) {
247 let goal_focus = target + Vector::y(self.config.focus_height);
248 self.focus +=
249 (goal_focus - self.focus) * timed_friction(self.config.focus_strength, timestep);
250
251 let goal_distance = self.goal_distance();
252 self.distance += (goal_distance - self.distance)
253 * timed_friction(self.config.distance_strength, timestep);
254 }
255
256 pub fn clip<C: Clip>(&mut self, world: &C) {
259 if self.distance <= self.config.clip_start {
260 return;
261 }
262 let direction = (self.eye() - self.focus) / self.distance;
263 let ray = Ray::new(self.focus + direction * self.config.clip_start, direction);
264 if let Some(hit) = world.raycast(&ray, self.distance - self.config.clip_start) {
265 let clipped = (self.config.clip_start + hit - self.config.clip_margin)
266 .max(self.config.min_distance);
267 if clipped < self.distance {
268 self.distance = clipped;
269 }
270 }
271 }
272
273 pub fn eye(&self) -> Vector<f32> {
275 self.eye_at(self.distance)
276 }
277
278 pub fn eye_at(&self, distance: f32) -> Vector<f32> {
283 let (sin_pitch, cos_pitch) = self.pitch.sin_cos();
284 let (sin_yaw, cos_yaw) = self.yaw.sin_cos();
285 self.focus + Vector::new(cos_pitch * sin_yaw, sin_pitch, cos_pitch * cos_yaw) * distance
286 }
287
288 pub fn steer_toward(&mut self, look_direction: Vector<f32>, strength: f32, timestep: f32) {
292 use std::f32::consts::{PI, TAU};
293 let length = look_direction.x.hypot(look_direction.z);
294 if length < 1e-4 {
295 return;
296 }
297 let target_yaw = (-look_direction.x).atan2(-look_direction.z);
298 let difference = (target_yaw - self.yaw + PI).rem_euclid(TAU) - PI;
299 self.yaw += difference * timed_friction(strength, timestep);
300 }
301
302 pub fn forward_xz(&self) -> Vector<f32> {
304 Vector::new(-self.yaw.sin(), 0.0, -self.yaw.cos())
305 }
306
307 pub fn right_xz(&self) -> Vector<f32> {
309 Vector::new(self.yaw.cos(), 0.0, -self.yaw.sin())
310 }
311
312 pub fn basis(&self) -> MovementBasis {
314 MovementBasis {
315 forward: self.forward_xz(),
316 right: self.right_xz(),
317 }
318 }
319
320 pub fn view(&self) -> ViewParameters {
322 ViewParameters {
323 eye: self.eye(),
324 focus: self.focus,
325 up: Vector::y(1.0),
326 field_of_view: self.config.field_of_view,
327 near_plane: self.config.near_plane,
328 far_plane: self.config.far_plane,
329 }
330 }
331
332 fn goal_distance(&self) -> f32 {
333 let pitch_ratio =
334 (self.pitch - self.config.min_pitch) / (self.config.max_pitch - self.config.min_pitch);
335 (self.config.low_distance
336 + (self.config.high_distance - self.config.low_distance) * pitch_ratio)
337 * self.zoom_factor
338 }
339}
340
341fn timed_friction(strength: f32, timestep: f32) -> f32 {
342 1.0 - (-strength * timestep).exp2()
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn eye_sits_above_and_behind_focus() {
351 let camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
352 let offset = camera.eye() - camera.focus();
353 let length = (offset.x * offset.x + offset.y * offset.y + offset.z * offset.z).sqrt();
354 assert!(length > 0.0);
355 assert!(offset.y > 0.0);
356 }
357
358 #[test]
359 fn rotate_clamps_pitch() {
360 let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
361 camera.rotate([0.0, 100.0]);
362 assert!((camera.pitch() - camera.config().min_pitch).abs() < 1e-5);
363 camera.rotate([0.0, -100.0]);
364 assert!((camera.pitch() - camera.config().max_pitch).abs() < 1e-5);
365 }
366
367 #[test]
368 fn follow_moves_focus_toward_target() {
369 let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
370 let start = camera.focus();
371 camera.follow(Vector::new(10.0, 0.0, 0.0), 1.0 / 60.0);
372 assert!(camera.focus().x > start.x);
373 assert!(camera.focus().x < 10.0);
374 }
375
376 #[test]
377 fn set_focus_and_distance_bypass_easing() {
378 let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
379 let focus = Vector::new(3.0, 1.0, -2.0);
380 camera.set_focus(focus);
381 camera.set_distance(5.0);
382 assert_eq!(camera.focus(), focus);
383 assert_eq!(camera.distance(), 5.0);
384 }
385
386 #[test]
387 fn set_yaw_is_absolute_and_set_pitch_clamps() {
388 let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
389 camera.set_yaw(1.25);
390 assert_eq!(camera.yaw(), 1.25);
391 camera.set_pitch(100.0);
392 assert!((camera.pitch() - camera.config().max_pitch).abs() < 1e-6);
393 }
394
395 #[test]
396 fn eye_at_matches_eye_for_current_distance() {
397 let camera = OrbitCamera::new(Vector::new(1.0, 2.0, 3.0));
398 let eye = camera.eye();
399 let probed = camera.eye_at(camera.distance());
400 assert!((eye - probed).x.abs() < 1e-6);
401 assert!((eye - probed).y.abs() < 1e-6);
402 assert!((eye - probed).z.abs() < 1e-6);
403 }
404}