1//! A camera controller that allows the user to move freely around the scene.
2//!
3//! Free cameras are helpful for exploring large scenes, level editors and for debugging.
4//! They are rarely useful as-is for gameplay,
5//! as they allow the user to move freely in all directions,
6//! which can be disorienting, and they can clip through objects and terrain.
7//!
8//! You may have heard of a "fly camera" — a type of free camera designed for fluid "flying" movement and quickly surveying large areas.
9//! By contrast, the default settings of this particular free camera are optimized for precise control.
10//!
11//! To use this controller, add [`FreeCameraPlugin`] to your app,
12//! and attach the [`FreeCamera`] component to your camera entity.
13//! The required [`FreeCameraState`] component will be added automatically.
14//!
15//! To configure the settings of this controller, modify the fields of the [`FreeCamera`] component.
16// TODO: Discuss switching camera to orthographic mode.
1718use bevy_app::{App, Plugin, RunFixedMainLoop, RunFixedMainLoopSystems};
19use bevy_camera::Camera;
20use bevy_curve::{Curve, Interval, SampleAutoCurve};
21use bevy_ecs::prelude::*;
22use bevy_input::keyboard::KeyCode;
23use bevy_input::mouse::{
24AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButton, MouseScrollPixelsPerLine,
25};
26use bevy_input::touch::Touches;
27use bevy_input::ButtonInput;
28use bevy_log::info;
29use bevy_math::{ops::exp, Dir3, EulerRot, Quat, StableInterpolate, Vec2, Vec3};
30use bevy_time::{Real, Time};
31use bevy_transform::prelude::Transform;
32use bevy_window::{CursorGrabMode, CursorOptions, Window};
3334use core::{f32::consts::*, fmt};
3536/// A freecam-style camera controller plugin.
37///
38/// Use the [`FreeCamera`] struct to add and customize the controller for a camera entity.
39/// The camera's dynamic state is managed by the [`FreeCameraState`] struct.
40pub struct FreeCameraPlugin;
4142impl Pluginfor FreeCameraPlugin {
43fn build(&self, app: &mut App) {
44// This ordering is required so that both fixed update and update systems can see the results correctly
45app.add_systems(
46RunFixedMainLoop,
47 (run_freecamera_controller, rotate_freecam_to)
48 .chain()
49 .in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop),
50 );
51 }
52}
5354/// Scales mouse motion into yaw/pitch movement.
55///
56/// Based on Valorant's default sensitivity, not entirely sure why it is exactly 1.0 / 180.0,
57/// but we're guessing it is a misunderstanding between degrees/radians and then sticking with
58/// it because it felt nice.
59const RADIANS_PER_DOT: f32 = 1.0 / 180.0;
6061/// Stores the settings for the [`FreeCamera`] controller.
62///
63/// This component defines static configuration for camera controls,
64/// including movement speed, sensitivity, and input bindings.
65///
66/// From the controller’s perspective, this data is treated as immutable,
67/// but it may be modified externally (e.g., by a settings UI) at runtime.
68///
69/// Add this component to a [`Camera`] entity to enable `FreeCamera` controls.
70/// The associated dynamic state is automatically handled by [`FreeCameraState`],
71/// which is added to the entity as a required component.
72///
73/// To activate the controller, add the [`FreeCameraPlugin`] to your [`App`].
74#[derive(#[doc =
"**Required Components**: [`FreeCameraState`]. \n\n A component's Required Components are inserted whenever it is inserted. Note that this will also insert the required components _of_ the required components, recursively, in depth-first order."]
impl bevy_ecs::component::Component for FreeCamera where
Self: ::core::marker::Send + ::core::marker::Sync + 'static {
const STORAGE_TYPE: bevy_ecs::component::StorageType =
bevy_ecs::component::StorageType::Table;
type Mutability = bevy_ecs::component::Mutable;
fn register_required_components(_requiree:
bevy_ecs::component::ComponentId,
required_components:
&mut bevy_ecs::component::RequiredComponentsRegistrator) {
required_components.register_required::<FreeCameraState>(<FreeCameraState
as ::core::default::Default>::default);
}
fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
use bevy_ecs::component::{
DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
};
(&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
}
fn relationship_accessor()
->
::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
::core::option::Option::None
}
}Component, #[automatically_derived]impl ::core::clone::Clone for FreeCamera {
#[inline]
fn clone(&self) -> Self {
FreeCamera {
sensitivity: ::core::clone::Clone::clone(&self.sensitivity),
key_forward: ::core::clone::Clone::clone(&self.key_forward),
key_back: ::core::clone::Clone::clone(&self.key_back),
key_left: ::core::clone::Clone::clone(&self.key_left),
key_right: ::core::clone::Clone::clone(&self.key_right),
key_up: ::core::clone::Clone::clone(&self.key_up),
key_down: ::core::clone::Clone::clone(&self.key_down),
key_run: ::core::clone::Clone::clone(&self.key_run),
mouse_key_cursor_grab: ::core::clone::Clone::clone(&self.mouse_key_cursor_grab),
keyboard_key_toggle_cursor_grab: ::core::clone::Clone::clone(&self.keyboard_key_toggle_cursor_grab),
key_snap_reverse: ::core::clone::Clone::clone(&self.key_snap_reverse),
axis_top: ::core::clone::Clone::clone(&self.axis_top),
axis_right: ::core::clone::Clone::clone(&self.axis_right),
axis_front: ::core::clone::Clone::clone(&self.axis_front),
walk_speed: ::core::clone::Clone::clone(&self.walk_speed),
run_speed: ::core::clone::Clone::clone(&self.run_speed),
scroll_factor: ::core::clone::Clone::clone(&self.scroll_factor),
friction: ::core::clone::Clone::clone(&self.friction),
rotation_speed: ::core::clone::Clone::clone(&self.rotation_speed),
vertical_movement_axis: ::core::clone::Clone::clone(&self.vertical_movement_axis),
}
}
}Clone)]
75#[require(FreeCameraState)]
76pub struct FreeCamera {
77/// Multiplier for pitch and yaw rotation speed.
78pub sensitivity: f32,
79/// [`KeyCode`] for forward translation.
80pub key_forward: KeyCode,
81/// [`KeyCode`] for backward translation.
82pub key_back: KeyCode,
83/// [`KeyCode`] for left translation.
84pub key_left: KeyCode,
85/// [`KeyCode`] for right translation.
86pub key_right: KeyCode,
87/// [`KeyCode`] for up translation.
88pub key_up: KeyCode,
89/// [`KeyCode`] for down translation.
90pub key_down: KeyCode,
91/// [`KeyCode`] to use [`run_speed`](FreeCamera::run_speed) instead of
92 /// [`walk_speed`](FreeCamera::walk_speed) for translation.
93pub key_run: KeyCode,
94/// [`MouseButton`] for grabbing the mouse focus.
95pub mouse_key_cursor_grab: MouseButton,
96/// [`KeyCode`] for grabbing the keyboard focus.
97pub keyboard_key_toggle_cursor_grab: KeyCode,
98/// Modifier [`KeyCode`] for making pressed axis alignment buttons go in opposite direction
99pub key_snap_reverse: KeyCode,
100/// [`KeyCode`] for snapping camera to top/bottom (+Y/-Y).
101pub axis_top: KeyCode,
102/// [`KeyCode`] for snapping camera to right/left (+X/-X).
103pub axis_right: KeyCode,
104/// [`KeyCode`] for snapping camera to front/back (-Z/+Z).
105pub axis_front: KeyCode,
106/// Base multiplier for unmodified translation speed.
107pub walk_speed: f32,
108/// Base multiplier for running translation speed.
109pub run_speed: f32,
110/// Multiplier for how much the mouse scroll wheel affects [`walk_speed`](FreeCamera::walk_speed)
111 /// and [`run_speed`](FreeCamera::run_speed).
112 ///
113 /// Mouse scroll affects speed exponentially. This is to ensure that scrolling the same
114 /// amount always has the same effect on speed, regardless of how the scroll amount
115 /// is reported by the hardware (i.e. as one big event vs many smaller events). This
116 /// also allows the free camera to navigate very large scenes easier.
117 ///
118 /// For every unit of scroll, the speed of the camera is multiplied by a factor of
119 /// `e^(scroll_factor)`.
120 ///
121 /// A reasonable value to start with is a `scroll_factor` between 0.04879016 (~ln(1.05))
122 /// and 0.0953102 (~ln(1.1)). They represent an increase by a factor between 1.05 and 1.1 per
123 /// positive unit scroll and a reduction between ~0.952 (~e^-0.04879016) and ~0.909
124 /// (~e^-0.0953102) times its value per negative unit scroll
125 ///
126 /// A `scroll_factor` closer to 0.0 means that speed will be less sensitive to scroll.
127 /// A `scroll_factor` equal to 0.0 means that speed is unaffected by scroll
128 /// (it will be multiplied by a factor of 1.0 per positive and negative unit scroll).
129pub scroll_factor: f32,
130/// Friction factor used to exponentially decay [`velocity`](FreeCameraState::velocity) over time.
131pub friction: f32,
132/// Speed of camera rotation to snapped axis in radians/second
133pub rotation_speed: f32,
134/// Whether the vertical inputs translate the camera in world or local space axes.
135pub vertical_movement_axis: VerticalMovementAxis,
136}
137138impl Defaultfor FreeCamera {
139fn default() -> Self {
140Self {
141 sensitivity: 0.2,
142 key_forward: KeyCode::KeyW,
143 key_back: KeyCode::KeyS,
144 key_left: KeyCode::KeyA,
145 key_right: KeyCode::KeyD,
146 key_up: KeyCode::KeyE,
147 key_down: KeyCode::KeyQ,
148 key_run: KeyCode::ShiftLeft,
149 mouse_key_cursor_grab: MouseButton::Right,
150 keyboard_key_toggle_cursor_grab: KeyCode::KeyM,
151 key_snap_reverse: KeyCode::ControlLeft,
152 axis_top: KeyCode::Numpad7,
153 axis_right: KeyCode::Numpad3,
154 axis_front: KeyCode::Numpad1,
155 walk_speed: 5.0,
156 run_speed: 15.0,
157// Approximation of ln(1.05)
158scroll_factor: 0.04879016,
159 friction: 40.0,
160 rotation_speed: PI / 16.0 * 60.0,
161 vertical_movement_axis: VerticalMovementAxis::default(),
162 }
163 }
164}
165166impl fmt::Displayfor FreeCamera {
167fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168f.write_fmt(format_args!("\nFreecamera Controls:\n Mouse\t- Move camera orientation\n Scroll\t- Adjust movement speed\n {0:?}\t- Hold to grab cursor\n {1:?}\t- Toggle cursor grab\n {2:?} & {3:?}\t- Fly forward & backwards\n {4:?} & {5:?}\t- Fly sideways left & right\n {6:?} & {7:?}\t- Fly up & down\n {8:?}\t- Fly faster while held\n [{9:?} + ]{10:?}\t- Snap to Up (+Y)/Down (-Y)\n [{11:?} + ]{12:?}\t- Snap to Right (+X)/Left (-X)\n [{13:?} + ]{14:?}\t- Snap to Front (-Z)/Back (+Z)",
self.mouse_key_cursor_grab, self.keyboard_key_toggle_cursor_grab,
self.key_forward, self.key_back, self.key_left, self.key_right,
self.key_up, self.key_down, self.key_run, self.key_snap_reverse,
self.axis_top, self.key_snap_reverse, self.axis_right,
self.key_snap_reverse, self.axis_front))write!(
169f,
170"
171Freecamera Controls:
172 Mouse\t- Move camera orientation
173 Scroll\t- Adjust movement speed
174 {:?}\t- Hold to grab cursor
175 {:?}\t- Toggle cursor grab
176 {:?} & {:?}\t- Fly forward & backwards
177 {:?} & {:?}\t- Fly sideways left & right
178 {:?} & {:?}\t- Fly up & down
179 {:?}\t- Fly faster while held
180 [{:?} + ]{:?}\t- Snap to Up (+Y)/Down (-Y)
181 [{:?} + ]{:?}\t- Snap to Right (+X)/Left (-X)
182 [{:?} + ]{:?}\t- Snap to Front (-Z)/Back (+Z)",
183self.mouse_key_cursor_grab,
184self.keyboard_key_toggle_cursor_grab,
185self.key_forward,
186self.key_back,
187self.key_left,
188self.key_right,
189self.key_up,
190self.key_down,
191self.key_run,
192self.key_snap_reverse,
193self.axis_top,
194self.key_snap_reverse,
195self.axis_right,
196self.key_snap_reverse,
197self.axis_front,
198 )199 }
200}
201202/// Whether the vertical inputs translate the camera in world or local space axes.
203#[derive(#[automatically_derived]impl ::core::fmt::Debug for VerticalMovementAxis {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::write_str(f,
match self {
VerticalMovementAxis::World => "World",
VerticalMovementAxis::Local => "Local",
})
}
}Debug, #[automatically_derived]impl ::core::default::Default for VerticalMovementAxis
{
#[inline]
fn default() -> Self { Self::World }
}Default, #[automatically_derived]#[doc(hidden)]unsafe impl ::core::clone::TrivialClone
for VerticalMovementAxis {
}
#[automatically_derived]impl ::core::clone::Clone for VerticalMovementAxis {
#[inline]
fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]impl ::core::marker::Copy for VerticalMovementAxis { }Copy)]
204pub enum VerticalMovementAxis {
205/// Vertical movement is aligned to the world.
206 ///
207 /// This is the default behavior in Bevy, Unreal and Blender.
208#[default]
209World,
210/// Vertical movement follows the camera's rotation.
211 ///
212 /// This is the default behavior in Unity and Godot.
213Local,
214}
215216/// Tracks the runtime state of a [`FreeCamera`] controller.
217///
218/// This component holds dynamic data that changes during camera operation,
219/// such as pitch, yaw, velocity, and whether the controller is currently enabled.
220///
221/// It is automatically added to any entity that has a [`FreeCamera`] component,
222/// and is updated by the [`FreeCameraPlugin`] systems in response to user input.
223#[derive(impl bevy_ecs::component::Component for FreeCameraState where
Self: ::core::marker::Send + ::core::marker::Sync + 'static {
const STORAGE_TYPE: bevy_ecs::component::StorageType =
bevy_ecs::component::StorageType::Table;
type Mutability = bevy_ecs::component::Mutable;
fn register_required_components(_requiree:
bevy_ecs::component::ComponentId,
required_components:
&mut bevy_ecs::component::RequiredComponentsRegistrator) {}
fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
use bevy_ecs::component::{
DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
};
(&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
}
fn relationship_accessor()
->
::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
::core::option::Option::None
}
}Component)]
224pub struct FreeCameraState {
225/// Enables [`FreeCamera`] controls when `true`.
226pub enabled: bool,
227/// Internal flag indicating if this controller has been initialized by the [`FreeCameraPlugin`].
228initialized: bool,
229/// This [`FreeCamera`]'s pitch rotation.
230pub pitch: f32,
231/// This [`FreeCamera`]'s yaw rotation.
232pub yaw: f32,
233/// Multiplier applied to movement speed.
234pub speed_multiplier: f32,
235/// This [`FreeCamera`]'s translation velocity.
236pub velocity: Vec3,
237/// Dictates camera movement during camera snap at speed, specified in [`FreeCamera`] by [`FreeCamera::rotation_speed`] field.
238 /// Consist of counter of seconds from pressing curve snap hotkeys and curve that used to interpolate between old and new rotation
239pub rotation_curve: Option<(f32, SampleAutoCurve<Quat>)>,
240}
241242impl Defaultfor FreeCameraState {
243fn default() -> Self {
244Self {
245 enabled: true,
246 initialized: false,
247 pitch: 0.0,
248 yaw: 0.0,
249 speed_multiplier: 1.0,
250 velocity: Vec3::ZERO,
251 rotation_curve: None,
252 }
253 }
254}
255256/// Updates the camera's position and orientation based on user input.
257///
258/// - [`FreeCamera`] contains static configuration such as key bindings, movement speed, and sensitivity.
259/// - [`FreeCameraState`] stores the dynamic runtime state, including pitch, yaw, velocity, and enable flags.
260///
261/// This system is typically added via the [`FreeCameraPlugin`].
262///
263/// Axis snapping takes priority over mouse movement.
264pub fn run_freecamera_controller(
265 time: Res<Time<Real>>,
266mut windows: Query<(&Window, &mut CursorOptions)>,
267 accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
268 accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
269 mouse_scroll_conversion: Res<MouseScrollPixelsPerLine>,
270 touch_input: Res<Touches>,
271 mouse_button_input: Res<ButtonInput<MouseButton>>,
272 key_input: Res<ButtonInput<KeyCode>>,
273mut toggle_cursor_grab: Local<bool>,
274mut mouse_cursor_grab: Local<bool>,
275mut query: Query<(&mut Transform, &mut FreeCameraState, &FreeCamera), With<Camera>>,
276) {
277let dt = time.delta_secs();
278279let Ok((mut transform, mut state, config)) = query.single_mut() else {
280return;
281 };
282283if !state.initialized {
284let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
285state.yaw = yaw;
286state.pitch = pitch;
287state.initialized = true;
288{
use ::tracing::__macro_support::Callsite as _;
static __CALLSITE: ::tracing::callsite::DefaultCallsite =
{
static META: ::tracing::Metadata<'static> =
{
::tracing_core::metadata::Metadata::new("event src/free_camera.rs:288",
"bevy_camera_controller::free_camera",
::tracing::Level::INFO,
::tracing_core::__macro_support::Option::Some("src/free_camera.rs"),
::tracing_core::__macro_support::Option::Some(288u32),
::tracing_core::__macro_support::Option::Some("bevy_camera_controller::free_camera"),
::tracing_core::field::FieldSet::new(&["message"],
::tracing_core::callsite::Identifier(&__CALLSITE)),
::tracing::metadata::Kind::EVENT)
};
::tracing::callsite::DefaultCallsite::new(&META)
};
let enabled =
::tracing::Level::INFO <= ::tracing::level_filters::STATIC_MAX_LEVEL
&&
::tracing::Level::INFO <=
::tracing::level_filters::LevelFilter::current() &&
{
let interest = __CALLSITE.interest();
!interest.is_never() &&
::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
interest)
};
if enabled {
(|value_set: ::tracing::field::ValueSet|
{
let meta = __CALLSITE.metadata();
::tracing::Event::dispatch(meta, &value_set);
;
})({
#[allow(unused_imports)]
use ::tracing::field::{debug, display, Value};
__CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&format_args!("{0}",
*config) as &dyn ::tracing::field::Value))])
});
} else { ; }
};info!("{}", *config);
289 }
290291if !state.enabled {
292// don't keep the cursor grabbed if the camera controller was disabled.
293if *toggle_cursor_grab || *mouse_cursor_grab {
294*toggle_cursor_grab = false;
295*mouse_cursor_grab = false;
296297for (_, mut cursor_options) in &mut windows {
298 cursor_options.grab_mode = CursorGrabMode::None;
299 cursor_options.visible = true;
300 }
301 }
302return;
303 }
304305let scroll = accumulated_mouse_scroll306 .to_lines(&mouse_scroll_conversion)
307 .delta
308 .y;
309// By using exponentiation we ensure that this scales up and down smoothly
310 // regardless of the amount of scrolling processed per frame
311state.speed_multiplier *= exp(config.scroll_factor * scroll);
312// Clamp the speed multiplier for safety.
313state.speed_multiplier = state.speed_multiplier.clamp(f32::EPSILON, f32::MAX);
314315// Handle key input
316let mut axis_input = Vec3::ZERO;
317if key_input.pressed(config.key_forward) {
318axis_input.z += 1.0;
319 }
320if key_input.pressed(config.key_back) {
321axis_input.z -= 1.0;
322 }
323if key_input.pressed(config.key_right) {
324axis_input.x += 1.0;
325 }
326if key_input.pressed(config.key_left) {
327axis_input.x -= 1.0;
328 }
329if key_input.pressed(config.key_up) {
330axis_input.y += 1.0;
331 }
332if key_input.pressed(config.key_down) {
333axis_input.y -= 1.0;
334 }
335336let mut cursor_grab_change = false;
337if key_input.just_pressed(config.keyboard_key_toggle_cursor_grab) {
338*toggle_cursor_grab = !*toggle_cursor_grab;
339cursor_grab_change = true;
340 }
341if mouse_button_input.just_pressed(config.mouse_key_cursor_grab) {
342*mouse_cursor_grab = true;
343cursor_grab_change = true;
344 }
345if mouse_button_input.just_released(config.mouse_key_cursor_grab) {
346*mouse_cursor_grab = false;
347cursor_grab_change = true;
348 }
349let cursor_grab = *mouse_cursor_grab || *toggle_cursor_grab;
350351// Update velocity
352if axis_input != Vec3::ZERO {
353let max_speed = if key_input.pressed(config.key_run) {
354config.run_speed * state.speed_multiplier
355 } else {
356config.walk_speed * state.speed_multiplier
357 };
358state.velocity = axis_input.normalize() * max_speed;
359 } else {
360let friction = config.friction.clamp(0.0, f32::MAX);
361state.velocity.smooth_nudge(&Vec3::ZERO, friction, dt);
362if state.velocity.length_squared() < 1e-6 {
363state.velocity = Vec3::ZERO;
364 }
365 }
366367// Apply movement update
368if state.velocity != Vec3::ZERO {
369let forward = *transform.forward();
370let right = *transform.right();
371let up = match config.vertical_movement_axis {
372 VerticalMovementAxis::World => Vec3::Y,
373 VerticalMovementAxis::Local => *transform.up(),
374 };
375transform.translation += state.velocity.x * dt * right376 + state.velocity.y * dt * up377 + state.velocity.z * dt * forward;
378 }
379380// Handle cursor grab
381if cursor_grab_change {
382if cursor_grab {
383for (window, mut cursor_options) in &mut windows {
384if !window.focused {
385continue;
386 }
387388 cursor_options.grab_mode = CursorGrabMode::Locked;
389 cursor_options.visible = false;
390 }
391 } else {
392for (_, mut cursor_options) in &mut windows {
393 cursor_options.grab_mode = CursorGrabMode::None;
394 cursor_options.visible = true;
395 }
396 }
397 }
398399// Handle mouse input
400if accumulated_mouse_motion.delta != Vec2::ZERO && cursor_grab {
401// Apply look update
402state.pitch = (state.pitch
403 - accumulated_mouse_motion.delta.y * RADIANS_PER_DOT * config.sensitivity)
404 .clamp(-PI / 2., PI / 2.);
405state.yaw -= accumulated_mouse_motion.delta.x * RADIANS_PER_DOT * config.sensitivity;
406transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, state.yaw, state.pitch);
407 }
408409// Handle touch input
410for touch in touch_input.iter() {
411if touch.delta() != Vec2::ZERO {
412 state.pitch = (state.pitch - touch.delta().y * RADIANS_PER_DOT * config.sensitivity)
413 .clamp(-PI / 2., PI / 2.);
414 state.yaw -= touch.delta().x * RADIANS_PER_DOT * config.sensitivity;
415 transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, state.yaw, state.pitch);
416 }
417 }
418// Axis snapping
419let mod_key_pressed = key_input.pressed(config.key_snap_reverse);
420let mut rotate_to = None;
421if key_input.just_pressed(config.axis_front) {
422if mod_key_pressed {
423rotate_to = Some((Dir3::Z, Dir3::Y));
424 } else {
425rotate_to = Some((Dir3::NEG_Z, Dir3::Y));
426 }
427 }
428if key_input.just_pressed(config.axis_right) {
429if mod_key_pressed {
430rotate_to = Some((Dir3::NEG_X, Dir3::Y));
431 } else {
432rotate_to = Some((Dir3::X, Dir3::Y));
433 }
434 }
435if key_input.just_pressed(config.axis_top) {
436if mod_key_pressed {
437rotate_to = Some((Dir3::NEG_Y, Dir3::NEG_Z));
438 } else {
439rotate_to = Some((Dir3::Y, Dir3::Z));
440 }
441 }
442if let Some((dir, up)) = rotate_to {
443let start = transform.rotation;
444let target = Transform::default().looking_to(dir, up).rotation; // I don't understand why Quat::look_to_rh produce different result.
445let angle = target.angle_between(start);
446let rotation_time = angle / config.rotation_speed;
447448if let Ok(interval) = Interval::new(0.0, rotation_time) {
449let curve = SampleAutoCurve::new(interval, [start, target])
450 .expect("Interval should be in bounds as start and end are finite numbers");
451state.rotation_curve = Some((0.0, curve));
452 }
453 }
454}
455456/// Smoothly changes orientation([`Transform`]) of [`FreeCamera`] camera according to target orientation in [`FreeCameraState`].
457///
458/// - [`FreeCamera`] contains static configuration such as key bindings and rotation speed.
459/// - [`FreeCameraState`] stores the dynamic runtime state, including direction for camera rotation and enable flags.
460///
461/// This system is typically added via the [`FreeCameraPlugin`].
462pub fn rotate_freecam_to(
463mut query: Query<(&mut Transform, &mut FreeCameraState), With<Camera>>,
464 time: Res<Time<Real>>,
465) {
466let Ok((mut transform, mut state)) = query.single_mut() else {
467return;
468 };
469if !state.enabled {
470return;
471 }
472let Some((progress, curve)) = state.rotation_curve.as_mut() else {
473return;
474 };
475*progress += time.delta_secs();
476transform.rotation = curve.sample_clamped(*progress);
477if !curve.domain().contains(*progress) {
478state.rotation_curve = None;
479 }
480let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
481state.pitch = pitch;
482state.yaw = yaw;
483}