notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }