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}