Skip to main content

bevy_orbit_camera/
control.rs

1use 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
11// Prevents huge jumps from RDP, focus changes, cursor teleporting, etc.
12const 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/// A 3rd person camera that orbits around the target.
24#[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    // Can only control one camera at a time.
58    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    // Amount of scroll since last update.
77    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    // World up vector.
87    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    // CursorMoved gives absolute cursor positions. We derive a stable delta ourselves.
104    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                    // Clamp instead of trusting large raw jumps.
116                    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    // ROTATE / ORBIT
132    // Changes the forward vector.
133    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    // TRANSLATE
146    // Changes the target vector.
147    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    // ZOOM
166    // Changes the radius.
167    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}