camera.rs (11074B)
1 use glam::{Mat4, Vec3}; 2 3 #[derive(Debug, Copy, Clone)] 4 pub struct Camera { 5 pub eye: Vec3, 6 pub target: Vec3, 7 pub up: Vec3, 8 9 pub fov_y: f32, 10 pub znear: f32, 11 pub zfar: f32, 12 } 13 14 /// Arcball camera controller for orbital navigation around a target point. 15 #[derive(Debug, Clone)] 16 pub struct ArcballController { 17 pub target: Vec3, 18 pub distance: f32, 19 pub yaw: f32, // radians, around Y axis 20 pub pitch: f32, // radians, up/down 21 pub sensitivity: f32, 22 pub zoom_sensitivity: f32, 23 pub min_distance: f32, 24 pub max_distance: f32, 25 } 26 27 impl Default for ArcballController { 28 fn default() -> Self { 29 Self { 30 target: Vec3::ZERO, 31 distance: 5.0, 32 yaw: 0.0, 33 pitch: 0.3, 34 sensitivity: 0.005, 35 zoom_sensitivity: 0.1, 36 min_distance: 0.1, 37 max_distance: 1000.0, 38 } 39 } 40 } 41 42 impl ArcballController { 43 /// Initialize from an existing camera. 44 pub fn from_camera(camera: &Camera) -> Self { 45 let offset = camera.eye - camera.target; 46 let distance = offset.length(); 47 48 // Compute yaw (rotation around Y) and pitch (elevation) 49 let yaw = offset.x.atan2(offset.z); 50 let pitch = (offset.y / distance).asin(); 51 52 Self { 53 target: camera.target, 54 distance, 55 yaw, 56 pitch, 57 ..Default::default() 58 } 59 } 60 61 /// Handle mouse drag delta (in pixels). 62 pub fn on_drag(&mut self, delta_x: f32, delta_y: f32) { 63 self.yaw -= delta_x * self.sensitivity; 64 self.pitch += delta_y * self.sensitivity; 65 66 // Clamp pitch to avoid gimbal lock 67 let limit = std::f32::consts::FRAC_PI_2 - 0.01; 68 self.pitch = self.pitch.clamp(-limit, limit); 69 } 70 71 /// Handle scroll for zoom (positive = zoom in). 72 pub fn on_scroll(&mut self, delta: f32) { 73 self.distance *= 1.0 - delta * self.zoom_sensitivity; 74 self.distance = self.distance.clamp(self.min_distance, self.max_distance); 75 } 76 77 /// Compute the camera eye position from current orbit state. 78 pub fn eye(&self) -> Vec3 { 79 let x = self.distance * self.pitch.cos() * self.yaw.sin(); 80 let y = self.distance * self.pitch.sin(); 81 let z = self.distance * self.pitch.cos() * self.yaw.cos(); 82 self.target + Vec3::new(x, y, z) 83 } 84 85 /// Update a camera with the current arcball state. 86 pub fn update_camera(&self, camera: &mut Camera) { 87 camera.eye = self.eye(); 88 camera.target = self.target; 89 } 90 } 91 92 /// FPS-style fly camera controller for free movement through the scene. 93 #[derive(Debug, Clone)] 94 pub struct FlyController { 95 pub position: Vec3, 96 pub yaw: f32, // radians, around Y axis 97 pub pitch: f32, // radians, up/down 98 pub speed: f32, 99 pub sensitivity: f32, 100 } 101 102 impl Default for FlyController { 103 fn default() -> Self { 104 Self { 105 position: Vec3::new(0.0, 2.0, 5.0), 106 yaw: 0.0, 107 pitch: 0.0, 108 speed: 5.0, 109 sensitivity: 0.003, 110 } 111 } 112 } 113 114 impl FlyController { 115 /// Initialize from an existing camera. 116 pub fn from_camera(camera: &Camera) -> Self { 117 let dir = (camera.target - camera.eye).normalize(); 118 let yaw = dir.x.atan2(dir.z); 119 let pitch = dir.y.asin(); 120 121 Self { 122 position: camera.eye, 123 yaw, 124 pitch, 125 ..Default::default() 126 } 127 } 128 129 /// Handle mouse movement for looking around. 130 pub fn on_mouse_look(&mut self, delta_x: f32, delta_y: f32) { 131 self.yaw -= delta_x * self.sensitivity; 132 self.pitch -= delta_y * self.sensitivity; 133 134 let limit = std::f32::consts::FRAC_PI_2 - 0.01; 135 self.pitch = self.pitch.clamp(-limit, limit); 136 } 137 138 /// Forward direction (horizontal plane + pitch). 139 pub fn forward(&self) -> Vec3 { 140 Vec3::new( 141 self.pitch.cos() * self.yaw.sin(), 142 self.pitch.sin(), 143 self.pitch.cos() * self.yaw.cos(), 144 ) 145 .normalize() 146 } 147 148 /// Right direction (always horizontal). 149 pub fn right(&self) -> Vec3 { 150 Vec3::new(self.yaw.cos(), 0.0, -self.yaw.sin()).normalize() 151 } 152 153 /// Move the camera. forward/right/up are signed: positive = forward/right/up. 154 pub fn process_movement(&mut self, forward: f32, right: f32, up: f32, dt: f32) { 155 let velocity = self.speed * dt; 156 self.position += self.forward() * forward * velocity; 157 self.position += self.right() * right * velocity; 158 self.position += Vec3::Y * up * velocity; 159 } 160 161 /// Adjust speed with scroll wheel. 162 pub fn on_scroll(&mut self, delta: f32) { 163 self.speed *= 1.0 + delta * 0.1; 164 self.speed = self.speed.clamp(0.5, 100.0); 165 } 166 167 /// Update a camera with the current fly state. 168 pub fn update_camera(&self, camera: &mut Camera) { 169 camera.eye = self.position; 170 camera.target = self.position + self.forward(); 171 } 172 } 173 174 /// Third-person camera controller that orbits around a movable avatar. 175 /// 176 /// WASD moves the avatar on the ground plane (camera-relative). 177 /// Mouse drag orbits the camera around the avatar. 178 /// Scroll zooms in/out. 179 #[derive(Debug, Clone)] 180 pub struct ThirdPersonController { 181 /// Avatar world position (Y stays at ground level) 182 pub avatar_position: Vec3, 183 /// Avatar facing direction in radians (around Y axis) 184 pub avatar_yaw: f32, 185 /// Height offset for the camera look-at target above avatar_position 186 pub avatar_eye_height: f32, 187 188 /// Camera orbit distance from avatar 189 pub distance: f32, 190 /// Camera orbit yaw (horizontal angle around avatar) 191 pub yaw: f32, 192 /// Camera orbit pitch (vertical angle, positive = looking down) 193 pub pitch: f32, 194 195 /// Avatar movement speed (units per second) 196 pub speed: f32, 197 /// Mouse orbit sensitivity 198 pub sensitivity: f32, 199 /// Scroll zoom sensitivity 200 pub zoom_sensitivity: f32, 201 /// Minimum orbit distance 202 pub min_distance: f32, 203 /// Maximum orbit distance 204 pub max_distance: f32, 205 } 206 207 impl Default for ThirdPersonController { 208 fn default() -> Self { 209 Self { 210 avatar_position: Vec3::ZERO, 211 avatar_yaw: 0.0, 212 avatar_eye_height: 1.5, 213 distance: 8.0, 214 yaw: 0.0, 215 pitch: 0.4, 216 speed: 5.0, 217 sensitivity: 0.005, 218 zoom_sensitivity: 0.1, 219 min_distance: 2.0, 220 max_distance: 30.0, 221 } 222 } 223 } 224 225 impl ThirdPersonController { 226 /// Initialize from an existing camera, inferring orbit parameters. 227 pub fn from_camera(camera: &Camera) -> Self { 228 let offset = camera.eye - camera.target; 229 let distance = offset.length().max(2.0); 230 let yaw = offset.x.atan2(offset.z); 231 let pitch = (offset.y / distance).asin().max(0.05); 232 233 Self { 234 avatar_position: Vec3::new(camera.target.x, 0.0, camera.target.z), 235 avatar_eye_height: camera.target.y.max(1.0), 236 distance, 237 yaw, 238 pitch, 239 ..Default::default() 240 } 241 } 242 243 /// Handle mouse drag to orbit camera around avatar. 244 pub fn on_mouse_look(&mut self, delta_x: f32, delta_y: f32) { 245 self.yaw -= delta_x * self.sensitivity; 246 self.pitch += delta_y * self.sensitivity; 247 248 let limit = std::f32::consts::FRAC_PI_2 - 0.05; 249 self.pitch = self.pitch.clamp(0.05, limit); 250 } 251 252 /// Handle scroll to zoom in/out. 253 pub fn on_scroll(&mut self, delta: f32) { 254 self.distance *= 1.0 - delta * self.zoom_sensitivity; 255 self.distance = self.distance.clamp(self.min_distance, self.max_distance); 256 } 257 258 /// Camera forward direction projected onto the ground plane. 259 fn camera_forward_flat(&self) -> Vec3 { 260 Vec3::new(self.yaw.sin(), 0.0, self.yaw.cos()).normalize() 261 } 262 263 /// Camera right direction (always horizontal). 264 fn camera_right(&self) -> Vec3 { 265 Vec3::new(self.yaw.cos(), 0.0, -self.yaw.sin()).normalize() 266 } 267 268 /// Move avatar on the ground plane (camera-relative WASD). 269 /// `_up` is ignored -- avatar stays on the ground. 270 pub fn process_movement(&mut self, forward: f32, right: f32, _up: f32, dt: f32) { 271 let velocity = self.speed * dt; 272 let move_dir = self.camera_forward_flat() * forward + self.camera_right() * right; 273 274 if move_dir.length_squared() > 0.001 { 275 let move_dir = move_dir.normalize(); 276 self.avatar_position += move_dir * velocity; 277 self.avatar_yaw = move_dir.x.atan2(move_dir.z); 278 } 279 } 280 281 /// Camera look-at target (avatar position + eye height offset). 282 pub fn target(&self) -> Vec3 { 283 self.avatar_position + Vec3::new(0.0, self.avatar_eye_height, 0.0) 284 } 285 286 /// Compute camera eye position from orbit state. 287 pub fn eye(&self) -> Vec3 { 288 let target = self.target(); 289 let x = self.distance * self.pitch.cos() * self.yaw.sin(); 290 let y = self.distance * self.pitch.sin(); 291 let z = self.distance * self.pitch.cos() * self.yaw.cos(); 292 target + Vec3::new(x, y, z) 293 } 294 295 /// Update a Camera struct from current orbit + avatar state. 296 pub fn update_camera(&self, camera: &mut Camera) { 297 camera.eye = self.eye(); 298 camera.target = self.target(); 299 } 300 } 301 302 impl Camera { 303 pub fn new(eye: Vec3, target: Vec3) -> Self { 304 Self { 305 eye, 306 target, 307 up: Vec3::Y, 308 fov_y: 45_f32.to_radians(), 309 znear: 0.1, 310 zfar: 1000.0, 311 } 312 } 313 314 fn view(&self) -> Mat4 { 315 Mat4::look_at_rh(self.eye, self.target, self.up) 316 } 317 318 fn proj(&self, width: f32, height: f32) -> Mat4 { 319 let aspect = width / height.max(1.0); 320 Mat4::perspective_rh(self.fov_y, aspect, self.znear, self.zfar) 321 } 322 323 pub fn view_proj(&self, width: f32, height: f32) -> Mat4 { 324 self.proj(width, height) * self.view() 325 } 326 327 pub fn fit_to_aabb( 328 bounds_min: Vec3, 329 bounds_max: Vec3, 330 aspect: f32, 331 fov_y: f32, 332 padding: f32, 333 ) -> Self { 334 let center = (bounds_min + bounds_max) * 0.5; 335 let radius = ((bounds_max - bounds_min) * 0.5).length().max(1e-4); 336 337 // horizontal fov derived from vertical fov + aspect 338 let half_fov_y = fov_y * 0.5; 339 let half_fov_x = (half_fov_y.tan() * aspect).atan(); 340 341 // fit in both directions 342 let limiting_half_fov = half_fov_y.min(half_fov_x); 343 let dist = (radius / limiting_half_fov.tan()) * padding; 344 345 // choose a viewing direction 346 let view_dir = Vec3::new(0.0, 0.35, 1.0).normalize(); 347 let eye = center + view_dir * dist; 348 349 // near/far based on distance + radius 350 let znear = (dist - radius * 2.0).max(0.01); 351 let zfar = dist + radius * 50.0; 352 353 Self { 354 eye, 355 target: center, 356 up: Vec3::Y, 357 fov_y, 358 znear, 359 zfar, 360 } 361 } 362 }