Skip to content

Commit

Permalink
Add 'roll' controls and custom 'up' vector support (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
Plonq authored Feb 10, 2024
1 parent efa159d commit abb913b
Show file tree
Hide file tree
Showing 10 changed files with 360 additions and 108 deletions.
60 changes: 40 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,37 @@
[![docs.rs](https://docs.rs/bevy_panorbit_camera/badge.svg)](https://docs.rs/bevy_panorbit_camera)
[![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking)

<div align="center">
<div style="text-align: center">
<h1>Bevy Pan/Orbit Camera</h1>
</div>

![A screen recording showing camera movement](https://user-images.githubusercontent.com/7709415/230715348-eb19d9a8-4826-4a73-a039-02cacdcb3dc9.gif "Demo of bevy_panorbit_camera")

## What Is This?
## Summary

Bevy Pan/Orbit Camera provides orbit camera controls for Bevy Engine, designed with simplicity and flexibility in mind.
Use it to quickly prototype, experiment, for model viewers, and more!

Default controls:
## Features:

- Smoothed orbiting, panning, and zooming
- Works with orthographic camera projection in addition to perspective
- Customisable controls, sensitivity, and more
- Touch support
- Currently 'beta' - please report any issues!
- Works with multiple viewports and/or windows
- Easy to control manually, e.g. for keyboard control or animation
- Can control cameras that render to a texture
- Supports the 'roll' axis and changing the 'up' vector, and thus controlling all 3 rotational axes
- Comes with caveats as explained in the documentation for `PanOrbitCamera.key_roll_left` /
`PanOrbitCamera.key_roll_right` and `PanOrbitCamera.base_transform`

## Controls

By default, you can only orbit, pan, and zoom. Optionally, you can enable the 'roll' axis, which modifies the 'up'
vector of the camera. You can also set the 'up' vector manually - see `PanOrbitCamera.base_transform`.

Default mouse controls:

- Left Mouse - Orbit
- Right Mouse - Pan
Expand All @@ -24,18 +43,10 @@ Touch controls:
- One finger - Orbit
- Two fingers - Pan
- Pinch - Zoom
- Two finger rotate - Roll (disabled by default)

## Features:

- Orbiting, panning and zooming
- Smooth motion
- Works with orthographic camera projection in addition to perspective
- Customisable controls, sensitivity, and more
- Touch support
- Currently 'beta' - please report any issues!
- Works with multiple viewports and/or windows
- Easy to control manually, e.g. for keyboard control or animation
- Can control cameras that render to a texture
Note: touch controls are currently not customisable. Please create an issue if you would like to customise the touch
controls.

## Quick Start

Expand Down Expand Up @@ -64,14 +75,23 @@ all the possible configuration options.

## What are `alpha` and `beta`?

Think of this camera as rotating around a point, and always pointing at that point (the `focus`). The sideways rotation,
i.e. the longitudinal rotation, is `alpha`, and the latitudinal rotation is `beta`. Both are measured in radians.
If `alpha` and `beta` are both `0.0`, then the camera will be looking directly forwards (-Z direction). Increasing
`alpha` will rotate around the `focus` to the right, and increasing `beta` will move the camera up and over the `focus`.
Typically you don't need to worry about the inner workings of this plugin - the defaults work well and are suitable for
most use cases. However, if you want to customise the behaviour, for example restricting the camera movement or
adjusting sensitivity, you probably want to know what the `alpha` and `beta` values represent.

While not strictly accurate, you can think of `alpha` as yaw and `beta` as tilt. More accurately, `alpha` represents the
angle around the _global_ Y axis, and `beta` represents the angle around the _local_ X axis (i.e. the X axis after Y
axis rotation has been applied). When both `alpha` and `beta` are `0.0`, the camera is pointing directly forward (-Z).
Thus, increasing `alpha` orbits around to the right (counter clockwise if looking from above), and increasing `beta`
orbits up and over (e.g. a `beta` value of 90 degrees (`PI / 2.0`) results in the camera looking straight down).

Note that if you change the up vector either by enabling roll controls or changing `PanOrbitCamera.base_transform`,
the concept of 'up' and 'down' change, and so the above explanation changes accordingly.

## Cargo Features

- `bevy_egui`: makes PanOrbitCamera ignore input when interacting with egui widgets/windows
- `bevy_egui` (optional): makes `PanOrbitCamera` ignore any input that `egui` uses, thus preventing moving the camera
when interacting with egui windows

## Version Compatibility

Expand All @@ -85,7 +105,7 @@ If `alpha` and `beta` are both `0.0`, then the camera will be looking directly f

- [Bevy Cheat Book](https://bevy-cheatbook.github.io): For providing an example that I started from
- [babylon.js](https://www.babylonjs.com): I referenced their arc rotate camera for some of this
- [bevy_pancam](https://github.com/johanhelsing/bevy_pancam): For the egui-related code
- [bevy_pancam](https://github.com/johanhelsing/bevy_pancam): For the egui feature idea

## License

Expand Down
15 changes: 14 additions & 1 deletion examples/advanced.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
//! Demonstrates all common configuration options,
//! and how to modify them at runtime
//!
//! Controls:
//! Orbit: Middle click
//! Pan: Shift + Middle click
//! Zoom: Mousewheel
//! Roll: A (roll left) and D (roll right)
use bevy::prelude::*;
use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin};
Expand Down Expand Up @@ -73,12 +79,19 @@ fn setup(
modifier_pan: Some(KeyCode::ShiftLeft),
// Reverse the zoom direction
reversed_zoom: true,
// Enable roll in addition to orbit.
// Note: when enabling roll you probably also want to set `allow_upside_down` to `true`
// because upside down loses its meaning when you can roll freely.
key_roll_left: Some(KeyCode::A),
key_roll_right: Some(KeyCode::D),
roll_sensitivity: 0.5,
..default()
},
));
}

// Press 'T' to toggle the camera controls
// This is how you can change config at runtime.
// Press 'T' to toggle the camera controls.
fn toggle_camera_controls_system(
key_input: Res<Input<KeyCode>>,
mut pan_orbit_query: Query<&mut PanOrbitCamera>,
Expand Down
63 changes: 63 additions & 0 deletions examples/alternate_up_vector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! Demonstrates how to change the 'up' vector of the camera
use bevy::prelude::*;
use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin};

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(PanOrbitCameraPlugin)
.add_systems(Startup, setup)
.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 {
transform: Transform::from_translation(Vec3::new(0.0, 1.5, 5.0)),
..default()
},
PanOrbitCamera {
base_transform: Transform::from_rotation(Quat::from_rotation_arc(
Vec3::NEG_Y,
// This is the new 'up' vector, and it must be normalised before passing to
// `from_rotation_arc`. Alternatively you can set `base_transform` any way you want,
// e.g. from a rotation or an existing transform.
Vec3::new(0.2, -1.2, 0.2).normalize(),
)),
// When changing the 'up' vector, you probably also want to allow upside down, because
// 'upside down' is based upon the world 'up' vector (via alpha/beta values), and so
// doesn't make any sense when the up vector is some arbitrary direction.
allow_upside_down: true,
..default()
},
));
}
2 changes: 1 addition & 1 deletion examples/orthographic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ fn setup(
..default()
},
PanOrbitCamera {
// Setting scale here will override the camera projection's initial scale
// Setting scale here will override the camera projection's initial scale
scale: Some(2.5),
..default()
},
Expand Down
3 changes: 3 additions & 0 deletions examples/render_to_texture.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//! Demonstrates the ability to manually override which instance of PanOrbitCamera receives input
//! events, which is necessary when rendering to a texture/image instead of a window/viewport.
//!
//! In this example, input controls the camera that is rendering the texture applied to the cube,
//! rather than the main window camera.
//!
//! This example is based off Bevy's render_to_texture example.
use std::f32::consts::PI;
Expand Down
8 changes: 7 additions & 1 deletion examples/touch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@ fn setup_scene(
transform: Transform::from_translation(Vec3::new(0.0, 1.5, 5.0)),
..default()
},
PanOrbitCamera::default(),
PanOrbitCamera {
// Enable roll for demonstration purposes. Typically you wouldn't want to enable
// this. See documentation for `key_roll_left` / `key_roll_right` and `base_transform`
// for more information.
touch_roll_enabled: true,
..default()
},
));
}

