Skip to content

Commit 0b49ff7

Browse files
Snap to view (#23674)
# Objective - Adds ``SnapToView`` camera controller as solution to #23499 ## Solution - Adds ``snap_to_view`` module behind ``free_camera`` feature flag ## Testing - Manually tested with ``cargo run --example free_camera_controller --features="free_camera bevy_dev_tools"`` and ``cargo run --example scene_viewer --features="free_camera bevy_dev_tools"``: - All six hotkeys works. - [LCtrl +] Numpad1 - Snap to front/back - [LCtrl +] Numpad3 - Snap to right/left - [LCtrl +] Numpad7 - Snap to top/bottom - Tested on Linux (Wayland) --- ## Showcase https://github.com/user-attachments/assets/cca71180-c273-473f-a168-9793438d861f Add ``SnapToViewCamera`` component to any ``Camera3d`` entity. Change parameters inside ``SnapToViewCamera`` to change snap speed and hotkeys. ```rust app.add_plugins(SnapToViewPlugin); commands.spawn(( Camera3d::default(), SnapToViewCamera::default(), )); ``` --------- Co-authored-by: taishi-sama <alexandra.2002.mikh@gmail.com>
1 parent 0d0fa6a commit 0b49ff7

4 files changed

Lines changed: 128 additions & 5 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
title: Snap to axes
3+
authors: ["@taishi-sama"]
4+
pull_requests: [23674]
5+
---
6+
7+
``FreeCamera`` now able to align itself to axes on hotkey presses.
8+
9+
| Default Key Binding | Action |
10+
|:--------------------|:-----------------------|
11+
| `Numpad1` | Snap to front(-Z) |
12+
| `LCtrl` + `Numpad1` | Snap to back(+Z) |
13+
| `Numpad3` | Snap to right(+X) |
14+
| `LCtrl` + `Numpad3` | Snap to left(-X) |
15+
| `Numpad7` | Snap to top(+Y) |
16+
| `LCtrl` + `Numpad7` | Snap to bottom(-Y) |
17+
18+
These hotkeys can be changed by modifying ``FreeCamera`` component.

crates/bevy_camera_controller/src/free_camera.rs

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
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

1718
use bevy_app::{App, Plugin, RunFixedMainLoop, RunFixedMainLoopSystems};
1819
use bevy_camera::Camera;
@@ -24,7 +25,9 @@ use bevy_input::mouse::{
2425
use bevy_input::touch::Touches;
2526
use bevy_input::ButtonInput;
2627
use 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};
2831
use bevy_time::{Real, Time};
2932
use bevy_transform::prelude::Transform;
3033
use 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

122137
impl 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

194226
impl 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.
213248
pub 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
}

examples/camera/free_camera_controller.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
//! | QE | Vertical movement |
2121
//! | Left shift | Run |
2222
//! | Scroll wheel | Change movement speed |
23+
//! | Numpad1 | Snap to front |
24+
//! | `LCtrl` + Numpad1 | Snap to back |
25+
//! | Numpad3 | Snap to right |
26+
//! | `LCtrl` + Numpad3 | Snap to left |
27+
//! | Numpad7 | Snap to top |
28+
//! | `LCtrl` + Numpad7 | Snap to bottom |
2329
//!
2430
//! The movement speed, sensitivity and friction can also be changed by the [`FreeCamera`] component.
2531
//!
@@ -103,7 +109,7 @@ fn spawn_text(mut commands: Commands, free_camera_query: Query<&FreeCamera>) {
103109
},
104110
children![Text::new(format!(
105111
"{}",
106-
free_camera_query.single().unwrap()
112+
free_camera_query.single().unwrap(),
107113
))],
108114
));
109115
commands.spawn((

examples/tools/scene_viewer/main.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@ fn setup_scene_after_load(
195195
run_speed: 3.0 * walk_speed,
196196
..default()
197197
};
198-
199198
// Display the controls of the scene viewer
200199
info!("{}", camera_controller);
201200
info!("{}", *scene_handle);

0 commit comments

Comments
 (0)