bevy_orbit_camera/
control.rs1use std::f32;
2
3use crate::transform::LookTransform;
4use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
5use bevy::prelude::*;
6use bevy::window::CursorMoved;
7
8const ROTATION_RADIANS_PER_PIXEL: f32 = 0.01;
9const TRANSLATION_UNITS_PER_PIXEL: f32 = 0.01;
10
11const MAX_CURSOR_DELTA: f32 = 100.0;
13
14#[derive(Clone, Component, Copy, Debug, Reflect)]
15#[reflect(Component, Debug)]
16pub(crate) struct OrbitControllerState {
17 pub(crate) yaw: f32,
18 pitch: f32,
19 yaw_zero_forward: Vec3,
20 last_cursor_position: Option<Vec2>,
21}
22
23#[derive(Clone, Component, Copy, Debug, Reflect)]
25#[reflect(Component, Default, Debug)]
26pub struct Controller {
27 pub mouse_rotate_sensitivity: Vec2,
28 pub mouse_translate_sensitivity: Vec2,
29 pub mouse_wheel_zoom_sensitivity: f32,
30 pub pixels_per_line: f32,
31}
32
33impl Default for Controller {
34 fn default() -> Self {
35 Self {
36 mouse_rotate_sensitivity: Vec2::splat(0.2),
37 mouse_translate_sensitivity: Vec2::splat(2.0),
38 mouse_wheel_zoom_sensitivity: 0.2,
39 pixels_per_line: 53.0,
40 }
41 }
42}
43
44pub fn system(
45 mut commands: Commands,
46 mut mouse_wheel_reader: MessageReader<MouseWheel>,
47 mut cursor_moved_events: MessageReader<CursorMoved>,
48 mouse_buttons: Res<ButtonInput<MouseButton>>,
49 keyboard: Res<ButtonInput<KeyCode>>,
50 mut cameras: Query<(
51 Entity,
52 &Controller,
53 Option<&mut OrbitControllerState>,
54 &mut LookTransform,
55 )>,
56) {
57 let (
59 entity,
60 Controller {
61 mouse_rotate_sensitivity,
62 mouse_translate_sensitivity,
63 mouse_wheel_zoom_sensitivity,
64 pixels_per_line,
65 ..
66 },
67 controller_state,
68 mut transform,
69 ) = match cameras.single_mut() {
70 Ok((entity, controller, controller_state, look_transform)) => {
71 (entity, controller, controller_state, look_transform)
72 }
73 Err(_) => return,
74 };
75
76 let scroll_delta = mouse_wheel_reader.read().fold(1.0, |acc, event| {
78 let scroll_amount = match event.unit {
79 MouseScrollUnit::Line => event.y,
80 MouseScrollUnit::Pixel => event.y / *pixels_per_line,
81 };
82
83 acc * (1.0 - scroll_amount * mouse_wheel_zoom_sensitivity)
84 });
85
86 let up = transform.up.try_normalize().unwrap_or(Vec3::Y);
88
89 let Some(initial_forward) = (transform.target - transform.eye).try_normalize() else {
90 return;
91 };
92
93 let mut state = controller_state
94 .as_deref()
95 .copied()
96 .unwrap_or_else(|| OrbitControllerState::from_forward(initial_forward, up));
97
98 let rotating =
99 keyboard.pressed(KeyCode::ControlLeft) || mouse_buttons.pressed(MouseButton::Middle);
100 let translating = mouse_buttons.pressed(MouseButton::Right);
101 let using_mouse = rotating || translating;
102
103 let cursor_position = cursor_moved_events
105 .read()
106 .last()
107 .map(|event| event.position);
108
109 let cursor_delta = match cursor_position {
110 Some(cursor_position) => {
111 let delta = match state.last_cursor_position {
112 Some(last_cursor_position) if using_mouse => {
113 let delta = cursor_position - last_cursor_position;
114
115 if delta.length() > MAX_CURSOR_DELTA {
117 delta.normalize_or_zero() * MAX_CURSOR_DELTA
118 } else {
119 delta
120 }
121 }
122 _ => Vec2::ZERO,
123 };
124
125 state.last_cursor_position = Some(cursor_position);
126 delta
127 }
128 None => Vec2::ZERO,
129 };
130
131 if rotating {
134 let delta = mouse_rotate_sensitivity * cursor_delta * ROTATION_RADIANS_PER_PIXEL;
135
136 state.yaw -= delta.x;
137 state.pitch = (state.pitch - delta.y).clamp(
138 -f32::consts::FRAC_PI_2 * 0.99,
139 f32::consts::FRAC_PI_2 * 0.99,
140 );
141 }
142
143 let forward = state.forward(up);
144
145 let mut target = transform.target;
148
149 if translating {
150 let delta = mouse_translate_sensitivity * cursor_delta * TRANSLATION_UNITS_PER_PIXEL;
151
152 if let Some(right) = up.cross(forward).try_normalize() {
153 let upish = forward.cross(right).normalize();
154 target += delta.x * right + delta.y * upish;
155 }
156 }
157
158 match controller_state {
159 Some(mut controller_state) => *controller_state = state,
160 None => {
161 commands.entity(entity).insert(state);
162 }
163 }
164
165 let radius = transform.radius() * scroll_delta;
168
169 transform.target = target;
170 transform.eye = target - forward * radius;
171}
172
173impl OrbitControllerState {
174 pub(crate) fn from_forward(forward: Vec3, up: Vec3) -> Self {
175 let pitch = forward.dot(up).clamp(-1.0, 1.0).asin();
176
177 let yaw_zero_forward = (forward - up * forward.dot(up))
178 .try_normalize()
179 .unwrap_or_else(|| up.any_orthonormal_vector());
180
181 Self {
182 yaw: 0.0,
183 pitch: pitch.clamp(
184 -f32::consts::FRAC_PI_2 * 0.99,
185 f32::consts::FRAC_PI_2 * 0.99,
186 ),
187 yaw_zero_forward,
188 last_cursor_position: None,
189 }
190 }
191
192 fn forward(&self, up: Vec3) -> Vec3 {
193 let horizontal = Quat::from_axis_angle(up, self.yaw) * self.yaw_zero_forward;
194 (horizontal * self.pitch.cos() + up * self.pitch.sin()).normalize()
195 }
196}