1313//! The required [`FreeCameraState`] component will be added automatically.
1414//!
1515//! To configure the settings of this controller, modify the fields of the [`FreeCamera`] component.
16+ // TODO: Discuss switching camera to orthographic mode.
1617
1718use bevy_app:: { App , Plugin , RunFixedMainLoop , RunFixedMainLoopSystems } ;
1819use bevy_camera:: Camera ;
@@ -24,7 +25,9 @@ use bevy_input::mouse::{
2425use bevy_input:: touch:: Touches ;
2526use bevy_input:: ButtonInput ;
2627use bevy_log:: info;
27- use bevy_math:: { ops:: exp, EulerRot , Quat , StableInterpolate , Vec2 , Vec3 } ;
28+ use bevy_math:: curve:: { Interval , SampleAutoCurve } ;
29+ use bevy_math:: Curve ;
30+ use bevy_math:: { ops:: exp, Dir3 , EulerRot , Quat , StableInterpolate , Vec2 , Vec3 } ;
2831use bevy_time:: { Real , Time } ;
2932use bevy_transform:: prelude:: Transform ;
3033use bevy_window:: { CursorGrabMode , CursorOptions , Window } ;
@@ -42,7 +45,9 @@ impl Plugin for FreeCameraPlugin {
4245 // This ordering is required so that both fixed update and update systems can see the results correctly
4346 app. add_systems (
4447 RunFixedMainLoop ,
45- run_freecamera_controller. in_set ( RunFixedMainLoopSystems :: BeforeFixedMainLoop ) ,
48+ ( run_freecamera_controller, rotate_freecam_to)
49+ . chain ( )
50+ . in_set ( RunFixedMainLoopSystems :: BeforeFixedMainLoop ) ,
4651 ) ;
4752 }
4853}
@@ -91,6 +96,14 @@ pub struct FreeCamera {
9196 pub mouse_key_cursor_grab : MouseButton ,
9297 /// [`KeyCode`] for grabbing the keyboard focus.
9398 pub keyboard_key_toggle_cursor_grab : KeyCode ,
99+ /// Modifier [`KeyCode`] for making pressed axis alignment buttons go in opposite direction
100+ pub key_snap_reverse : KeyCode ,
101+ /// [`KeyCode`] for snapping camera to top/bottom (+Y/-Y).
102+ pub axis_top : KeyCode ,
103+ /// [`KeyCode`] for snapping camera to right/left (+X/-X).
104+ pub axis_right : KeyCode ,
105+ /// [`KeyCode`] for snapping camera to front/back (-Z/+Z).
106+ pub axis_front : KeyCode ,
94107 /// Base multiplier for unmodified translation speed.
95108 pub walk_speed : f32 ,
96109 /// Base multiplier for running translation speed.
@@ -117,6 +130,8 @@ pub struct FreeCamera {
117130 pub scroll_factor : f32 ,
118131 /// Friction factor used to exponentially decay [`velocity`](FreeCameraState::velocity) over time.
119132 pub friction : f32 ,
133+ /// Speed of camera rotation to snapped axis in radians/second
134+ pub rotation_speed : f32 ,
120135}
121136
122137impl Default for FreeCamera {
@@ -132,11 +147,16 @@ impl Default for FreeCamera {
132147 key_run : KeyCode :: ShiftLeft ,
133148 mouse_key_cursor_grab : MouseButton :: Right ,
134149 keyboard_key_toggle_cursor_grab : KeyCode :: KeyM ,
150+ key_snap_reverse : KeyCode :: ControlLeft ,
151+ axis_top : KeyCode :: Numpad7 ,
152+ axis_right : KeyCode :: Numpad3 ,
153+ axis_front : KeyCode :: Numpad1 ,
135154 walk_speed : 5.0 ,
136155 run_speed : 15.0 ,
137156 // Approximation of ln(1.05)
138157 scroll_factor : 0.04879016 ,
139158 friction : 40.0 ,
159+ rotation_speed : PI / 16.0 * 60.0 ,
140160 }
141161 }
142162}
@@ -154,7 +174,10 @@ Freecamera Controls:
154174 {:?} & {:?}\t - Fly forward & backwards
155175 {:?} & {:?}\t - Fly sideways left & right
156176 {:?} & {:?}\t - Fly up & down
157- {:?}\t - Fly faster while held" ,
177+ {:?}\t - Fly faster while held
178+ [{:?} + ]{:?}\t - Snap to Up (+Y)/Down (-Y)
179+ [{:?} + ]{:?}\t - Snap to Right (+X)/Left (-X)
180+ [{:?} + ]{:?}\t - Snap to Front (-Z)/Back (+Z)" ,
158181 self . mouse_key_cursor_grab,
159182 self . keyboard_key_toggle_cursor_grab,
160183 self . key_forward,
@@ -164,6 +187,12 @@ Freecamera Controls:
164187 self . key_up,
165188 self . key_down,
166189 self . key_run,
190+ self . key_snap_reverse,
191+ self . axis_top,
192+ self . key_snap_reverse,
193+ self . axis_right,
194+ self . key_snap_reverse,
195+ self . axis_front,
167196 )
168197 }
169198}
@@ -189,6 +218,9 @@ pub struct FreeCameraState {
189218 pub speed_multiplier : f32 ,
190219 /// This [`FreeCamera`]'s translation velocity.
191220 pub velocity : Vec3 ,
221+ /// Dictates camera movement during camera snap at speed, specified in [`FreeCamera`] by [`FreeCamera::rotation_speed`] field.
222+ /// Consist of counter of seconds from pressing curve snap hotkeys and curve that used to interpolate between old and new rotation
223+ pub rotation_curve : Option < ( f32 , SampleAutoCurve < Quat > ) > ,
192224}
193225
194226impl Default for FreeCameraState {
@@ -200,6 +232,7 @@ impl Default for FreeCameraState {
200232 yaw : 0.0 ,
201233 speed_multiplier : 1.0 ,
202234 velocity : Vec3 :: ZERO ,
235+ rotation_curve : None ,
203236 }
204237 }
205238}
@@ -210,6 +243,8 @@ impl Default for FreeCameraState {
210243/// - [`FreeCameraState`] stores the dynamic runtime state, including pitch, yaw, velocity, and enable flags.
211244///
212245/// This system is typically added via the [`FreeCameraPlugin`].
246+ ///
247+ /// Axis snapping takes priority over mouse movement.
213248pub fn run_freecamera_controller (
214249 time : Res < Time < Real > > ,
215250 mut windows : Query < ( & Window , & mut CursorOptions ) > ,
@@ -361,4 +396,69 @@ pub fn run_freecamera_controller(
361396 transform. rotation = Quat :: from_euler ( EulerRot :: ZYX , 0.0 , state. yaw , state. pitch ) ;
362397 }
363398 }
399+ // Axis snapping
400+ let mod_key_pressed = key_input. pressed ( config. key_snap_reverse ) ;
401+ let mut rotate_to = None ;
402+ if key_input. just_pressed ( config. axis_front ) {
403+ if mod_key_pressed {
404+ rotate_to = Some ( ( Dir3 :: Z , Dir3 :: Y ) ) ;
405+ } else {
406+ rotate_to = Some ( ( Dir3 :: NEG_Z , Dir3 :: Y ) ) ;
407+ }
408+ }
409+ if key_input. just_pressed ( config. axis_right ) {
410+ if mod_key_pressed {
411+ rotate_to = Some ( ( Dir3 :: NEG_X , Dir3 :: Y ) ) ;
412+ } else {
413+ rotate_to = Some ( ( Dir3 :: X , Dir3 :: Y ) ) ;
414+ }
415+ }
416+ if key_input. just_pressed ( config. axis_top ) {
417+ if mod_key_pressed {
418+ rotate_to = Some ( ( Dir3 :: NEG_Y , Dir3 :: NEG_Z ) ) ;
419+ } else {
420+ rotate_to = Some ( ( Dir3 :: Y , Dir3 :: Z ) ) ;
421+ }
422+ }
423+ if let Some ( ( dir, up) ) = rotate_to {
424+ let start = transform. rotation ;
425+ let target = Transform :: default ( ) . looking_to ( dir, up) . rotation ; // I don't understand why Quat::look_to_rh produce different result.
426+ let angle = target. angle_between ( start) ;
427+ let rotation_time = angle / config. rotation_speed ;
428+
429+ if let Ok ( interval) = Interval :: new ( 0.0 , rotation_time) {
430+ let curve = SampleAutoCurve :: new ( interval, [ start, target] )
431+ . expect ( "Interval should be in bounds as start and end are finite numbers" ) ;
432+ state. rotation_curve = Some ( ( 0.0 , curve) ) ;
433+ }
434+ }
435+ }
436+
437+ /// Smoothly changes orientation([`Transform`]) of [`FreeCamera`] camera according to target orientation in [`FreeCameraState`].
438+ ///
439+ /// - [`FreeCamera`] contains static configuration such as key bindings and rotation speed.
440+ /// - [`FreeCameraState`] stores the dynamic runtime state, including direction for camera rotation and enable flags.
441+ ///
442+ /// This system is typically added via the [`FreeCameraPlugin`].
443+ pub fn rotate_freecam_to (
444+ mut query : Query < ( & mut Transform , & mut FreeCameraState ) , With < Camera > > ,
445+ time : Res < Time < Real > > ,
446+ ) {
447+ let Ok ( ( mut transform, mut state) ) = query. single_mut ( ) else {
448+ return ;
449+ } ;
450+ if !state. enabled {
451+ return ;
452+ }
453+ let Some ( ( progress, curve) ) = state. rotation_curve . as_mut ( ) else {
454+ return ;
455+ } ;
456+ * progress += time. delta_secs ( ) ;
457+ transform. rotation = curve. sample_clamped ( * progress) ;
458+ if !curve. domain ( ) . contains ( * progress) {
459+ state. rotation_curve = None ;
460+ }
461+ let ( yaw, pitch, _roll) = transform. rotation . to_euler ( EulerRot :: YXZ ) ;
462+ state. pitch = pitch;
463+ state. yaw = yaw;
364464}
0 commit comments