1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
use glam::Vec3;
use winit::event::MouseButton;
use crate::camera::Camera;
use crate::input::Input;
/// Controls how the orbit camera moves.
#[derive(Clone, Copy, Debug)]
pub enum OrbitMode {
/// User controls camera with mouse drag and scroll wheel.
Interactive,
/// Camera auto-rotates around the target, ignoring input.
AutoRotate {
/// Rotation speed in radians per second (positive = counterclockwise from above).
speed: f32,
},
}
impl Default for OrbitMode {
fn default() -> Self {
Self::Interactive
}
}
/// A camera controller that orbits around a target point.
///
/// # Example
/// ```ignore
/// let mut orbit = OrbitCamera::new()
/// .target(Vec3::ZERO)
/// .distance(5.0)
/// .mode(OrbitMode::Interactive);
///
/// // In frame loop:
/// orbit.update(frame.input, frame.dt);
/// *frame.camera = orbit.camera();
/// ```
#[derive(Clone, Debug)]
pub struct OrbitCamera {
/// Point the camera orbits around.
pub target: Vec3,
/// Distance from target.
pub distance: f32,
/// Horizontal angle in radians (yaw).
pub azimuth: f32,
/// Vertical angle in radians (pitch), clamped to avoid gimbal lock.
pub elevation: f32,
/// Field of view in radians.
pub fov: f32,
/// Control mode.
pub mode: OrbitMode,
/// Mouse sensitivity for interactive mode.
pub sensitivity: f32,
/// Scroll zoom sensitivity.
pub zoom_sensitivity: f32,
/// Minimum distance from target.
pub min_distance: f32,
/// Maximum distance from target.
pub max_distance: f32,
}
impl Default for OrbitCamera {
fn default() -> Self {
Self {
target: Vec3::ZERO,
distance: 5.0,
azimuth: 0.0,
elevation: 0.3,
fov: std::f32::consts::FRAC_PI_2,
mode: OrbitMode::Interactive,
sensitivity: 0.005,
zoom_sensitivity: 0.5,
min_distance: 0.5,
max_distance: 100.0,
}
}
}
impl OrbitCamera {
pub fn new() -> Self {
Self::default()
}
/// Set the target point to orbit around.
pub fn target(mut self, target: impl Into<Vec3>) -> Self {
self.target = target.into();
self
}
/// Set the distance from target.
pub fn distance(mut self, distance: f32) -> Self {
self.distance = distance.clamp(self.min_distance, self.max_distance);
self
}
/// Set the control mode.
pub fn mode(mut self, mode: OrbitMode) -> Self {
self.mode = mode;
self
}
/// Set the field of view in degrees.
pub fn fov(mut self, fov_degrees: f32) -> Self {
self.fov = fov_degrees.to_radians();
self
}
/// Set the initial azimuth (horizontal angle) in radians.
pub fn azimuth(mut self, azimuth: f32) -> Self {
self.azimuth = azimuth;
self
}
/// Set the initial elevation (vertical angle) in radians.
pub fn elevation(mut self, elevation: f32) -> Self {
self.elevation = elevation.clamp(
-std::f32::consts::FRAC_PI_2 + 0.01,
std::f32::consts::FRAC_PI_2 - 0.01,
);
self
}
/// Set mouse sensitivity for interactive mode.
pub fn sensitivity(mut self, sensitivity: f32) -> Self {
self.sensitivity = sensitivity;
self
}
/// Set scroll zoom sensitivity.
pub fn zoom_sensitivity(mut self, sensitivity: f32) -> Self {
self.zoom_sensitivity = sensitivity;
self
}
/// Set distance limits.
pub fn distance_limits(mut self, min: f32, max: f32) -> Self {
self.min_distance = min;
self.max_distance = max;
self.distance = self.distance.clamp(min, max);
self
}
/// Update the camera based on input and delta time.
pub fn update(&mut self, input: &Input, dt: f32) {
match self.mode {
OrbitMode::Interactive => {
// Rotate when left mouse button is held
if input.mouse_down(MouseButton::Left) {
let delta = input.mouse_delta();
self.azimuth -= delta.x * self.sensitivity;
self.elevation += delta.y * self.sensitivity;
// Clamp elevation to avoid gimbal lock
self.elevation = self.elevation.clamp(
-std::f32::consts::FRAC_PI_2 + 0.01,
std::f32::consts::FRAC_PI_2 - 0.01,
);
}
// Zoom with scroll wheel
let scroll = input.scroll_delta();
if scroll.y.abs() > 0.0 {
self.distance -= scroll.y * self.zoom_sensitivity;
self.distance = self.distance.clamp(self.min_distance, self.max_distance);
}
}
OrbitMode::AutoRotate { speed } => {
self.azimuth += speed * dt;
}
}
}
/// Get the current camera state.
pub fn camera(&self) -> Camera {
// Spherical to Cartesian conversion
let offset = Vec3::new(
self.distance * self.elevation.cos() * self.azimuth.sin(),
self.distance * self.elevation.sin(),
self.distance * self.elevation.cos() * self.azimuth.cos(),
);
let position = self.target + offset;
Camera {
position,
forward: (self.target - position).normalize_or(Vec3::NEG_Z),
up: Vec3::Y,
fov: self.fov,
near: 0.1,
far: 1000.0,
}
}
}