use glam::{Mat4, Vec2, Vec3};
use super::{Category, SceneObject};
use crate::ui::icons::path;
mod follow;
pub use follow::Follow;
use crate::ecs::{Component, Query, Res, ResMut, Resource};
use crate::hid::{Finger, GamepadState};
use crate::input::{KeyCode, Keys};
use crate::time::Time;
use crate::ui::MouseInput;
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Projection {
Perspective {
fov_y: f32,
},
Orthographic {
left: f32,
right: f32,
bottom: f32,
top: f32,
},
}
#[derive(Resource, Clone, Copy, PartialEq, Debug)]
pub struct Camera {
pub eye: Vec3,
pub target: Vec3,
pub up: Vec3,
pub projection: Projection,
pub near: f32,
pub far: f32,
}
impl Default for Camera {
fn default() -> Self {
Self {
eye: Vec3::new(0.0, 1.0, 2.0),
target: Vec3::ZERO,
up: Vec3::Y,
projection: Projection::Perspective {
fov_y: 45f32.to_radians(),
},
near: 0.01,
far: 100.0,
}
}
}
impl Camera {
pub fn looking_at(eye: Vec3, target: Vec3) -> Self {
Self {
eye,
target,
..Self::default()
}
}
pub fn orthographic(
mut self,
left: f32,
right: f32,
bottom: f32,
top: f32,
near: f32,
far: f32,
) -> Self {
self.projection = Projection::Orthographic {
left,
right,
bottom,
top,
};
self.near = near;
self.far = far;
self
}
pub fn perspective(mut self, fov_y: f32) -> Self {
self.projection = Projection::Perspective { fov_y };
self
}
pub fn up(mut self, up: Vec3) -> Self {
self.up = up;
self
}
fn screen_up(&self) -> Vec3 {
let forward = self.forward();
if forward.cross(self.up).length_squared() > 1e-8 {
return self.up;
}
match forward.y.abs() > 0.99 {
true => Vec3::NEG_Z,
false => Vec3::Y,
}
}
pub fn ray(&self, cursor: (f32, f32), width: f32, height: f32) -> Ray {
let ndc = Vec2::new(
2.0 * cursor.0 / width.max(1.0) - 1.0,
1.0 - 2.0 * cursor.1 / height.max(1.0),
);
let inverse = self.view_proj(width / height.max(1.0)).inverse();
let near = inverse * glam::Vec4::new(ndc.x, ndc.y, 0.0, 1.0);
let far = inverse * glam::Vec4::new(ndc.x, ndc.y, 1.0, 1.0);
let near = near.truncate() / near.w;
let far = far.truncate() / far.w;
Ray {
origin: near,
direction: (far - near).normalize_or(Vec3::NEG_Z),
}
}
pub fn view(&self) -> Mat4 {
glam::camera::rh::view::look_at_mat4(self.eye, self.target, self.screen_up())
}
pub fn projection(&self, aspect: f32) -> Mat4 {
match self.projection {
Projection::Perspective { fov_y } => glam::camera::rh::proj::directx::perspective(
fov_y,
aspect.max(0.001),
self.near,
self.far,
),
Projection::Orthographic {
left,
right,
bottom,
top,
} => glam::camera::rh::proj::directx::orthographic(
left, right, bottom, top, self.near, self.far,
),
}
}
pub fn view_proj(&self, aspect: f32) -> Mat4 {
self.projection(aspect) * self.view()
}
pub fn forward(&self) -> Vec3 {
(self.target - self.eye).normalize_or(Vec3::NEG_Z)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Ray {
pub origin: Vec3,
pub direction: Vec3,
}
impl Ray {
pub fn plane_hit(&self, y: f32) -> Option<Vec3> {
let distance = (y - self.origin.y) / self.direction.y;
(distance.is_finite() && distance >= 0.0).then(|| self.origin + self.direction * distance)
}
}
const PITCH_LIMIT: f32 = 1.5533431;
#[derive(Component, Clone, Copy, Debug)]
pub struct OrbitCamera {
pub focus: Vec3,
pub yaw: f32,
pub pitch: f32,
pub distance: f32,
pub move_speed: f32,
pub sensitivity: f32,
pub touchpad: bool,
pub touch_sensitivity: f32,
pub touch_zoom: f32,
pub zoom_step: f32,
pub min_distance: f32,
pub max_distance: f32,
gesture: Option<Gesture>,
}
#[derive(Clone, Copy, Debug)]
enum Gesture {
Drag(Finger),
Pinch { ids: (u8, u8), gap: f32 },
}
impl Gesture {
fn read(fingers: [Option<Finger>; 2]) -> Option<Gesture> {
match fingers {
[Some(a), Some(b)] => Some(Gesture::Pinch {
ids: (a.id.min(b.id), a.id.max(b.id)),
gap: (a.at - b.at).length(),
}),
[Some(one), None] | [None, Some(one)] => Some(Gesture::Drag(one)),
[None, None] => None,
}
}
}
impl Default for OrbitCamera {
fn default() -> Self {
Self {
focus: Vec3::ZERO,
yaw: 0.6,
pitch: 0.45,
distance: 6.0,
move_speed: 0.9,
sensitivity: 0.002,
touchpad: false,
touch_sensitivity: std::f32::consts::PI,
touch_zoom: 7.0,
zoom_step: 0.9,
min_distance: 0.2,
max_distance: 200.0,
gesture: None,
}
}
}
impl OrbitCamera {
pub fn object(name: impl Into<String>) -> SceneObject {
SceneObject::new(name, path::CAMERA, Category::Camera)
}
pub fn new(focus: Vec3, distance: f32) -> Self {
Self {
focus,
distance,
..Self::default()
}
}
pub fn yaw(mut self, radians: f32) -> Self {
self.yaw = radians;
self
}
pub fn pitch(mut self, radians: f32) -> Self {
self.pitch = radians;
self
}
pub fn range(mut self, min: f32, max: f32) -> Self {
self.min_distance = min.max(1e-4);
self.max_distance = max.max(self.min_distance);
self.distance = self.distance.clamp(self.min_distance, self.max_distance);
self
}
pub fn move_speed(mut self, units_per_second: f32) -> Self {
self.move_speed = units_per_second;
self
}
pub fn sensitivity(mut self, radians_per_count: f32) -> Self {
self.sensitivity = radians_per_count;
self
}
pub fn touchpad(mut self) -> Self {
self.touchpad = true;
self
}
pub fn touch_sensitivity(mut self, radians_per_sweep: f32) -> Self {
self.touch_sensitivity = radians_per_sweep;
self
}
pub fn touch_zoom(mut self, clicks_per_pinch: f32) -> Self {
self.touch_zoom = clicks_per_pinch;
self
}
pub fn eye(&self) -> Vec3 {
let (sin_pitch, cos_pitch) = self.pitch.sin_cos();
let (sin_yaw, cos_yaw) = self.yaw.sin_cos();
self.focus + Vec3::new(cos_pitch * sin_yaw, sin_pitch, cos_pitch * cos_yaw) * self.distance
}
pub fn camera(&self, base: Camera) -> Camera {
Camera {
eye: self.eye(),
target: self.focus,
up: Vec3::Y,
..base
}
}
fn pan(&mut self, motion: Vec2) {
const PER_COUNT: f32 = 0.0015;
let forward = (self.focus - self.eye()).normalize_or(Vec3::NEG_Z);
let right = forward.cross(Vec3::Y).normalize_or(Vec3::X);
let up = right.cross(forward);
let step = (up * motion.y - right * motion.x) * (self.distance * PER_COUNT);
self.focus += step;
}
fn orbit(&mut self, drag: Vec2, scale: f32) {
self.yaw -= drag.x * scale;
self.pitch = (self.pitch + drag.y * scale).clamp(-PITCH_LIMIT, PITCH_LIMIT);
}
fn zoom(&mut self, clicks: f32) {
if clicks == 0.0 {
return;
}
self.distance = (self.distance * self.zoom_step.powf(clicks))
.clamp(self.min_distance, self.max_distance);
}
fn drive_touchpad(&mut self, fingers: [Option<Finger>; 2]) {
if !self.touchpad {
self.gesture = None;
return;
}
let now = Gesture::read(fingers);
match (self.gesture, now) {
(Some(Gesture::Drag(before)), Some(Gesture::Drag(after))) if before.id == after.id => {
self.orbit(after.at - before.at, self.touch_sensitivity);
}
(
Some(Gesture::Pinch { ids: before, gap }),
Some(Gesture::Pinch {
ids: after,
gap: now,
}),
) if before == after => self.zoom((now - gap) * self.touch_zoom),
_ => {}
}
self.gesture = now;
}
pub fn drive(
&mut self,
delta: f32,
keys: &Keys,
mouse: &MouseInput,
fingers: [Option<Finger>; 2],
taken: bool,
) {
let mouse = &match taken {
true => MouseInput::default(),
false => *mouse,
};
if mouse.right_down {
self.orbit(mouse.motion, self.sensitivity);
}
if mouse.middle_down {
self.pan(mouse.motion);
}
self.drive_touchpad(fingers);
self.zoom(mouse.scroll);
let (sin_yaw, cos_yaw) = self.yaw.sin_cos();
let forward = Vec3::new(-sin_yaw, 0.0, -cos_yaw);
let right = Vec3::new(cos_yaw, 0.0, -sin_yaw);
let mut step = Vec3::ZERO;
if keys.pressed(KeyCode::KeyW) {
step += forward;
}
if keys.pressed(KeyCode::KeyS) {
step -= forward;
}
if keys.pressed(KeyCode::KeyD) {
step += right;
}
if keys.pressed(KeyCode::KeyA) {
step -= right;
}
if keys.pressed(KeyCode::KeyE) {
step += Vec3::Y;
}
if keys.pressed(KeyCode::KeyQ) {
step -= Vec3::Y;
}
if step != Vec3::ZERO {
let speed = self.move_speed * self.distance;
self.focus += step.normalize() * speed * delta;
}
}
}
pub fn orbit_camera_system(
time: Res<Time>,
keys: Res<Keys>,
mouse: Res<MouseInput>,
capture: Res<crate::ui::PointerCapture>,
pad: Res<GamepadState>,
mut camera: ResMut<Camera>,
mut views: ResMut<crate::views::Views>,
mut rigs: Query<&mut OrbitCamera>,
) {
let fingers = pad.fingers();
for mut rig in &mut rigs {
rig.drive(time.delta, &keys, &mouse, fingers, capture.taken());
*camera = rig.camera(*camera);
views.set_camera(*camera);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dragged(motion: Vec2) -> OrbitCamera {
let mut rig = OrbitCamera::new(Vec3::ZERO, 4.0).yaw(0.0).pitch(0.0);
let held = MouseInput {
right_down: true,
motion,
..MouseInput::default()
};
rig.drive(0.016, &Keys::default(), &held, [None, None], false);
rig
}
#[test]
fn a_middle_drag_slides_the_world_with_the_mouse() {
let mut rig = OrbitCamera::new(Vec3::ZERO, 4.0).yaw(0.0).pitch(0.0);
let held = MouseInput {
middle_down: true,
motion: Vec2::new(100.0, 50.0),
..MouseInput::default()
};
rig.drive(0.016, &Keys::default(), &held, [None, None], false);
assert!(rig.focus.x < 0.0, "{}", rig.focus);
assert!(rig.focus.y > 0.0, "{}", rig.focus);
assert!(rig.focus.z.abs() < 1e-5, "no drift along the line of sight");
assert_eq!((rig.yaw, rig.pitch), (0.0, 0.0), "panning does not turn");
let mut far = OrbitCamera::new(Vec3::ZERO, 40.0).yaw(0.0).pitch(0.0);
far.drive(0.016, &Keys::default(), &held, [None, None], false);
assert!(
(far.focus.x / rig.focus.x - 10.0).abs() < 1e-3,
"ten times the range, ten times the slide"
);
}
#[test]
fn the_eye_starts_on_the_far_side_and_swings_round() {
let rig = OrbitCamera::new(Vec3::ZERO, 4.0).yaw(0.0).pitch(0.0);
let eye = rig.eye();
assert!((eye - Vec3::new(0.0, 0.0, 4.0)).length() < 1e-4, "{eye}");
let quarter = OrbitCamera {
yaw: std::f32::consts::FRAC_PI_2,
..rig
};
let eye = quarter.eye();
assert!((eye - Vec3::new(4.0, 0.0, 0.0)).length() < 1e-3, "{eye}");
}
#[test]
fn moving_the_mouse_without_the_button_turns_nothing() {
let mut rig = OrbitCamera::new(Vec3::ZERO, 4.0).yaw(0.0).pitch(0.0);
let loose = MouseInput {
motion: Vec2::new(900.0, 900.0),
..MouseInput::default()
};
rig.drive(0.016, &Keys::default(), &loose, [None, None], false);
assert_eq!(rig.yaw, 0.0);
assert_eq!(rig.pitch, 0.0);
}
#[test]
fn dragging_moves_the_world_the_way_the_mouse_goes() {
let rig = dragged(Vec2::new(40.0, 0.0));
assert!(rig.yaw < 0.0, "{}", rig.yaw);
assert!(rig.eye().x < 0.0, "the eye came round to -X");
let rig = dragged(Vec2::new(0.0, 40.0));
assert!(rig.pitch > 0.0);
assert!(rig.eye().y > 0.0);
}
#[test]
fn a_count_turns_the_same_amount_whatever_the_frame_rate() {
let one = dragged(Vec2::new(10.0, 0.0));
let mut ten = OrbitCamera::new(Vec3::ZERO, 4.0).yaw(0.0).pitch(0.0);
let held = MouseInput {
right_down: true,
motion: Vec2::new(1.0, 0.0),
..MouseInput::default()
};
for _ in 0..10 {
ten.drive(0.001, &Keys::default(), &held, [None, None], false);
}
assert!(
(one.yaw - ten.yaw).abs() < 1e-6,
"{} vs {}",
one.yaw,
ten.yaw
);
}
#[test]
fn the_pitch_stops_short_of_straight_down() {
let rig = dragged(Vec2::new(0.0, 100_000.0));
assert!(rig.pitch <= PITCH_LIMIT);
assert!(rig.eye().normalize().dot(Vec3::Y) < 0.9999);
}
#[test]
fn the_wheel_scales_the_range_rather_than_stepping_it() {
let mut rig = OrbitCamera::new(Vec3::ZERO, 10.0);
let wheel = MouseInput {
scroll: 2.0,
..MouseInput::default()
};
rig.drive(0.016, &Keys::default(), &wheel, [None, None], false);
assert!(
(rig.distance - 10.0 * 0.9 * 0.9).abs() < 1e-4,
"{}",
rig.distance
);
let mut rig = OrbitCamera::new(Vec3::ZERO, 10.0);
let spun = MouseInput {
scroll: 500.0,
..MouseInput::default()
};
rig.drive(0.016, &Keys::default(), &spun, [None, None], false);
assert_eq!(rig.distance, rig.min_distance);
}
fn with_touchpad() -> OrbitCamera {
OrbitCamera::new(Vec3::ZERO, 4.0)
.yaw(0.0)
.pitch(0.0)
.touchpad()
.touch_sensitivity(4.0)
}
fn finger(id: u8, x: f32, y: f32) -> Option<Finger> {
Some(Finger {
id,
at: Vec2::new(x, y),
})
}
fn touch(rig: &mut OrbitCamera, fingers: [Option<Finger>; 2]) {
rig.drive(
0.016,
&Keys::default(),
&MouseInput::default(),
fingers,
false,
);
}
fn swiped(rig: &mut OrbitCamera, id: u8, from: Vec2, to: Vec2) {
for at in [from, to] {
touch(rig, [Some(Finger { id, at }), None]);
}
}
fn pinched(rig: &mut OrbitCamera, from: f32, to: f32) {
for gap in [from, to] {
let (left, right) = (0.5 - gap * 0.5, 0.5 + gap * 0.5);
touch(rig, [finger(1, left, 0.5), finger(2, right, 0.5)]);
}
}
#[test]
fn the_touchpad_is_ignored_unless_a_scene_asks_for_it() {
let mut rig = OrbitCamera::new(Vec3::ZERO, 4.0).yaw(0.0).pitch(0.0);
swiped(&mut rig, 1, Vec2::new(0.2, 0.5), Vec2::new(0.8, 0.5));
assert_eq!(rig.yaw, 0.0);
pinched(&mut rig, 0.2, 0.8);
assert_eq!(rig.distance, 4.0);
}
#[test]
fn a_touch_drag_orbits_the_way_a_right_drag_does() {
let mut rig = with_touchpad();
swiped(&mut rig, 1, Vec2::new(0.2, 0.5), Vec2::new(0.45, 0.5));
assert!((rig.yaw - -1.0).abs() < 1e-5, "{}", rig.yaw);
let mut rig = with_touchpad();
swiped(&mut rig, 1, Vec2::new(0.5, 0.2), Vec2::new(0.5, 0.6));
assert!(rig.pitch > 0.0);
assert!(rig.eye().y > 0.0);
}
#[test]
fn putting_a_finger_down_somewhere_else_starts_a_new_drag() {
let mut rig = with_touchpad();
swiped(&mut rig, 1, Vec2::new(0.1, 0.5), Vec2::new(0.2, 0.5));
let after_first = rig.yaw;
swiped(&mut rig, 2, Vec2::new(0.9, 0.5), Vec2::new(0.9, 0.5));
assert_eq!(rig.yaw, after_first, "landing again turns nothing");
}
#[test]
fn lifting_a_finger_ends_the_drag() {
let mut rig = with_touchpad();
swiped(&mut rig, 1, Vec2::new(0.1, 0.5), Vec2::new(0.2, 0.5));
let after_first = rig.yaw;
touch(&mut rig, [None, None]);
swiped(&mut rig, 1, Vec2::new(0.9, 0.5), Vec2::new(0.9, 0.5));
assert_eq!(rig.yaw, after_first);
}
#[test]
fn opening_a_pinch_comes_closer_and_closing_it_backs_off() {
let mut rig = with_touchpad().touch_zoom(7.0);
pinched(&mut rig, 0.2, 0.7);
let opened = rig.distance;
assert!(opened < 4.0, "spreading two fingers zooms in: {opened}");
pinched(&mut rig, 0.7, 0.2);
assert!((rig.distance - 4.0).abs() < 1e-4, "{}", rig.distance);
}
#[test]
fn a_pinch_does_not_orbit_and_a_drag_does_not_zoom() {
let mut rig = with_touchpad();
touch(&mut rig, [finger(1, 0.2, 0.5), finger(2, 0.4, 0.5)]);
touch(&mut rig, [finger(1, 0.6, 0.5), finger(2, 0.8, 0.5)]);
assert_eq!(rig.yaw, 0.0, "a pinch is not a drag");
assert!(
(rig.distance - 4.0).abs() < 1e-5,
"and the gap never changed: {}",
rig.distance
);
let mut rig = with_touchpad();
swiped(&mut rig, 1, Vec2::new(0.2, 0.5), Vec2::new(0.8, 0.5));
assert_eq!(rig.distance, 4.0, "a drag is not a pinch");
}
#[test]
fn a_second_finger_landing_neither_orbits_nor_zooms() {
let mut rig = with_touchpad();
touch(&mut rig, [finger(1, 0.2, 0.5), None]);
touch(&mut rig, [finger(1, 0.2, 0.5), finger(2, 0.9, 0.5)]);
assert_eq!((rig.yaw, rig.distance), (0.0, 4.0));
touch(&mut rig, [finger(1, 0.2, 0.5), None]);
assert_eq!((rig.yaw, rig.distance), (0.0, 4.0));
}
#[test]
fn a_pinch_survives_its_fingers_swapping_slots() {
let mut rig = with_touchpad();
touch(&mut rig, [finger(1, 0.3, 0.5), finger(2, 0.7, 0.5)]);
touch(&mut rig, [finger(2, 0.8, 0.5), finger(1, 0.2, 0.5)]);
assert!(rig.distance < 4.0, "the gap opened: {}", rig.distance);
}
#[test]
fn walking_stays_on_the_ground_however_the_camera_is_tilted() {
let mut rig = OrbitCamera::new(Vec3::ZERO, 4.0).yaw(0.0).pitch(1.2);
let mut keys = Keys::default();
keys.press(KeyCode::KeyW, false);
rig.drive(0.5, &keys, &MouseInput::default(), [None, None], false);
assert_eq!(rig.focus.y, 0.0, "W must not fly into the floor");
assert!(rig.focus.z < 0.0, "forward at yaw 0 is -Z");
}
}