use glam::{Vec2, Vec3};
use super::look::{build_orientation, orientation_to_aim, yaw_pitch_from_orientation};
use crate::Camera;
use crate::interaction::input::action_frame::ActionFrame;
pub struct ThirdPersonCameraController {
pub yaw: f32,
pub pitch: f32,
pub yaw_sensitivity: f32,
pub pitch_sensitivity: f32,
pub pitch_clamp: (f32, f32),
pub distance: f32,
pub height: f32,
pub shoulder_offset: Vec2,
}
impl ThirdPersonCameraController {
pub const DEFAULT_DISTANCE: f32 = 6.0;
pub const DEFAULT_HEIGHT: f32 = 1.2;
pub fn new(yaw_sensitivity: f32, pitch_sensitivity: f32) -> Self {
Self {
yaw: 0.0,
pitch: 0.0,
yaw_sensitivity,
pitch_sensitivity,
pitch_clamp: (-std::f32::consts::FRAC_PI_4, std::f32::consts::FRAC_PI_4),
distance: Self::DEFAULT_DISTANCE,
height: Self::DEFAULT_HEIGHT,
shoulder_offset: Vec2::ZERO,
}
}
pub fn with_distance(mut self, distance: f32) -> Self {
self.distance = distance;
self
}
pub fn with_height(mut self, height: f32) -> Self {
self.height = height;
self
}
pub fn with_shoulder_offset(mut self, offset: Vec2) -> Self {
self.shoulder_offset = offset;
self
}
pub fn with_pitch_clamp(mut self, min: f32, max: f32) -> Self {
self.pitch_clamp = if min <= max { (min, max) } else { (max, min) };
self
}
pub fn apply(&mut self, camera: &mut Camera, frame: &ActionFrame, target: Vec3) {
let look = frame.navigation.orbit;
self.yaw += look.x * self.yaw_sensitivity;
self.pitch -= look.y * self.pitch_sensitivity;
self.pitch = self.pitch.clamp(self.pitch_clamp.0, self.pitch_clamp.1);
let orientation = build_orientation(self.yaw, self.pitch);
let mut pivot = target + Vec3::Z * self.height;
if self.shoulder_offset.x != 0.0 {
pivot += self.right_dir() * self.shoulder_offset.x;
}
if self.shoulder_offset.y != 0.0 {
pivot += Vec3::Z * self.shoulder_offset.y;
}
camera.center = pivot;
camera.distance = self.distance;
camera.orientation = orientation;
}
pub fn sync_from_camera(&mut self, camera: &Camera) {
let (yaw, pitch) = yaw_pitch_from_orientation(camera.orientation);
self.yaw = yaw;
self.pitch = pitch.clamp(self.pitch_clamp.0, self.pitch_clamp.1);
}
pub fn set_look(&mut self, yaw: f32, pitch: f32) {
self.yaw = yaw;
self.pitch = pitch.clamp(self.pitch_clamp.0, self.pitch_clamp.1);
}
pub fn forward_dir(&self) -> Vec3 {
let (s, c) = self.yaw.sin_cos();
Vec3::new(s, c, 0.0)
}
pub fn right_dir(&self) -> Vec3 {
self.forward_dir().cross(Vec3::Z).normalize_or_zero()
}
pub fn aim_dir(&self) -> Vec3 {
orientation_to_aim(build_orientation(self.yaw, self.pitch))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cam() -> ThirdPersonCameraController {
ThirdPersonCameraController::new(0.0035, 0.0030)
}
fn look(x: f32, y: f32) -> ActionFrame {
let mut f = ActionFrame::default();
f.navigation.orbit = Vec2::new(x, y);
f
}
fn approx_eq(a: Vec3, b: Vec3) -> bool {
(a - b).length() < 1e-4
}
#[test]
fn default_forward_is_plus_y() {
assert!(approx_eq(cam().forward_dir(), Vec3::Y));
}
#[test]
fn right_look_increases_yaw() {
let mut c = cam();
let mut camera = Camera::default();
c.apply(&mut camera, &look(10.0, 0.0), Vec3::ZERO);
assert!(c.yaw > 0.0);
}
#[test]
fn pitch_clamp_is_respected() {
let mut c = cam().with_pitch_clamp(-0.1, 0.5);
let mut camera = Camera::default();
c.apply(&mut camera, &look(0.0, 100.0), Vec3::ZERO);
assert!((c.pitch - -0.1).abs() < 1e-5, "pitch={}", c.pitch);
}
#[test]
fn pivot_is_above_target_and_boom_pulls_back() {
let mut c = cam().with_distance(6.0).with_height(1.2);
let mut camera = Camera::default();
c.apply(&mut camera, &look(0.0, 0.0), Vec3::ZERO);
let eye = camera.eye_position();
assert!(approx_eq(eye, Vec3::new(0.0, -6.0, 1.2)), "eye={eye:?}");
}
#[test]
fn shoulder_offset_shifts_pivot_right() {
let mut c = cam()
.with_distance(6.0)
.with_height(1.2)
.with_shoulder_offset(Vec2::new(0.5, 0.0));
let mut camera = Camera::default();
c.apply(&mut camera, &look(0.0, 0.0), Vec3::ZERO);
assert!((camera.center.x - 0.5).abs() < 1e-5);
assert!((camera.center.z - 1.2).abs() < 1e-5);
}
#[test]
fn sync_from_camera_round_trips() {
let mut c = cam();
let mut camera = Camera::default();
c.set_look(0.6, 0.2);
c.apply(&mut camera, &look(0.0, 0.0), Vec3::ZERO);
let mut c2 = cam();
c2.sync_from_camera(&camera);
assert!((c2.yaw - 0.6).abs() < 1e-4, "yaw={}", c2.yaw);
assert!((c2.pitch - 0.2).abs() < 1e-4, "pitch={}", c2.pitch);
}
}