Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to control camera manually (e.g. with keyboard) #11

Merged
merged 10 commits into from
Apr 29, 2023
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Default controls:
- Works with orthographic camera projection in addition to perspective
- Customisable controls, sensitivity, and more
- Works with multiple viewports and/or windows
- Easy to animate, e.g. to slowly rotate around an object
- Easy to control manually, e.g. for keyboard control or animation

## Quick Start

Expand Down
7 changes: 5 additions & 2 deletions examples/animate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ fn setup(
commands.spawn((
Camera3dBundle::default(),
PanOrbitCamera {
// Optionally disable smoothing to have full control over the camera's position
// orbit_smoothness: 0.0,
// Disable smoothing, since the animation takes care of that
orbit_smoothness: 0.0,
// Might want to disable the controls
enabled: false,
..default()
Expand All @@ -61,5 +61,8 @@ fn animate(time: Res<Time>, mut pan_orbit_query: Query<&mut PanOrbitCamera>) {
pan_orbit.target_alpha += 15f32.to_radians() * time.delta_seconds();
pan_orbit.target_beta = time.elapsed_seconds_wrapped().sin() * TAU * 0.1;
pan_orbit.radius = (((time.elapsed_seconds_wrapped() * 2.0).cos() + 1.0) * 0.5) * 2.0 + 4.0;

// Force camera to update its transform
pan_orbit.force_update = true;
}
}
114 changes: 114 additions & 0 deletions examples/keyboard_controls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! Demonstrates how to control the camera using the keyboard

use bevy::prelude::*;
use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin};
use std::f32::consts::TAU;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(PanOrbitCameraPlugin)
.add_startup_system(setup)
.add_system(keyboard_controls)
.run();
}

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Ground
commands.spawn(PbrBundle {
mesh: meshes.add(shape::Plane::from_size(5.0).into()),
material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()),
..default()
});
// Cube
commands.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
});
// Light
commands.spawn(PointLightBundle {
point_light: PointLight {
intensity: 1500.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
// Camera
commands.spawn((Camera3dBundle::default(), PanOrbitCamera::default()));
}

fn keyboard_controls(
time: Res<Time>,
key_input: Res<Input<KeyCode>>,
mut pan_orbit_query: Query<(&mut PanOrbitCamera, &mut Transform)>,
) {
for (mut pan_orbit, mut transform) in pan_orbit_query.iter_mut() {
// Jump by 45 degrees using Left Ctrl + Arrows
if key_input.pressed(KeyCode::LControl) {
if key_input.just_pressed(KeyCode::Right) {
pan_orbit.target_alpha += 45f32.to_radians();
}
if key_input.just_pressed(KeyCode::Left) {
pan_orbit.target_alpha -= 45f32.to_radians();
}
if key_input.just_pressed(KeyCode::Up) {
pan_orbit.target_beta += 45f32.to_radians();
}
if key_input.just_pressed(KeyCode::Down) {
pan_orbit.target_beta -= 45f32.to_radians();
}
}
// Pan using Left Shift + Arrows
else if key_input.pressed(KeyCode::LShift) {
let mut delta_translation = Vec3::ZERO;
if key_input.pressed(KeyCode::Right) {
delta_translation += transform.rotation * Vec3::X * time.delta_seconds();
}
if key_input.pressed(KeyCode::Left) {
delta_translation += transform.rotation * Vec3::NEG_X * time.delta_seconds();
}
if key_input.pressed(KeyCode::Up) {
delta_translation += transform.rotation * Vec3::Y * time.delta_seconds();
}
if key_input.pressed(KeyCode::Down) {
delta_translation += transform.rotation * Vec3::NEG_Y * time.delta_seconds();
}
transform.translation += delta_translation;
pan_orbit.focus += delta_translation;
}
// Smooth rotation using arrow keys without modifier
else {
if key_input.pressed(KeyCode::Right) {
pan_orbit.target_alpha += 50f32.to_radians() * time.delta_seconds();
}
if key_input.pressed(KeyCode::Left) {
pan_orbit.target_alpha -= 50f32.to_radians() * time.delta_seconds();
}
if key_input.pressed(KeyCode::Up) {
pan_orbit.target_beta += 50f32.to_radians() * time.delta_seconds();
}
if key_input.pressed(KeyCode::Down) {
pan_orbit.target_beta -= 50f32.to_radians() * time.delta_seconds();
}

// Zoom with Z and X
if key_input.pressed(KeyCode::Z) {
pan_orbit.radius -= 5.0 * time.delta_seconds();
}
if key_input.pressed(KeyCode::X) {
pan_orbit.radius += 5.0 * time.delta_seconds();
}
}

// Force camera to update its transform
pan_orbit.force_update = true;
}
}
116 changes: 67 additions & 49 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,30 +90,40 @@ pub struct PanOrbitCamera {
/// Rotation in radians around the global Y axis (longitudinal). Updated automatically.
/// If both `alpha` and `beta` are `0.0`, then the camera will be looking forward, i.e. in
/// the `Vec3::NEG_Z` direction, with up being `Vec3::Y`.
/// You should not update this manually - use `target_alpha` instead.
/// Defaults to `0.0`.
pub alpha: f32,
/// Rotation in radians around the local X axis (latitudinal). Updated automatically.
/// If both `alpha` and `beta` are `0.0`, then the camera will be looking forward, i.e. in
/// the `Vec3::NEG_Z` direction, with up being `Vec3::Y`.
/// You should not update this manually - use `target_beta` instead.
/// Defaults to `0.0`.
pub beta: f32,
/// The target alpha value. The camera will smoothly transition to this value. Used internally
/// and typically you won't set this manually.
/// The target alpha value. The camera will smoothly transition to this value. Updated
/// automatically, but you can also update it manually to control the camera independently of
/// the mouse controls, e.g. with the keyboard.
/// Defaults to `0.0`.
pub target_alpha: f32,
/// The target beta value. The camera will smoothly transition to this value. Used internally
/// and typically you won't set this manually.
/// The target beta value. The camera will smoothly transition to this value Updated
/// automatically, but you can also update it manually to control the camera independently of
/// the mouse controls, e.g. with the keyboard.
/// Defaults to `0.0`.
pub target_beta: f32,
/// Upper limit on the `alpha` value, in radians. Use this to restrict the maximum rotation
/// around the global Y axis.
/// Defaults to `None`.
pub alpha_upper_limit: Option<f32>,
/// Lower limit on the `alpha` value, in radians. Use this to restrict the maximum rotation
/// around the global Y axis.
/// Defaults to `None`.
pub alpha_lower_limit: Option<f32>,
/// Upper limit on the `beta` value, in radians. Use this to restrict the maximum rotation
/// around the local X axis.
/// Defaults to `None`.
pub beta_upper_limit: Option<f32>,
/// Lower limit on the `beta` value, in radians. Use this to restrict the maximum rotation
/// around the local X axis.
/// Defaults to `None`.
pub beta_lower_limit: Option<f32>,
/// The sensitivity of the orbiting motion. Defaults to `1.0`.
pub orbit_sensitivity: f32,
Expand Down Expand Up @@ -145,6 +155,11 @@ pub struct PanOrbitCamera {
/// Set to `true` if you want the camera to smoothly animate to its initial position.
/// Defaults to `false`.
pub initialized: bool,
/// Whether to update the camera's transform regardless of whether there are any changes/input.
/// Set this to `true` if you want to modify values directly.
/// This will be automatically set back to `false` after one frame.
/// Defaults to `false`.
pub force_update: bool,
}

impl Default for PanOrbitCamera {
Expand Down Expand Up @@ -173,6 +188,7 @@ impl Default for PanOrbitCamera {
alpha_lower_limit: None,
beta_upper_limit: None,
beta_lower_limit: None,
force_update: false,
}
}
}
Expand Down Expand Up @@ -327,6 +343,11 @@ fn pan_orbit_camera(
pan_orbit.beta = lower_beta;
}
}

if let Projection::Orthographic(ref mut p) = *projection {
p.scale = pan_orbit.radius;
}

update_orbit_transform(pan_orbit.alpha, pan_orbit.beta, &pan_orbit, &mut transform);
pan_orbit.target_alpha = pan_orbit.alpha;
pan_orbit.target_beta = pan_orbit.beta;
Expand Down Expand Up @@ -395,35 +416,6 @@ fn pan_orbit_camera(
pan_orbit.target_alpha -= delta_x;
pan_orbit.target_beta += delta_y;

if let Some(upper_alpha) = pan_orbit.alpha_upper_limit {
if pan_orbit.target_alpha > upper_alpha {
pan_orbit.target_alpha = upper_alpha;
}
}
if let Some(lower_alpha) = pan_orbit.alpha_lower_limit {
if pan_orbit.target_alpha < lower_alpha {
pan_orbit.target_alpha = lower_alpha;
}
}
if let Some(upper_beta) = pan_orbit.beta_upper_limit {
if pan_orbit.target_beta > upper_beta {
pan_orbit.target_beta = upper_beta;
}
}
if let Some(lower_beta) = pan_orbit.beta_lower_limit {
if pan_orbit.target_beta < lower_beta {
pan_orbit.target_beta = lower_beta;
}
}

if !pan_orbit.allow_upside_down {
if pan_orbit.target_beta < -PI / 2.0 {
pan_orbit.target_beta = -PI / 2.0;
}
if pan_orbit.target_beta > PI / 2.0 {
pan_orbit.target_beta = PI / 2.0;
}
}
has_moved = true;
}
} else if pan.length_squared() > 0.0 {
Expand All @@ -448,27 +440,49 @@ fn pan_orbit_camera(
has_moved = true;
}
} else if scroll.abs() > 0.0 {
match *projection {
Projection::Perspective(_) => {
pan_orbit.radius -= scroll * pan_orbit.radius * 0.2;
// Prevent zoom to zero otherwise we can get stuck there
pan_orbit.radius = f32::max(pan_orbit.radius, 0.05);
}
Projection::Orthographic(ref mut p) => {
p.scale -= scroll * p.scale * 0.2;
// Prevent zoom to zero otherwise we can get stuck there
p.scale = f32::max(p.scale, 0.05);
}
pan_orbit.radius -= scroll * pan_orbit.radius * 0.2;
// Prevent zoom to zero otherwise we can get stuck there
pan_orbit.radius = f32::max(pan_orbit.radius, 0.05);
if let Projection::Orthographic(ref mut p) = *projection {
p.scale = pan_orbit.radius;
}
has_moved = true;
}

// 3 - Apply orbit rotation based on target alpha/beta
// 3 - Apply rotation constraints

if has_moved
|| pan_orbit.target_alpha != pan_orbit.alpha
|| pan_orbit.target_beta != pan_orbit.beta
{
if let Some(upper_alpha) = pan_orbit.alpha_upper_limit {
if pan_orbit.target_alpha > upper_alpha {
pan_orbit.target_alpha = upper_alpha;
}
}
if let Some(lower_alpha) = pan_orbit.alpha_lower_limit {
if pan_orbit.target_alpha < lower_alpha {
pan_orbit.target_alpha = lower_alpha;
}
}
if let Some(upper_beta) = pan_orbit.beta_upper_limit {
if pan_orbit.target_beta > upper_beta {
pan_orbit.target_beta = upper_beta;
}
}
if let Some(lower_beta) = pan_orbit.beta_lower_limit {
if pan_orbit.target_beta < lower_beta {
pan_orbit.target_beta = lower_beta;
}
}
if !pan_orbit.allow_upside_down {
if pan_orbit.target_beta < -PI / 2.0 {
pan_orbit.target_beta = -PI / 2.0;
}
if pan_orbit.target_beta > PI / 2.0 {
pan_orbit.target_beta = PI / 2.0;
}
}

// 4 - Apply orbit rotation based on target alpha/beta

if has_moved || pan_orbit.force_update {
// Interpolate towards the target value
let t = 1.0 - pan_orbit.orbit_smoothness;
let mut target_alpha = pan_orbit.alpha.lerp(&pan_orbit.target_alpha, &t);
Expand All @@ -487,6 +501,10 @@ fn pan_orbit_camera(
// Update current alpha and beta values
pan_orbit.alpha = target_alpha;
pan_orbit.beta = target_beta;

if pan_orbit.force_update {
pan_orbit.force_update = false;
}
}
}
}
Expand Down