bevy_trackball/
lib.rs

1//! Coherent virtual trackball controller/camera plugin for Bevy
2//!
3//! Run interactive [examples] in your browser using [WebAssembly] and [WebGL].
4//!
5//! [WebAssembly]: https://en.wikipedia.org/wiki/WebAssembly
6//! [WebGL]: https://en.wikipedia.org/wiki/WebGL
7//!
8//! **NOTE**: Not all features are enabled by default, see [Optional Features](#optional-features).
9//! On Linux the `bevy/wayland` or `bevy/x11` feature gate must be enabled for a successful build.
10//!
11//! # Camera Modes
12//!
13//! Supports multiple camera modes:
14//!
15//!   * Trackball mode rotates camera around target.
16//!   * First-person mode rotates target around camera.
17//!   * Spectator mode translates target and camera.
18//!
19//! # Coherence Features
20//!
21//! This is an alternative trackball technique using exponential map and parallel transport to
22//! preserve distances and angles for inducing coherent and intuitive trackball rotations. For
23//! instance, displacements on straight radial lines through the screen's center are carried to arcs
24//! of the same length on great circles of the trackball (e.g., dragging the mouse along an eights
25//! of the trackball's circumference rolls the camera by 360/8=45 degrees, dragging the mouse from
26//! the screen's center to its further edge *linearly* rotates the camera by 1 [radian], where the
27//! trackball's diameter is the maximum of the screen's width and height). This is in contrast to
28//! state-of-the-art techniques using orthogonal projection which distorts radial distances further
29//! away from the screen's center (e.g., the rotation accelerates towards the edge).
30//!
31//! [radian]: https://en.wikipedia.org/wiki/Radian
32//!
33//!   * Coherent and intuitive orbiting via the exponential map, see the underlying [`trackball`]
34//!     crate which follows the recipe given in the paper of Stantchev, G.. “Virtual Trackball
35//!     Modeling and the Exponential Map.”. [S2CID] [44199608]. See the [`exponential_map`] example.
36//!   * Coherent first-person mode aka free look or mouse look with the world trackball centered at
37//!     eye instead of target.
38//!   * Coherent scaling by translating mouse wheel device units, see [`TrackballWheelUnit`]. Scales
39//!     eye distance from current cursor position or centroid of finger positions projected onto
40//!     focus plane.
41//!   * Coherent linear/angular [`TrackballVelocity`] for sliding/orbiting or free look by
42//!     time-based input (e.g., pressed key). By default, the linear velocity is deduced from the
43//!     angular velocity (where target and eye positions define the world radius) which in turn is
44//!     defined in units of vertical field of view per seconds and hence independent of the world
45//!     unit scale.
46//!
47//! [S2CID]: https://en.wikipedia.org/wiki/S2CID_(identifier)
48//! [44199608]: https://api.semanticscholar.org/CorpusID:44199608
49//!
50//! # Additional Features
51//!
52//!   * Time-free multi-touch gesture recognition for orbit, scale, slide, and focus (i.e., slide to
53//!     cursor/finger position) operations.
54//!   * Smoothing of movement implemented as fps-agnostic exponential ease-out.
55//!   * Gimbal lock-free using quaternion instead of Euler angles.
56//!   * Gliding clamp (experimental): The movement of a camera can be restricted to user-defined
57//!     boundary conditions (e.g., to not orbit below the ground plane). When the movement is not
58//!     orthogonal to a boundary plane, it is changed such that the camera glides along the boundary
59//!     plane. Currently, only implemented for orbit and slide operations, see the [`gliding_clamp`]
60//!     example.
61//!   * Camera constellation: A camera is decoupled from its input controller and instead multiple
62//!     cameras can be sensitive to zero or multiple selected controllers (e.g., a minimap
63//!     controlled by the same controller of the main viewport).
64//!   * Constellation clamp: Cameras sensitive to the same controller are referred to as a group
65//!     and can be configured to clamp the movement for the whole group whenever a group member
66//!     crosses a boundary condition (e.g., rigid and loose constellation clamp), see the
67//!     [`constellation_clamp`] example.
68//!   * Viewport stealing: This allows UI system (e.g., egui behind `bevy_egui` feature gate) to
69//!     steal the viewport and hence capture the input instead, see the [`egui`] example.
70//!   * Scale-preserving transitioning between orthographic and perspective projection mode.
71//!   * Converting between scaling modes (i.e., fixed vertical or horizontal field of view or fixed
72//!     unit per pixels). This defines whether the scene scales or the corresponding vertical or
73//!     horizontal field of view adjusts whenever the height or width of the viewport is resized,
74//!     see the [`scaling_modes`] example.
75//!   * Object inspection mode scaling clip plane distances by measuring from target instead of eye.
76//!     This benefits the precision of the depth map. Applicable, whenever the extend of the object
77//!     to inspect is known and hence the near clip plane can safely be placed just in front of it.
78//!   * `f64`-ready for large worlds (e.g., solar system scale) whenever Bevy is, see issue [#1680].
79//!
80//! [#1680]: https://github.com/bevyengine/bevy/issues/1680
81//!
82//! # Optional Features
83//!
84//! Following features are disabled unless their corresponding feature gate is enabled:
85//!
86//!   * `bevy_egui` for automatic viewport stealing whenever `egui` wants focus.
87//!   * `serialize` for `serde` support of various structures of this crate and its dependencies.
88//!   * `c11-orbit` for testing the behaviorally identical C implementation of the exponential map.
89//!
90//! # Roadmap
91//!
92//!   * Implement gliding clamp for first-person mode and scale operation, see
93//!     [issue](https://github.com/qu1x/bevy_trackball/issues/5).
94//!   * Support more camera modes out of the box by adding dedicated controllers for each mode, see
95//!     [issue](https://github.com/qu1x/bevy_trackball/issues/3).
96//!   * Support gamepad inputs, see [issue](https://github.com/qu1x/bevy_trackball/issues/4).
97//!
98//! # Input Mappings
99//!
100//! Following mappings are the defaults which can be customized, see [`TrackballInput`].
101//!
102//! Mouse (Buttons)         | Touch (Fingers)         | Keyboard | Operation
103//! ----------------------- | ----------------------- | -------- | ---------------------------------
104//! Left Press + Drag       | One + Drag              | `ijkl`   | Orbits around target.
105//! ↳ at trackball's border | Two + Roll              | `uo`     | Rolls about view direction.
106//! Middle Press + Drag     | Any + Drag + Left Shift | `↑←↓→`   | First-person mode.
107//! Right Press + Drag      | Two + Drag              | `esdf`   | Slides trackball on focus plane.
108//!                    |                    | `gv`     | Slides trackball in/out.
109//! Scroll In/Out           | Two + Pinch Out/In      | `hn`     | Scales distance zooming in/out.
110//! Left Press + Release    | Any + Release           |     | Slides to cursor/finger position.
111//!                    |                    | `m`      | Toggle `esdf`/`wasd` mapping.
112//!                    |                    | `p`      | Toggle orthographic/perspective.
113//!                    |                    | `Enter`  | Reset camera transform.
114//!
115//! Alternatively, [`TrackballInput::map_wasd`] maps `wasd`/`Space`/`ControlLeft` to slide
116//! operations where `ws` slides in/out and `Space`/`ControlLeft` slides up/down (jump/crouch).
117//!
118//! # Usage
119//!
120//! Add the [`TrackballPlugin`] followed by spawning a [`TrackballController`] together with a
121//! [`TrackballCamera`] and a `Camera3dBundle` or try the interactive [examples].
122//!
123//! ```no_run
124//! use bevy::prelude::*;
125//! use bevy_trackball::prelude::*;
126//!
127//! // Add the trackball plugin.
128//! fn main() {
129//! 	App::new()
130//! 		.add_plugins(DefaultPlugins)
131//! 		.add_plugins(TrackballPlugin)
132//! 		.add_systems(Startup, setup)
133//! 		.run();
134//! }
135//!
136//! // Add a trackball controller and trackball camera to a camera 3D bundle.
137//! fn setup(mut commands: Commands) {
138//! 	let [target, eye, up] = [Vec3::ZERO, Vec3::Z * 10.0, Vec3::Y];
139//! 	commands.spawn((
140//! 		TrackballController::default(),
141//! 		TrackballCamera::look_at(target, eye, up),
142//! 		Camera3d::default(),
143//! 	));
144//!
145//! 	// Set up your scene...
146//! }
147//! ```
148//!
149//! [examples]: https://qu1x.dev/bevy_trackball
150//! [`exponential_map`]: https://qu1x.dev/bevy_trackball/exponential_map.html
151//! [`gliding_clamp`]: https://qu1x.dev/bevy_trackball/gliding_clamp.html
152//! [`constellation_clamp`]: https://qu1x.dev/bevy_trackball/constellation_clamp.html
153//! [`egui`]: https://qu1x.dev/bevy_trackball/egui.html
154//! [`scaling_modes`]: https://github.com/qu1x/bevy_trackball/blob/main/examples/scaling_modes.rs
155
156use bevy::prelude::*;
157pub use camera::TrackballCamera;
158use camera::trackball_camera;
159use constellation::trackball_constellation;
160use controller::trackball_controller;
161pub use controller::{
162	TrackballController, TrackballInput, TrackballVelocity, TrackballViewport, TrackballWheelUnit,
163};
164pub use trackball;
165use trackball::{
166	Delta,
167	nalgebra::{Point3, Unit, UnitQuaternion, Vector3},
168};
169
170/// Prelude to get started quickly.
171pub mod prelude {
172	pub use super::{
173		TrackballCamera, TrackballController, TrackballEvent, TrackballInput, TrackballPlugin,
174		TrackballSetup, TrackballSystemSet, TrackballVelocity, TrackballViewport,
175		TrackballWheelUnit,
176		trackball::{
177			Bound, Clamp, Delta, Fixed, Frame, Plane, Scope,
178			approx::{
179				AbsDiffEq, RelativeEq, UlpsEq, abs_diff_eq, abs_diff_ne, assert_abs_diff_eq,
180				assert_abs_diff_ne, assert_relative_eq, assert_relative_ne, assert_ulps_eq,
181				assert_ulps_ne, relative_eq, relative_ne, ulps_eq, ulps_ne,
182			},
183			nalgebra::{Isometry3, Point3, Unit, UnitQuaternion, Vector3},
184		},
185	};
186}
187mod camera;
188mod constellation;
189mod controller;
190
191/// Plugin adding and configuring systems and their resources.
192///
193/// Halts [`TrackballSystemSet::Controller`] for supported UI systems (i.e., `bevy_egui` feature
194/// gate) whenever they request focus by marking the active viewport as stolen.
195///
196/// See [`TrackballViewport::set_stolen`] in order to steal the viewport and hence exclusively
197/// consume its input events for UI systems that are not yet supported behind feature gate.
198#[derive(Default)]
199pub struct TrackballPlugin;
200
201/// Event sent from [`TrackballController`] component to group of [`TrackballCamera`] components.
202#[derive(Event)]
203pub struct TrackballEvent {
204	/// Entity of [`TrackballController`] component which sent this event.
205	///
206	/// Read by group of [`TrackballCamera`] components which knows about this entity.
207	pub group: Entity,
208	/// Delta transform from initial to final [`Frame`] of [`TrackballCamera`].
209	///
210	/// [`Frame`]: trackball::Frame
211	pub delta: Delta<f32>,
212	/// Setup of [`TrackballCamera`].
213	pub setup: Option<TrackballSetup>,
214}
215
216impl TrackballEvent {
217	/// Creates [`Delta::First`] event for camera `group`.
218	#[must_use]
219	#[inline]
220	pub const fn first(group: Entity, pitch: f32, yaw: f32, yaw_axis: Unit<Vector3<f32>>) -> Self {
221		Self {
222			group,
223			delta: Delta::First {
224				pitch,
225				yaw,
226				yaw_axis,
227			},
228			setup: None,
229		}
230	}
231	/// Creates [`Delta::Track`] event for camera `group`.
232	#[must_use]
233	#[inline]
234	pub const fn track(group: Entity, vec: Vector3<f32>) -> Self {
235		Self {
236			group,
237			delta: Delta::Track { vec },
238			setup: None,
239		}
240	}
241	/// Creates [`Delta::Orbit`] event for camera `group`.
242	#[must_use]
243	#[inline]
244	pub const fn orbit(group: Entity, rot: UnitQuaternion<f32>, pos: Point3<f32>) -> Self {
245		Self {
246			group,
247			delta: Delta::Orbit { rot, pos },
248			setup: None,
249		}
250	}
251	/// Creates [`Delta::Slide`] event for camera `group`.
252	#[must_use]
253	#[inline]
254	pub const fn slide(group: Entity, vec: Vector3<f32>) -> Self {
255		Self {
256			group,
257			delta: Delta::Slide { vec },
258			setup: None,
259		}
260	}
261	/// Creates [`Delta::Scale`] event for camera `group`.
262	#[must_use]
263	#[inline]
264	pub const fn scale(group: Entity, rat: f32, pos: Point3<f32>) -> Self {
265		Self {
266			group,
267			delta: Delta::Scale { rat, pos },
268			setup: None,
269		}
270	}
271	/// Creates [`TrackballSetup::Reset`] event for camera `group`.
272	#[must_use]
273	#[inline]
274	pub const fn reset(group: Entity) -> Self {
275		Self {
276			group,
277			delta: Delta::Frame,
278			setup: Some(TrackballSetup::Reset),
279		}
280	}
281	/// Creates [`TrackballSetup::Ortho`] event for camera `group`.
282	#[must_use]
283	#[inline]
284	pub const fn ortho(group: Entity, ortho: Option<bool>) -> Self {
285		Self {
286			group,
287			delta: Delta::Frame,
288			setup: Some(TrackballSetup::Ortho(ortho)),
289		}
290	}
291
292	/// Applies `transmission` ratio.
293	#[must_use]
294	#[inline]
295	pub fn transmission(mut self, transmission: f32) -> Self {
296		self.delta = self.delta.lerp_slerp(transmission);
297		self
298	}
299}
300
301/// Setup of [`TrackballCamera`] as part of [`TrackballEvent`].
302#[derive(Debug, PartialEq, Eq, Clone, Copy)]
303#[non_exhaustive]
304pub enum TrackballSetup {
305	/// Reset camera frame.
306	Reset,
307	/// Set projection mode.
308	///
309	///   * Orthographic with `Some(true)`
310	///   * Perspective with `Some(false)`
311	///   * Toggle with `None`
312	Ortho(Option<bool>),
313}
314
315/// System sets configured by [`TrackballPlugin`].
316#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
317#[non_exhaustive]
318pub enum TrackballSystemSet {
319	/// Trackball controller system translating [`TrackballInput`] of [`TrackballController`]
320	/// components into [`TrackballEvent`] for [`TrackballSystemSet::Constellation`].
321	Controller,
322	/// Trackball constellation system translating [`TrackballEvent`] from [`TrackballController`]
323	/// components into new [`Frame`] and [`Scope`] of [`TrackballCamera`] components.
324	///
325	/// [`Frame`]: trackball::Frame
326	/// [`Scope`]: trackball::Scope
327	Constellation,
328	/// Trackball camera system translating [`Frame`] and [`Scope`] of [`TrackballCamera`]
329	/// components into [`Transform`] and [`Projection`] bundles (e.g., `Camera3DBundle`).
330	///
331	/// [`Frame`]: trackball::Frame
332	/// [`Scope`]: trackball::Scope
333	Camera,
334}
335
336impl Plugin for TrackballPlugin {
337	fn build(&self, app: &mut App) {
338		app.init_resource::<TrackballViewport>()
339			.add_event::<TrackballEvent>()
340			.add_systems(
341				Update,
342				(
343					trackball_controller
344						.in_set(TrackballSystemSet::Controller)
345						.run_if(not(TrackballViewport::stolen)),
346					trackball_constellation.in_set(TrackballSystemSet::Constellation),
347					trackball_camera.in_set(TrackballSystemSet::Camera),
348				)
349					.chain(),
350			);
351		#[cfg(feature = "bevy_egui")]
352		{
353			use bevy_egui::{EguiContext, EguiPreUpdateSet};
354
355			fn egui_viewport_theft(
356				mut viewport: ResMut<TrackballViewport>,
357				mut contexts: Query<&mut EguiContext>,
358			) {
359				let stolen = contexts.iter_mut().next().is_some_and(|mut context| {
360					let context = context.get_mut();
361					context.wants_pointer_input() || context.wants_keyboard_input()
362				});
363				viewport.set_stolen(stolen.then_some(2));
364			}
365
366			app.add_systems(
367				Update,
368				egui_viewport_theft
369					.after(EguiPreUpdateSet::InitContexts)
370					.before(TrackballSystemSet::Controller),
371			);
372		}
373	}
374}