#![deny(missing_docs)]
use collide_ray::Ray;
use ga3::Vector;
pub trait Clip {
fn raycast(&self, ray: &Ray<Vector<f32>>, max_distance: f32) -> Option<f32>;
}
#[cfg(feature = "collide-mesh")]
impl Clip for collide_mesh::CollisionWorld {
fn raycast(&self, ray: &Ray<Vector<f32>>, max_distance: f32) -> Option<f32> {
collide_mesh::CollisionWorld::raycast(self, ray, max_distance)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct CameraConfig {
pub min_pitch: f32,
pub max_pitch: f32,
pub low_distance: f32,
pub high_distance: f32,
pub min_distance: f32,
pub min_zoom: f32,
pub max_zoom: f32,
pub zoom_step: f32,
pub focus_height: f32,
pub focus_strength: f32,
pub distance_strength: f32,
pub clip_start: f32,
pub clip_margin: f32,
pub default_pitch: f32,
pub field_of_view: f32,
pub near_plane: f32,
pub far_plane: f32,
}
impl Default for CameraConfig {
fn default() -> Self {
Self {
min_pitch: -0.524,
max_pitch: 1.047,
low_distance: 4.0,
high_distance: 9.0,
min_distance: 1.0,
min_zoom: 0.5,
max_zoom: 2.0,
zoom_step: 0.2,
focus_height: 1.5,
focus_strength: 4.0,
distance_strength: 8.0,
clip_start: 0.5,
clip_margin: 0.2,
default_pitch: 0.4,
field_of_view: std::f32::consts::FRAC_PI_3,
near_plane: 0.1,
far_plane: 200.0,
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct MovementBasis {
pub forward: Vector<f32>,
pub right: Vector<f32>,
}
#[derive(Copy, Clone, Debug)]
pub struct ViewParameters {
pub eye: Vector<f32>,
pub focus: Vector<f32>,
pub up: Vector<f32>,
pub field_of_view: f32,
pub near_plane: f32,
pub far_plane: f32,
}
#[derive(Copy, Clone, Debug)]
pub struct OrbitCamera {
yaw: f32,
pitch: f32,
zoom_factor: f32,
distance: f32,
focus: Vector<f32>,
config: CameraConfig,
}
impl OrbitCamera {
pub fn new(target: Vector<f32>) -> Self {
Self::facing(target, 0.0, CameraConfig::default())
}
pub fn facing(target: Vector<f32>, yaw: f32, config: CameraConfig) -> Self {
let mut camera = Self {
yaw,
pitch: config.default_pitch,
zoom_factor: 1.0,
distance: 0.0,
focus: target + Vector::y(config.focus_height),
config,
};
camera.distance = camera.goal_distance();
camera
}
pub fn config(&self) -> &CameraConfig {
&self.config
}
pub fn set_config(&mut self, config: CameraConfig) {
self.config = config;
}
pub fn rotate(&mut self, delta: [f32; 2]) {
let [delta_yaw, delta_pitch] = delta;
self.yaw -= delta_yaw;
self.pitch = (self.pitch - delta_pitch).clamp(self.config.min_pitch, self.config.max_pitch);
}
pub fn yaw(&self) -> f32 {
self.yaw
}
pub fn set_yaw(&mut self, yaw: f32) {
self.yaw = yaw;
}
pub fn pitch(&self) -> f32 {
self.pitch
}
pub fn set_pitch(&mut self, pitch: f32) {
self.pitch = pitch.clamp(self.config.min_pitch, self.config.max_pitch);
}
pub fn focus(&self) -> Vector<f32> {
self.focus
}
pub fn set_focus(&mut self, focus: Vector<f32>) {
self.focus = focus;
}
pub fn distance(&self) -> f32 {
self.distance
}
pub fn set_distance(&mut self, distance: f32) {
self.distance = distance;
}
pub fn zoom(&mut self, amount: f32) {
self.zoom_factor = (self.zoom_factor * (-amount * self.config.zoom_step).exp2())
.clamp(self.config.min_zoom, self.config.max_zoom);
}
pub fn follow(&mut self, target: Vector<f32>, timestep: f32) {
let goal_focus = target + Vector::y(self.config.focus_height);
self.focus +=
(goal_focus - self.focus) * timed_friction(self.config.focus_strength, timestep);
let goal_distance = self.goal_distance();
self.distance += (goal_distance - self.distance)
* timed_friction(self.config.distance_strength, timestep);
}
pub fn clip<C: Clip>(&mut self, world: &C) {
if self.distance <= self.config.clip_start {
return;
}
let direction = (self.eye() - self.focus) / self.distance;
let ray = Ray::new(self.focus + direction * self.config.clip_start, direction);
if let Some(hit) = world.raycast(&ray, self.distance - self.config.clip_start) {
let clipped = (self.config.clip_start + hit - self.config.clip_margin)
.max(self.config.min_distance);
if clipped < self.distance {
self.distance = clipped;
}
}
}
pub fn eye(&self) -> Vector<f32> {
self.eye_at(self.distance)
}
pub fn eye_at(&self, distance: f32) -> Vector<f32> {
let (sin_pitch, cos_pitch) = self.pitch.sin_cos();
let (sin_yaw, cos_yaw) = self.yaw.sin_cos();
self.focus + Vector::new(cos_pitch * sin_yaw, sin_pitch, cos_pitch * cos_yaw) * distance
}
pub fn steer_toward(&mut self, look_direction: Vector<f32>, strength: f32, timestep: f32) {
use std::f32::consts::{PI, TAU};
let length = look_direction.x.hypot(look_direction.z);
if length < 1e-4 {
return;
}
let target_yaw = (-look_direction.x).atan2(-look_direction.z);
let difference = (target_yaw - self.yaw + PI).rem_euclid(TAU) - PI;
self.yaw += difference * timed_friction(strength, timestep);
}
pub fn forward_xz(&self) -> Vector<f32> {
Vector::new(-self.yaw.sin(), 0.0, -self.yaw.cos())
}
pub fn right_xz(&self) -> Vector<f32> {
Vector::new(self.yaw.cos(), 0.0, -self.yaw.sin())
}
pub fn basis(&self) -> MovementBasis {
MovementBasis {
forward: self.forward_xz(),
right: self.right_xz(),
}
}
pub fn view(&self) -> ViewParameters {
ViewParameters {
eye: self.eye(),
focus: self.focus,
up: Vector::y(1.0),
field_of_view: self.config.field_of_view,
near_plane: self.config.near_plane,
far_plane: self.config.far_plane,
}
}
fn goal_distance(&self) -> f32 {
let pitch_ratio =
(self.pitch - self.config.min_pitch) / (self.config.max_pitch - self.config.min_pitch);
(self.config.low_distance
+ (self.config.high_distance - self.config.low_distance) * pitch_ratio)
* self.zoom_factor
}
}
fn timed_friction(strength: f32, timestep: f32) -> f32 {
1.0 - (-strength * timestep).exp2()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eye_sits_above_and_behind_focus() {
let camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
let offset = camera.eye() - camera.focus();
let length = (offset.x * offset.x + offset.y * offset.y + offset.z * offset.z).sqrt();
assert!(length > 0.0);
assert!(offset.y > 0.0);
}
#[test]
fn rotate_clamps_pitch() {
let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
camera.rotate([0.0, 100.0]);
assert!((camera.pitch() - camera.config().min_pitch).abs() < 1e-5);
camera.rotate([0.0, -100.0]);
assert!((camera.pitch() - camera.config().max_pitch).abs() < 1e-5);
}
#[test]
fn follow_moves_focus_toward_target() {
let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
let start = camera.focus();
camera.follow(Vector::new(10.0, 0.0, 0.0), 1.0 / 60.0);
assert!(camera.focus().x > start.x);
assert!(camera.focus().x < 10.0);
}
#[test]
fn set_focus_and_distance_bypass_easing() {
let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
let focus = Vector::new(3.0, 1.0, -2.0);
camera.set_focus(focus);
camera.set_distance(5.0);
assert_eq!(camera.focus(), focus);
assert_eq!(camera.distance(), 5.0);
}
#[test]
fn set_yaw_is_absolute_and_set_pitch_clamps() {
let mut camera = OrbitCamera::new(Vector::new(0.0, 0.0, 0.0));
camera.set_yaw(1.25);
assert_eq!(camera.yaw(), 1.25);
camera.set_pitch(100.0);
assert!((camera.pitch() - camera.config().max_pitch).abs() < 1e-6);
}
#[test]
fn eye_at_matches_eye_for_current_distance() {
let camera = OrbitCamera::new(Vector::new(1.0, 2.0, 3.0));
let eye = camera.eye();
let probed = camera.eye_at(camera.distance());
assert!((eye - probed).x.abs() < 1e-6);
assert!((eye - probed).y.abs() < 1e-6);
assert!((eye - probed).z.abs() < 1e-6);
}
}