bevy_trackball/
camera.rs

1use std::collections::HashMap;
2
3use bevy::prelude::*;
4use trackball::{Clamp, Delta, Fixed, Frame, Scope, approx::AbsDiffEq, nalgebra::Point2};
5
6/// Trackball camera component mainly defined by [`Frame`] and [`Scope`].
7#[derive(Component, Debug)]
8pub struct TrackballCamera {
9	/// Camera frame defining [`Transform`].
10	///
11	/// Comprises following properties:
12	///
13	///   * target position as trackball center
14	///   * camera eye rotation on trackball surface (incl. roll, gimbal lock-free using quaternion)
15	///   * trackball radius
16	pub frame: Frame<f32>,
17	old_frame: Frame<f32>,
18	/// Camera scope defining [`Projection`].
19	///
20	/// Comprises following properties:
21	///
22	///   * field of view angle (default is 45 degrees) and its mode of either [`Fixed::Ver`]
23	///     (default), [`Fixed::Hor`], or [`Fixed::Upp`].
24	///   * projection mode of either perspective (default) or orthographic (scale preserving)
25	///   * clip planes either measured from eye (default) or target (object inspection mode)
26	pub scope: Scope<f32>,
27	old_scope: Scope<f32>,
28	old_max: Point2<f32>,
29	/// Blend half-life from 0 (fast) to 1000 (slow) milliseconds. Default is `40.0`.
30	///
31	/// It is the time passed until halfway of fps-agnostic exponential ease-out.
32	pub blend: f32,
33	/// Camera frame to reset to when [`TrackballInput::reset_key`] is pressed.
34	///
35	/// [`TrackballInput::reset_key`]: crate::TrackballInput::reset_key
36	pub reset: Frame<f32>,
37	/// User boundary conditions clamping camera [`Frame`].
38	///
39	/// Allows to limit target/eye position or minimal/maximal target/eye distance or up rotation.
40	pub clamp: Option<Box<dyn Clamp<f32>>>,
41	pub(crate) delta: Option<Delta<f32>>,
42	/// Additional [`TrackballController`] entities to which this camera is sensitive.
43	///
44	/// It is always sensitive to its own controller if it has one. A mapped value of `true` will
45	/// clamp the active controller as well and hence all other cameras of this group whenever this
46	/// camera is clamped. If `false`, only this camera is clamped whereas other cameras of this
47	/// group continue to follow the active controller.
48	///
49	/// [`TrackballController`]: crate::TrackballController
50	/// [`TrackballEvent`]: crate::TrackballEvent
51	pub group: HashMap<Entity, bool>,
52}
53
54impl TrackballCamera {
55	/// Defines camera with `target` position and `eye` position inclusive its roll attitude (`up`).
56	#[must_use]
57	pub fn look_at(target: Vec3, eye: Vec3, up: Vec3) -> Self {
58		let frame = Frame::look_at(target.into(), &eye.into(), &up.into());
59		Self {
60			frame,
61			old_frame: Frame::default(),
62			scope: Scope::default(),
63			old_scope: Scope::default(),
64			old_max: Point2::default(),
65			blend: 40.0,
66			reset: frame,
67			clamp: None,
68			delta: None,
69			group: HashMap::default(),
70		}
71	}
72	/// Defines scope, see [`Self::scope`].
73	#[must_use]
74	#[allow(clippy::type_complexity)]
75	pub const fn with_scope(mut self, scope: Scope<f32>) -> Self {
76		self.scope = scope;
77		self
78	}
79	/// Defines blend half-life, see [`Self::blend`].
80	#[must_use]
81	pub const fn with_blend(mut self, blend: f32) -> Self {
82		self.blend = blend;
83		self
84	}
85	/// Defines reset frame, see [`Self::reset`].
86	#[must_use]
87	#[allow(clippy::type_complexity)]
88	pub const fn with_reset(mut self, reset: Frame<f32>) -> Self {
89		self.reset = reset;
90		self
91	}
92	/// Defines user boundary conditions, see [`Self::clamp`].
93	#[must_use]
94	#[allow(clippy::type_complexity)]
95	pub fn with_clamp(mut self, clamp: impl Clamp<f32>) -> Self {
96		self.clamp = Some(Box::new(clamp));
97		self
98	}
99	/// Adds additional controller to which this camera is sensitive, see [`Self::group`].
100	#[must_use]
101	pub fn add_controller(mut self, id: Entity, rigid: bool) -> Self {
102		self.group.insert(id, rigid);
103		self
104	}
105}
106
107#[allow(clippy::needless_pass_by_value)]
108pub fn trackball_camera(
109	time: Res<Time>,
110	mut cameras: Query<(
111		&Camera,
112		&mut TrackballCamera,
113		&mut Transform,
114		&mut Projection,
115	)>,
116) {
117	for (camera, mut trackball, mut transform, mut projection) in &mut cameras {
118		let Some(max) = camera.logical_viewport_size().map(Point2::from) else {
119			continue;
120		};
121		#[allow(clippy::float_cmp)]
122		let new_zat = trackball.frame.distance() != trackball.old_frame.distance();
123		if trackball.frame != trackball.old_frame {
124			if trackball.old_frame == Frame::default() {
125				trackball.old_frame = trackball.frame;
126			}
127			let blend = (trackball.blend * 1e-3).clamp(0.0, 1.0);
128			let blend = 1.0 - 0.5f32.powf(time.delta_secs() / blend);
129			trackball.old_frame = trackball
130				.old_frame
131				.abs_diff_ne(&trackball.frame, f32::EPSILON.sqrt())
132				.then(|| {
133					trackball
134						.old_frame
135						.try_lerp_slerp(&trackball.frame, blend, 0.0)
136						.map(|mut frame| {
137							frame.renormalize();
138							frame
139						})
140				})
141				.flatten()
142				.unwrap_or(trackball.frame);
143			let view = trackball.old_frame.view();
144			transform.translation = view.translation.into();
145			transform.rotation = view.rotation.into();
146		}
147		let new_scope = trackball.scope != trackball.old_scope;
148		let new_max = max != trackball.old_max;
149		trackball.old_scope = trackball.scope;
150		trackball.old_max = max;
151		let fov = trackball.scope.fov();
152		let zat = trackball.old_frame.distance();
153		let (near, far) = trackball.scope.clip_planes(zat);
154		if trackball.scope.ortho() {
155			if new_scope || new_max || new_zat {
156				let (_max, upp) = fov.max_and_upp(zat, &max);
157				*projection = Projection::Orthographic(OrthographicProjection {
158					near,
159					far,
160					scale: upp,
161					..OrthographicProjection::default_3d()
162				});
163			}
164		} else if new_scope || (new_max && !matches!(fov, Fixed::Ver(_fov))) {
165			let fov = fov.to_ver(&max).into_inner();
166			let aspect_ratio = max.x / max.y;
167			*projection = Projection::Perspective(PerspectiveProjection {
168				fov,
169				aspect_ratio,
170				near,
171				far,
172			});
173		}
174	}
175}