Skip to main content

gizmo_renderer/components/
camera.rs

1use gizmo_math::Vec3;
2
3/// How a [`Camera`] projects the scene onto the screen.
4#[non_exhaustive]
5#[derive(Clone, Copy, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize)]
6pub enum ProjectionMode {
7    /// Perspective projection (the default), using the camera's `fov`.
8    #[default]
9    Perspective,
10    /// Orthographic projection. `height` is the vertical extent of the view
11    /// volume in world units; the width is derived from the aspect ratio.
12    Orthographic { height: f32 },
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
16pub struct Camera {
17    pub fov: f32,
18    pub near: f32,
19    pub far: f32,
20    pub yaw: f32,
21    pub pitch: f32,
22    pub exposure: f32, // Fiziksel kamera pozlaması (EV tabanlı veya doğrudan çarpan)
23    pub primary: bool,
24    /// Perspective (default) or orthographic projection. `#[serde(default)]` keeps
25    /// scenes saved before this field was added loading as perspective.
26    #[serde(default)]
27    pub projection: ProjectionMode,
28}
29
30impl Camera {
31    pub fn new(
32        mut fov: f32,
33        mut near: f32,
34        mut far: f32,
35        mut yaw: f32,
36        mut pitch: f32,
37        primary: bool,
38    ) -> Self {
39        fov = fov.max(0.001);
40        near = near.max(0.001);
41        far = far.max(near + 0.1);
42        yaw %= std::f32::consts::TAU;
43        pitch = pitch.clamp(
44            -std::f32::consts::PI / 2.0 + 0.001,
45            std::f32::consts::PI / 2.0 - 0.001,
46        );
47
48        Self {
49            fov,
50            near,
51            far,
52            yaw,
53            pitch,
54            exposure: 1.0, // Varsayılan pozlama 1.0
55            primary,
56            projection: ProjectionMode::Perspective,
57        }
58    }
59
60    /// Toggles between perspective and orthographic projection. When switching to
61    /// orthographic, the vertical extent is chosen so the framing roughly matches
62    /// the current perspective `fov` at the given `distance` from the camera.
63    pub fn toggle_projection(&mut self, distance: f32) {
64        self.projection = match self.projection {
65            ProjectionMode::Perspective => ProjectionMode::Orthographic {
66                height: 2.0 * distance.abs().max(0.001) * (self.fov * 0.5).tan(),
67            },
68            ProjectionMode::Orthographic { .. } => ProjectionMode::Perspective,
69        };
70    }
71
72    /// Fazla birikmeyi önlemek icin acilari temizler (yaw mod TAU, pitch clamp)
73    pub fn sanitize_angles(&mut self) {
74        self.yaw %= std::f32::consts::TAU;
75        self.pitch = self.pitch.clamp(
76            -std::f32::consts::PI / 2.0 + 0.001,
77            std::f32::consts::PI / 2.0 - 0.001,
78        );
79    }
80
81    pub fn get_projection(&self, aspect: f32) -> gizmo_math::Mat4 {
82        match self.projection {
83            ProjectionMode::Perspective => {
84                gizmo_math::Mat4::perspective_rh(self.fov, aspect, self.near, self.far)
85            }
86            ProjectionMode::Orthographic { height } => {
87                let half_h = (height * 0.5).max(0.001);
88                let half_w = half_h * aspect.max(0.001);
89                gizmo_math::Mat4::orthographic_rh(
90                    -half_w, half_w, -half_h, half_h, self.near, self.far,
91                )
92            }
93        }
94    }
95
96    pub fn get_view(&self, position: Vec3) -> gizmo_math::Mat4 {
97        let front = self.get_front();
98        let right = self.get_right();
99        let up = right.cross(front);
100        gizmo_math::Mat4::look_at_rh(position, position + front, up)
101    }
102
103    pub fn get_front(&self) -> Vec3 {
104        let pitch = self.pitch.clamp(
105            -std::f32::consts::PI / 2.0 + 0.001,
106            std::f32::consts::PI / 2.0 - 0.001,
107        );
108        let fx = self.yaw.cos() * pitch.cos();
109        let fy = pitch.sin();
110        let fz = self.yaw.sin() * pitch.cos();
111        Vec3::new(fx, fy, fz).normalize()
112    }
113
114    pub fn get_right(&self) -> Vec3 {
115        // Front x (0,1,0) reduces mathematically to (-sin(yaw), 0, cos(yaw))
116        Vec3::new(-self.yaw.sin(), 0.0, self.yaw.cos())
117    }
118
119    /// Build a world-space picking ray from a screen/cursor pixel through this
120    /// camera — the engine's screen→world unproject (à la Bevy's
121    /// `Camera::viewport_to_world`). Combine with `PhysicsWorld::raycast` (or a
122    /// plane intersection) to pick / drag the object under the cursor.
123    ///
124    /// * `screen` — cursor position in pixels, origin **top-left** (matches
125    ///   [`gizmo_core`]'s `Input::mouse_position`).
126    /// * `viewport` — framebuffer size in the same pixels (e.g. `WindowInfo`).
127    /// * `world_pos` — the camera's world position (its `Transform.position`,
128    ///   since the view matrix takes the position separately).
129    ///
130    /// The heavy lifting (NDC → world via the inverse view-projection, with
131    /// singular-matrix / degenerate-direction guards) is [`gizmo_math::Ray::from_ndc`].
132    pub fn screen_to_ray(
133        &self,
134        screen: (f32, f32),
135        viewport: (f32, f32),
136        world_pos: Vec3,
137    ) -> gizmo_math::Ray {
138        let (w, h) = (viewport.0.max(1.0), viewport.1.max(1.0));
139        // Pixel → NDC: x∈[-1,1] rightward, y∈[-1,1] UPward (flip the top-left screen y).
140        let ndc = gizmo_math::Vec2::new((screen.0 / w) * 2.0 - 1.0, 1.0 - (screen.1 / h) * 2.0);
141        let view_proj_inv = (self.get_projection(w / h) * self.get_view(world_pos)).inverse();
142        gizmo_math::Ray::from_ndc(ndc, view_proj_inv)
143    }
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
147pub struct Camera2D {
148    pub zoom: f32,
149    pub primary: bool,
150}
151
152impl Camera2D {
153    pub fn new(zoom: f32, primary: bool) -> Self {
154        Self { zoom, primary }
155    }
156
157    pub fn get_projection(&self, width: f32, height: f32) -> gizmo_math::Mat4 {
158        let safe_zoom = self.zoom.max(0.001);
159        let hw = (width / 2.0) / safe_zoom;
160        let hh = (height / 2.0) / safe_zoom;
161        gizmo_math::Mat4::orthographic_rh(-hw, hw, -hh, hh, -1000.0, 1000.0)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn screen_to_ray_center_points_along_camera_front() {
171        // Camera at (0,0,10) looking down -Z (yaw = -90°, pitch = 0 → front = (0,0,-1)).
172        let cam = Camera::new(
173            std::f32::consts::FRAC_PI_2,
174            0.1,
175            100.0,
176            -std::f32::consts::FRAC_PI_2,
177            0.0,
178            true,
179        );
180        let pos = Vec3::new(0.0, 0.0, 10.0);
181        // Centre pixel of a 200x100 viewport → NDC (0,0) → ray along the camera front.
182        let ray = cam.screen_to_ray((100.0, 50.0), (200.0, 100.0), pos);
183        assert!((ray.direction.z - (-1.0)).abs() < 1e-4, "centre ray looks -Z, got {:?}", ray.direction);
184        assert!(ray.direction.x.abs() < 1e-4 && ray.direction.y.abs() < 1e-4);
185        assert!((ray.direction.length() - 1.0).abs() < 1e-5, "direction normalized");
186    }
187
188    #[test]
189    fn screen_to_ray_offset_pixels_tilt_the_ray() {
190        let cam = Camera::new(
191            std::f32::consts::FRAC_PI_2,
192            0.1,
193            100.0,
194            -std::f32::consts::FRAC_PI_2,
195            0.0,
196            true,
197        );
198        let pos = Vec3::new(0.0, 0.0, 10.0);
199        // Right of centre → ray tilts +X; below centre (larger screen-y) → ray tilts -Y.
200        let right = cam.screen_to_ray((150.0, 50.0), (200.0, 100.0), pos);
201        assert!(right.direction.x > 0.05, "right pixel tilts +X, got {:?}", right.direction);
202        let down = cam.screen_to_ray((100.0, 90.0), (200.0, 100.0), pos);
203        assert!(down.direction.y < -0.05, "lower pixel tilts -Y, got {:?}", down.direction);
204    }
205}