Skip to main content

bevy_camera_controller/
free_camera.rs

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.
17
18use 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::{
24    AccumulatedMouseMotion, 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};
33
34use core::{f32::consts::*, fmt};
35
36/// 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;
41
42impl Plugin for FreeCameraPlugin {
43    fn build(&self, app: &mut App) {
44        // This ordering is required so that both fixed update and update systems can see the results correctly
45        app.add_systems(
46            RunFixedMainLoop,
47            (run_freecamera_controller, rotate_freecam_to)
48                .chain()
49                .in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop),
50        );
51    }
52}
53
54/// 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;
60
61/// 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.
78    pub sensitivity: f32,
79    /// [`KeyCode`] for forward translation.
80    pub key_forward: KeyCode,
81    /// [`KeyCode`] for backward translation.
82    pub key_back: KeyCode,
83    /// [`KeyCode`] for left translation.
84    pub key_left: KeyCode,
85    /// [`KeyCode`] for right translation.
86    pub key_right: KeyCode,
87    /// [`KeyCode`] for up translation.
88    pub key_up: KeyCode,
89    /// [`KeyCode`] for down translation.
90    pub key_down: KeyCode,
91    /// [`KeyCode`] to use [`run_speed`](FreeCamera::run_speed) instead of
92    /// [`walk_speed`](FreeCamera::walk_speed) for translation.
93    pub key_run: KeyCode,
94    /// [`MouseButton`] for grabbing the mouse focus.
95    pub mouse_key_cursor_grab: MouseButton,
96    /// [`KeyCode`] for grabbing the keyboard focus.
97    pub keyboard_key_toggle_cursor_grab: KeyCode,
98    /// Modifier [`KeyCode`] for making pressed axis alignment buttons go in opposite direction
99    pub key_snap_reverse: KeyCode,
100    /// [`KeyCode`] for snapping camera to top/bottom (+Y/-Y).
101    pub axis_top: KeyCode,
102    /// [`KeyCode`] for snapping camera to right/left (+X/-X).
103    pub axis_right: KeyCode,
104    /// [`KeyCode`] for snapping camera to front/back (-Z/+Z).
105    pub axis_front: KeyCode,
106    /// Base multiplier for unmodified translation speed.
107    pub walk_speed: f32,
108    /// Base multiplier for running translation speed.
109    pub 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).
129    pub scroll_factor: f32,
130    /// Friction factor used to exponentially decay [`velocity`](FreeCameraState::velocity) over time.
131    pub friction: f32,
132    /// Speed of camera rotation to snapped axis in radians/second
133    pub rotation_speed: f32,
134    /// Whether the vertical inputs translate the camera in world or local space axes.
135    pub vertical_movement_axis: VerticalMovementAxis,
136}
137
138impl Default for FreeCamera {
139    fn default() -> Self {
140        Self {
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)
158            scroll_factor: 0.04879016,
159            friction: 40.0,
160            rotation_speed: PI / 16.0 * 60.0,
161            vertical_movement_axis: VerticalMovementAxis::default(),
162        }
163    }
164}
165
166impl fmt::Display for FreeCamera {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        f.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!(
169            f,
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)",
183            self.mouse_key_cursor_grab,
184            self.keyboard_key_toggle_cursor_grab,
185            self.key_forward,
186            self.key_back,
187            self.key_left,
188            self.key_right,
189            self.key_up,
190            self.key_down,
191            self.key_run,
192            self.key_snap_reverse,
193            self.axis_top,
194            self.key_snap_reverse,
195            self.axis_right,
196            self.key_snap_reverse,
197            self.axis_front,
198        )
199    }
200}
201
202/// 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]
209    World,
210    /// Vertical movement follows the camera's rotation.
211    ///
212    /// This is the default behavior in Unity and Godot.
213    Local,
214}
215
216/// 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`.
226    pub enabled: bool,
227    /// Internal flag indicating if this controller has been initialized by the [`FreeCameraPlugin`].
228    initialized: bool,
229    /// This [`FreeCamera`]'s pitch rotation.
230    pub pitch: f32,
231    /// This [`FreeCamera`]'s yaw rotation.
232    pub yaw: f32,
233    /// Multiplier applied to movement speed.
234    pub speed_multiplier: f32,
235    /// This [`FreeCamera`]'s translation velocity.
236    pub 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
239    pub rotation_curve: Option<(f32, SampleAutoCurve<Quat>)>,
240}
241
242impl Default for FreeCameraState {
243    fn default() -> Self {
244        Self {
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}
255
256/// 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>>,
266    mut 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>>,
273    mut toggle_cursor_grab: Local<bool>,
274    mut mouse_cursor_grab: Local<bool>,
275    mut query: Query<(&mut Transform, &mut FreeCameraState, &FreeCamera), With<Camera>>,
276) {
277    let dt = time.delta_secs();
278
279    let Ok((mut transform, mut state, config)) = query.single_mut() else {
280        return;
281    };
282
283    if !state.initialized {
284        let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
285        state.yaw = yaw;
286        state.pitch = pitch;
287        state.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    }
290
291    if !state.enabled {
292        // don't keep the cursor grabbed if the camera controller was disabled.
293        if *toggle_cursor_grab || *mouse_cursor_grab {
294            *toggle_cursor_grab = false;
295            *mouse_cursor_grab = false;
296
297            for (_, mut cursor_options) in &mut windows {
298                cursor_options.grab_mode = CursorGrabMode::None;
299                cursor_options.visible = true;
300            }
301        }
302        return;
303    }
304
305    let scroll = accumulated_mouse_scroll
306        .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
311    state.speed_multiplier *= exp(config.scroll_factor * scroll);
312    // Clamp the speed multiplier for safety.
313    state.speed_multiplier = state.speed_multiplier.clamp(f32::EPSILON, f32::MAX);
314
315    // Handle key input
316    let mut axis_input = Vec3::ZERO;
317    if key_input.pressed(config.key_forward) {
318        axis_input.z += 1.0;
319    }
320    if key_input.pressed(config.key_back) {
321        axis_input.z -= 1.0;
322    }
323    if key_input.pressed(config.key_right) {
324        axis_input.x += 1.0;
325    }
326    if key_input.pressed(config.key_left) {
327        axis_input.x -= 1.0;
328    }
329    if key_input.pressed(config.key_up) {
330        axis_input.y += 1.0;
331    }
332    if key_input.pressed(config.key_down) {
333        axis_input.y -= 1.0;
334    }
335
336    let mut cursor_grab_change = false;
337    if key_input.just_pressed(config.keyboard_key_toggle_cursor_grab) {
338        *toggle_cursor_grab = !*toggle_cursor_grab;
339        cursor_grab_change = true;
340    }
341    if mouse_button_input.just_pressed(config.mouse_key_cursor_grab) {
342        *mouse_cursor_grab = true;
343        cursor_grab_change = true;
344    }
345    if mouse_button_input.just_released(config.mouse_key_cursor_grab) {
346        *mouse_cursor_grab = false;
347        cursor_grab_change = true;
348    }
349    let cursor_grab = *mouse_cursor_grab || *toggle_cursor_grab;
350
351    // Update velocity
352    if axis_input != Vec3::ZERO {
353        let max_speed = if key_input.pressed(config.key_run) {
354            config.run_speed * state.speed_multiplier
355        } else {
356            config.walk_speed * state.speed_multiplier
357        };
358        state.velocity = axis_input.normalize() * max_speed;
359    } else {
360        let friction = config.friction.clamp(0.0, f32::MAX);
361        state.velocity.smooth_nudge(&Vec3::ZERO, friction, dt);
362        if state.velocity.length_squared() < 1e-6 {
363            state.velocity = Vec3::ZERO;
364        }
365    }
366
367    // Apply movement update
368    if state.velocity != Vec3::ZERO {
369        let forward = *transform.forward();
370        let right = *transform.right();
371        let up = match config.vertical_movement_axis {
372            VerticalMovementAxis::World => Vec3::Y,
373            VerticalMovementAxis::Local => *transform.up(),
374        };
375        transform.translation += state.velocity.x * dt * right
376            + state.velocity.y * dt * up
377            + state.velocity.z * dt * forward;
378    }
379
380    // Handle cursor grab
381    if cursor_grab_change {
382        if cursor_grab {
383            for (window, mut cursor_options) in &mut windows {
384                if !window.focused {
385                    continue;
386                }
387
388                cursor_options.grab_mode = CursorGrabMode::Locked;
389                cursor_options.visible = false;
390            }
391        } else {
392            for (_, mut cursor_options) in &mut windows {
393                cursor_options.grab_mode = CursorGrabMode::None;
394                cursor_options.visible = true;
395            }
396        }
397    }
398
399    // Handle mouse input
400    if accumulated_mouse_motion.delta != Vec2::ZERO && cursor_grab {
401        // Apply look update
402        state.pitch = (state.pitch
403            - accumulated_mouse_motion.delta.y * RADIANS_PER_DOT * config.sensitivity)
404            .clamp(-PI / 2., PI / 2.);
405        state.yaw -= accumulated_mouse_motion.delta.x * RADIANS_PER_DOT * config.sensitivity;
406        transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, state.yaw, state.pitch);
407    }
408
409    // Handle touch input
410    for touch in touch_input.iter() {
411        if 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
419    let mod_key_pressed = key_input.pressed(config.key_snap_reverse);
420    let mut rotate_to = None;
421    if key_input.just_pressed(config.axis_front) {
422        if mod_key_pressed {
423            rotate_to = Some((Dir3::Z, Dir3::Y));
424        } else {
425            rotate_to = Some((Dir3::NEG_Z, Dir3::Y));
426        }
427    }
428    if key_input.just_pressed(config.axis_right) {
429        if mod_key_pressed {
430            rotate_to = Some((Dir3::NEG_X, Dir3::Y));
431        } else {
432            rotate_to = Some((Dir3::X, Dir3::Y));
433        }
434    }
435    if key_input.just_pressed(config.axis_top) {
436        if mod_key_pressed {
437            rotate_to = Some((Dir3::NEG_Y, Dir3::NEG_Z));
438        } else {
439            rotate_to = Some((Dir3::Y, Dir3::Z));
440        }
441    }
442    if let Some((dir, up)) = rotate_to {
443        let start = transform.rotation;
444        let target = Transform::default().looking_to(dir, up).rotation; // I don't understand why Quat::look_to_rh produce different result.
445        let angle = target.angle_between(start);
446        let rotation_time = angle / config.rotation_speed;
447
448        if let Ok(interval) = Interval::new(0.0, rotation_time) {
449            let curve = SampleAutoCurve::new(interval, [start, target])
450                .expect("Interval should be in bounds as start and end are finite numbers");
451            state.rotation_curve = Some((0.0, curve));
452        }
453    }
454}
455
456/// 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(
463    mut query: Query<(&mut Transform, &mut FreeCameraState), With<Camera>>,
464    time: Res<Time<Real>>,
465) {
466    let Ok((mut transform, mut state)) = query.single_mut() else {
467        return;
468    };
469    if !state.enabled {
470        return;
471    }
472    let Some((progress, curve)) = state.rotation_curve.as_mut() else {
473        return;
474    };
475    *progress += time.delta_secs();
476    transform.rotation = curve.sample_clamped(*progress);
477    if !curve.domain().contains(*progress) {
478        state.rotation_curve = None;
479    }
480    let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
481    state.pitch = pitch;
482    state.yaw = yaw;
483}