gizmo_renderer/components/
camera.rs1use gizmo_math::Vec3;
2
3#[non_exhaustive]
5#[derive(Clone, Copy, Debug, PartialEq, Default, serde::Serialize, serde::Deserialize)]
6pub enum ProjectionMode {
7 #[default]
9 Perspective,
10 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, pub primary: bool,
24 #[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, primary,
56 projection: ProjectionMode::Perspective,
57 }
58 }
59
60 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 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 Vec3::new(-self.yaw.sin(), 0.0, self.yaw.cos())
117 }
118
119 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 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 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 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 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}