use std::f32::consts; use bevy::{ prelude::*, window::{CursorGrabMode, PrimaryWindow}, }; use crate::{ input::{Action, CameraMotion, InputAction, Lifecycle, Selection}, interaction::Selected, math::{ Easing, InterpPosition, InterpVelocity, Interpolation, InterpolationSystem, NearlyEq, Velocity, }, }; pub struct CameraControllerPlugin; #[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct CameraMotionSystem; impl Plugin for CameraControllerPlugin { fn build(&self, app: &mut App) { app.add_systems( Update, ( camera_control_kinematic, (process_physical_inputs, process_focus_inputs).before(InterpolationSystem), (camera_control_physical, interp_camera_focus).after(InterpolationSystem), ) .in_set(CameraMotionSystem), ); } } #[derive(Component)] pub struct CameraController { pub focus: Vec3, pub radius: f32, pub upside_down: bool, } impl Default for CameraController { fn default() -> Self { Self { focus: Vec3::ZERO, radius: 10.0, upside_down: false, } } } #[derive(Bundle)] pub struct CameraControllerBundle { controller: CameraController, velocity: Velocity, } impl CameraControllerBundle { pub fn new(focus: Vec3, position: Vec3) -> Self { Self { controller: CameraController { focus, radius: Vec3::distance(position, focus), ..default() }, velocity: Velocity(Vec3::ZERO), } } } fn camera_control_kinematic( mut q_window: Query<&mut Window, With>, mut input: EventReader, mut q_camera: Query<(&mut CameraController, &mut Transform, &Projection)>, ) { if input.is_empty() { return; } let (mut controller, mut xform, proj) = q_camera.single_mut(); let mut window = q_window.single_mut(); let mut any_ongoing = false; let mut any_started = false; let mut any_stopped = false; for action in input.read().filter(|action| { !matches!( action, InputAction(_, Action::Camera(CameraMotion::Translate(_))) ) }) { if let InputAction(lifecycle, Action::Camera(motion)) = action { use CameraMotion::*; match lifecycle { Lifecycle::Start => any_started = true, Lifecycle::Ongoing => any_ongoing = true, Lifecycle::Stop => any_stopped = true, } match motion { Pan(delta) => do_pan(proj, &window, *delta, &mut xform, &mut controller), Zoom(delta) => do_zoom(&window, *delta, &mut xform, &mut controller), Orbit(delta) => do_orbit(&window, *delta, *lifecycle, &mut xform, &mut controller), Rotate(delta) => do_rotate(&window, *delta, &mut xform, &mut controller), _ => {} } } } if any_started { capture_pointer(&mut window); } else if any_stopped && !any_ongoing { release_pointer(&mut window); } } fn process_physical_inputs( mut cmd: Commands, mut input: EventReader, q_camera: Query<(Entity, &Velocity), With>, mut q_interp: Query<&mut InterpVelocity>, ) { for action in input.read() { let &InputAction(_, Action::Camera(CameraMotion::Translate(delta))) = action else { continue; }; for (ent, velocity) in q_camera.iter() { let target = delta * 0.075; if let Ok(mut interp) = q_interp.get_mut(ent) { if interp.target.nearly_eq_within(target, 1e-3) { continue; } interp.initial = **velocity; interp.target = target; interp.t = 0.; } else { cmd.entity(ent).insert(InterpVelocity { inner: Interpolation { initial: **velocity, target, easing: Easing::Sine, ..default() }, accel_zero2max: 0.333, }); } } } } fn process_focus_inputs( mut cmd: Commands, mut input: EventReader, mut q_camera: Query<(Entity, &CameraController, &mut Velocity)>, q_interp_vel: Query<&InterpVelocity>, mut q_interp_pos: Query<&mut InterpPosition>, q_selected_xform: Query<&GlobalTransform, With>, ) { if input.read().any(|action| { matches!( action, InputAction(Lifecycle::Stop, Action::Selection(Selection::Focus)) ) }) { let Ok(target) = q_selected_xform .get_single() .map(|xform| xform.translation()) else { return; }; for (ent, controller, mut velocity) in q_camera.iter_mut() { // Zero out any existing camera velocity or interpolation **velocity = Vec3::ZERO; info!("Setting focus to {:?}", target); if q_interp_vel.get(ent).is_ok() { cmd.entity(ent).remove::(); } let initial = controller.focus; let easing = Easing::SineOut; let duration = 0.25; if let Ok(mut interp) = q_interp_pos.get_mut(ent) { if interp.target.nearly_eq_within(target, 1e-3) { continue; } interp.initial = initial; interp.target = target; interp.easing = easing; interp.t = 0.; interp.duration = duration; } else { cmd.entity(ent).insert(InterpPosition { inner: Interpolation { initial, target, easing, ..default() }, duration, }); } } } } fn camera_control_physical( mut q_camera: Query<(&mut CameraController, &mut Transform, &Velocity)>, ) { for (mut controller, mut xform, velocity) in q_camera.iter_mut() { let delta_pos = xform.rotation * (**velocity); xform.translation += delta_pos; controller.focus += delta_pos; } } fn interp_camera_focus( mut q_camera: Query<(&mut CameraController, &mut Transform, &InterpPosition)>, ) { for (mut controller, mut xform, interp) in q_camera.iter_mut() { let target_focus = interp.current(); let delta_pos = target_focus - controller.focus; controller.focus = target_focus; xform.translation += delta_pos; } } #[inline] fn capture_pointer(window: &mut Window) { window.cursor.visible = false; window.cursor.grab_mode = CursorGrabMode::Confined; } #[inline] fn release_pointer(window: &mut Window) { window.cursor.visible = true; window.cursor.grab_mode = CursorGrabMode::None; } #[inline] fn do_orbit( win: &Window, delta: Vec2, event_lifecycle: Lifecycle, xform: &mut Transform, controller: &mut CameraController, ) { // Set `upside_down` flag if the event just started or stopped if matches!(event_lifecycle, Lifecycle::Start | Lifecycle::Stop) { controller.upside_down = xform.up().y <= 0.; } // Translate to origin xform.translation += xform.forward() * controller.radius; // Calculate rotation let delta_x = { let delta = delta.x / (win.physical_width() as f32) * consts::PI * 2.0; match controller.upside_down { true => -delta, false => delta, } }; let delta_y = delta.y / (win.physical_height() as f32) * consts::PI; let yaw = Quat::from_rotation_y(-delta_x); let pitch = Quat::from_rotation_x(-delta_y); // Rotate around global Y and local X xform.rotation = yaw * xform.rotation; xform.rotation *= pitch; // Translate back to the original position xform.translation += xform.back() * controller.radius; } #[inline] fn do_pan( proj: &Projection, win: &Window, delta: Vec2, xform: &mut Transform, controller: &mut CameraController, ) { // Normalize the pan distance with respect to the window resolution let pan = match proj { Projection::Perspective(p) => { delta * Vec2 { x: (p.fov * p.aspect_ratio) / (win.physical_width() as f32), y: p.fov / (win.physical_height() as f32), } } _ => delta, }; // Pan along local XY plane, with the distance scaled by the radius let up = xform.up(); let left = xform.left(); let delta = (pan.y * up + pan.x * left) * controller.radius; // Update camera translation and focus point xform.translation += delta; controller.focus += delta; } #[inline] fn do_zoom(win: &Window, delta: f32, xform: &mut Transform, controller: &mut CameraController) { let delta = (delta * 1.5) / (win.physical_height() as f32); controller.radius -= controller.radius * delta; xform.translation = controller.focus + xform.back() * controller.radius; } #[inline] fn do_rotate(win: &Window, delta: Vec2, xform: &mut Transform, controller: &mut CameraController) { // TODO: Move this somewhere else let sensitivity = 0.75; let delta_x = delta.x / (win.physical_width() as f32) * consts::PI * 2.0 * sensitivity; let delta_y = delta.y / (win.physical_height() as f32) * consts::PI * sensitivity; let yaw = Quat::from_rotation_y(-delta_x); let pitch = Quat::from_rotation_x(-delta_y); // Rotate around global Y and local X xform.rotation = yaw * xform.rotation; xform.rotation *= pitch; // Update focus point controller.focus = xform.translation + xform.forward() * controller.radius; }