Skip to main content

engvis_core/
camera.rs

1use glam::{Mat4, Vec3};
2use crate::scene::Scene;
3use crate::aabb::Aabb;
4
5#[derive(Debug, Clone)]
6pub struct OrbitCamera {
7    /// Point the camera orbits around
8    pub target: Vec3,
9    /// Azimuth angle (radians, around Y axis)
10    pub yaw: f32,
11    /// Elevation angle (radians, clamped)
12    pub pitch: f32,
13    /// Distance from target
14    pub distance: f32,
15    /// Vertical field of view (radians)
16    pub fov_y: f32,
17    /// Near clipping plane
18    pub near: f32,
19    /// Far clipping plane
20    pub far: f32,
21    /// Aspect ratio (width / height)
22    pub aspect_ratio: f32,
23}
24
25impl OrbitCamera {
26    pub fn new(target: Vec3, distance: f32) -> Self {
27        Self {
28            target,
29            yaw: -0.785, // -45 degrees
30            pitch: 0.615, // ~35 degrees
31            distance,
32            fov_y: std::f32::consts::FRAC_PI_4, // 45 degrees
33            near: 0.01,
34            far: 1000.0,
35            aspect_ratio: 16.0 / 9.0,
36        }
37    }
38
39    /// Target + distance variant (no pitch/yaw offset).
40    pub fn looking_at(target: Vec3, distance: f32) -> Self {
41        Self {
42            target,
43            yaw: 0.0,
44            pitch: 0.0,
45            distance,
46            ..Default::default()
47        }
48    }
49
50    /// Camera world-space position
51    pub fn position(&self) -> Vec3 {
52        let cos_pitch = self.pitch.cos();
53        let offset = Vec3::new(
54            self.distance * cos_pitch * self.yaw.sin(),
55            self.distance * self.pitch.sin(),
56            self.distance * cos_pitch * self.yaw.cos(),
57        );
58        self.target + offset
59    }
60
61    /// Right direction vector
62    pub fn right(&self) -> Vec3 {
63        let forward = (self.target - self.position()).normalize();
64        forward.cross(Vec3::Y).normalize()
65    }
66
67    /// Up direction vector
68    pub fn up(&self) -> Vec3 {
69        let forward = (self.target - self.position()).normalize();
70        let right = forward.cross(Vec3::Y).normalize();
71        right.cross(forward).normalize()
72    }
73
74    /// View matrix (world -> camera)
75    pub fn view_matrix(&self) -> Mat4 {
76        Mat4::look_at_rh(self.position(), self.target, Vec3::Y)
77    }
78
79    /// Projection matrix
80    pub fn projection_matrix(&self) -> Mat4 {
81        Mat4::perspective_rh(self.fov_y, self.aspect_ratio, self.near, self.far)
82    }
83
84    /// Combined view-projection matrix
85    pub fn view_projection(&self) -> Mat4 {
86        self.projection_matrix() * self.view_matrix()
87    }
88
89    /// Orbit by delta yaw/pitch
90    pub fn orbit(&mut self, delta_yaw: f32, delta_pitch: f32) {
91        self.yaw += delta_yaw;
92        self.pitch = (self.pitch + delta_pitch).clamp(
93            -std::f32::consts::FRAC_PI_2 + 0.01,
94            std::f32::consts::FRAC_PI_2 - 0.01,
95        );
96    }
97
98    /// Pan the target point in camera-local XY
99    pub fn pan(&mut self, delta_x: f32, delta_y: f32) {
100        let right = self.right();
101        let up = self.up();
102        self.target += right * delta_x + up * delta_y;
103    }
104
105    /// Zoom (change distance)
106    pub fn zoom(&mut self, delta: f32) {
107        self.distance *= 1.0 - delta;
108        self.distance = self.distance.clamp(0.1, 500.0);
109    }
110
111    /// Fit camera to show a bounding box, adjusting near/far automatically.
112    pub fn fit_to_aabb(&mut self, aabb: Aabb) {
113        if !aabb.is_valid() {
114            return;
115        }
116        self.target = aabb.center();
117        let radius = aabb.diagonal() * 0.5;
118        let min_dist = (radius / (self.fov_y * 0.5).sin()).max(0.5);
119        self.distance = min_dist * 1.4; // +40% margin
120        self.near = (min_dist - radius * 2.0).max(0.01);
121        self.far = (min_dist + radius * 4.0).max(10.0);
122    }
123
124    /// Fit camera to show an entire scene.
125    pub fn fit_to_scene(&mut self, scene: &Scene) {
126        self.fit_to_aabb(scene.compute_aabb());
127    }
128
129    /// Preset: front view
130    pub fn view_front(&mut self) {
131        self.yaw = 0.0;
132        self.pitch = 0.0;
133    }
134
135    /// Preset: top view
136    pub fn view_top(&mut self) {
137        self.yaw = 0.0;
138        self.pitch = std::f32::consts::FRAC_PI_2 - 0.01;
139    }
140
141    /// Preset: right view
142    pub fn view_right(&mut self) {
143        self.yaw = -std::f32::consts::FRAC_PI_2;
144        self.pitch = 0.0;
145    }
146
147    /// Preset: isometric view
148    pub fn view_iso(&mut self) {
149        self.yaw = -0.785;
150        self.pitch = 0.615;
151    }
152}
153
154impl Default for OrbitCamera {
155    fn default() -> Self {
156        Self::new(Vec3::ZERO, 5.0)
157    }
158}