Skip to content

Commit

Permalink
Add ability to control camera manually (e.g. with keyboard) (#11)
Browse files Browse the repository at this point in the history
* Apply rotation constraints always, not just if there's input

* Add force_update option

* Update animate example to use force_update

* Add keyboard controls example

* Base orthographic scale off radius
  • Loading branch information
Plonq authored Apr 29, 2023
1 parent afed64b commit 60d0acf
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 52 deletions.
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

0 comments on commit 60d0acf

Please sign in to comment.