Expand Down
52 changes: 49 additions & 3 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{ActiveCameraData, PanOrbitCamera};
use bevy::input::mouse::{MouseMotion, MouseScrollUnit, MouseWheel};
use bevy::input::touch::Touch;
use bevy::prelude::*;
use std::f32::consts::TAU;

use crate::traits::Midpoint;

Expand All @@ -11,6 +12,7 @@ pub struct MouseKeyTracker {
pub pan: Vec2,
pub scroll_line: f32,
pub scroll_pixel: f32,
pub roll_angle: f32,
pub orbit_button_changed: bool,
}

Expand All @@ -31,15 +33,18 @@ pub fn mouse_key_tracker(
let mut pan = Vec2::ZERO;
let mut scroll_line = 0.0;
let mut scroll_pixel = 0.0;
let mut roll_angle = 0.0;
let mut orbit_button_changed = false;

// Orbit and pan
if orbit_pressed(pan_orbit, &mouse_input, &key_input) {
orbit += mouse_delta;
} else if pan_pressed(pan_orbit, &mouse_input, &key_input) {
// Pan only if we're not rotating at the moment
pan += mouse_delta;
}

// Zoom
for ev in scroll_events.read() {
let delta_scroll = ev.y;
match ev.unit {
Expand All @@ -52,6 +57,21 @@ pub fn mouse_key_tracker(
};
}

// Roll
let roll_amount = TAU * 0.003;

if let Some(roll_left_key) = pan_orbit.key_roll_left {
if key_input.pressed(roll_left_key) {
roll_angle -= roll_amount;
}
}
if let Some(roll_right_key) = pan_orbit.key_roll_right {
if key_input.pressed(roll_right_key) {
roll_angle += roll_amount;
}
}

// Other
if orbit_just_pressed(pan_orbit, &mouse_input, &key_input)
|| orbit_just_released(pan_orbit, &mouse_input, &key_input)
{
Expand All @@ -62,6 +82,7 @@ pub fn mouse_key_tracker(
camera_movement.pan = pan;
camera_movement.scroll_line = scroll_line;
camera_movement.scroll_pixel = scroll_pixel;
camera_movement.roll_angle = roll_angle;
camera_movement.orbit_button_changed = orbit_button_changed;
}
}
Expand All @@ -76,13 +97,15 @@ pub struct TouchTracker {

impl TouchTracker {
/// Return orbit, pan, and zoom values based on touch data
pub fn calculate_movement(&self) -> (Vec2, Vec2, f32) {
pub fn calculate_movement(&self) -> (Vec2, Vec2, f32, f32) {
let mut orbit = Vec2::ZERO;
let mut pan = Vec2::ZERO;
let mut roll_angle = 0.0;
let mut zoom_pixel = 0.0;

// Only match when curr and prev have same number of touches, for simplicity.
// I did not notice any adverse behaviour as a result.
// I did not notice any adverse behaviour as a result of ignoring the single frame
// where the number of touches changes (e.g. from 1 to 2 or from 2 to 1).
match (self.curr_pressed, self.prev_pressed) {
((Some(curr), None), (Some(prev), None)) => {
let curr_pos = curr.position();
Expand All @@ -96,18 +119,41 @@ impl TouchTracker {
let prev1_pos = prev1.position();
let prev2_pos = prev2.position();

// Pan
let curr_midpoint = curr1_pos.midpoint(curr2_pos);
let prev_midpoint = prev1_pos.midpoint(prev2_pos);
pan += curr_midpoint - prev_midpoint;

// Zoom
let curr_dist = curr1_pos.distance(curr2_pos);
let prev_dist = prev1_pos.distance(prev2_pos);
zoom_pixel += (curr_dist - prev_dist) * 0.015;

// Roll
let prev_vec = prev2_pos - prev1_pos;
let curr_vec = curr2_pos - curr1_pos;
let prev_angle_negy = prev_vec.angle_between(Vec2::NEG_Y);
let curr_angle_negy = curr_vec.angle_between(Vec2::NEG_Y);
let prev_angle_posy = prev_vec.angle_between(Vec2::Y);
let curr_angle_posy = curr_vec.angle_between(Vec2::Y);
let roll_angle_negy = prev_angle_negy - curr_angle_negy;
let roll_angle_posy = prev_angle_posy - curr_angle_posy;
// The angle between -1deg and +1deg is 358deg according to Vec2::angle_between,
// but we want the answer to be +2deg (or -2deg if swapped). Therefore, we calculate
// two angles - one from UP and one from DOWN, and use the smallest absolute value
// of the two. This is necessary to get a predictable result when the two touches
// swap sides (change from one being on the left and one being on the right to the
// other way round).
if roll_angle_negy.abs() < roll_angle_posy.abs() {
roll_angle = roll_angle_negy;
} else {
roll_angle = roll_angle_posy;
}
}
_ => {}
}

(orbit, pan, zoom_pixel)
(orbit, pan, roll_angle, zoom_pixel)
}
}

Expand Down
Loading

0 comments on commit abb913b

Please sign in to comment.