1use glam::{DMat4, DVec3, dvec2};
4
5use crate::{
6 Extract,
7 core_item::CoreItem,
8 prelude::{Alignable, Interpolatable},
9 traits::ApplyTransform,
10};
11
12#[derive(bevy_ecs::component::Component, Clone, Debug, PartialEq)]
17pub struct CameraFrame {
18 pub pos: DVec3,
20 pub up: DVec3,
22 pub facing: DVec3,
24
25 pub near: f64,
28 pub far: f64,
30 pub perspective_blend: f64,
32
33 pub frame_height: f64,
35 pub scale: f64,
37
38 pub fovy: f64,
40}
41
42impl Extract for CameraFrame {
43 type Target = CoreItem;
44 fn extract_into(&self, buf: &mut Vec<Self::Target>) {
45 buf.push(CoreItem::CameraFrame(self.clone()));
46 }
47}
48
49impl Interpolatable for CameraFrame {
50 fn lerp(&self, target: &Self, t: f64) -> Self {
51 Self {
52 pos: self.pos.lerp(target.pos, t),
53 up: self.up.lerp(target.up, t),
54 facing: self.facing.lerp(target.facing, t),
55 scale: self.scale.lerp(&target.scale, t),
56 fovy: self.fovy.lerp(&target.fovy, t),
57 near: self.near.lerp(&target.near, t),
58 far: self.far.lerp(&target.far, t),
59 frame_height: self.frame_height.lerp(&target.frame_height, t),
60 perspective_blend: self
61 .perspective_blend
62 .lerp(&target.perspective_blend, t)
63 .clamp(0.0, 1.0),
64 }
65 }
66}
67
68impl Alignable for CameraFrame {
69 fn is_aligned(&self, _other: &Self) -> bool {
70 true
71 }
72 fn align_with(&mut self, _other: &mut Self) {}
73}
74
75impl<G: Into<glam::DAffine3>> ApplyTransform<G> for CameraFrame {
76 fn apply(&mut self, transform: G) -> &mut Self {
79 let transform = transform.into();
80 self.pos = transform.transform_point3(self.pos);
81 self.up = transform.transform_vector3(self.up).normalize();
82 self.facing = transform.transform_vector3(self.facing).normalize();
83 self
84 }
85}
86
87impl Default for CameraFrame {
88 fn default() -> Self {
89 Self {
90 pos: DVec3::ZERO,
91 up: DVec3::Y,
92 facing: DVec3::NEG_Z,
93
94 near: -1000.0,
95 far: 1000.0,
96 perspective_blend: 0.0,
97
98 scale: 1.0,
99 frame_height: 8.0,
100
101 fovy: std::f64::consts::PI / 2.0,
102 }
103 }
104}
105
106impl CameraFrame {
107 pub fn new() -> Self {
109 Self::default()
110 }
111}
112
113impl CameraFrame {
114 pub fn set_view_matrix(&mut self, view_matrix: DMat4) {
116 let inv = view_matrix.inverse();
117 self.pos = inv.transform_point3(DVec3::ZERO);
118 self.up = inv.transform_vector3(DVec3::Y).normalize();
119 self.facing = inv.transform_vector3(DVec3::NEG_Z).normalize();
120 }
121
122 pub fn with_view_matrix(mut self, view_matrix: DMat4) -> Self {
124 self.set_view_matrix(view_matrix);
125 self
126 }
127
128 pub fn view_matrix(&self) -> DMat4 {
130 glam::dcamera::rh::view::look_to_mat4(self.pos, self.facing, self.up)
131 }
132
133 pub fn orthographic_mat(&self, aspect_ratio: f64) -> DMat4 {
135 let frame_size = dvec2(self.frame_height * aspect_ratio, self.frame_height);
136 let frame_size = frame_size * self.scale;
137 glam::dcamera::rh::proj::directx::orthographic(
138 -frame_size.x / 2.0,
139 frame_size.x / 2.0,
140 -frame_size.y / 2.0,
141 frame_size.y / 2.0,
142 self.near,
143 self.far,
144 )
145 }
146
147 pub fn perspective_mat(&self, aspect_ratio: f64) -> DMat4 {
149 let near = self.near.max(0.1);
150 let far = self.far.max(near);
151 glam::dcamera::rh::proj::directx::perspective(self.fovy, aspect_ratio, near, far)
152 }
153
154 pub fn projection_matrix(&self, aspect_ratio: f64) -> DMat4 {
156 self.orthographic_mat(aspect_ratio)
157 .lerp(&self.perspective_mat(aspect_ratio), self.perspective_blend)
158 }
159
160 pub fn view_projection_matrix(&self, aspect_ratio: f64) -> DMat4 {
162 self.projection_matrix(aspect_ratio) * self.view_matrix()
163 }
164}
165
166impl CameraFrame {
167 pub fn from_spherical(phi: f64, theta: f64, distance: f64) -> Self {
173 let mut cam = Self {
174 perspective_blend: 1.0,
175 up: DVec3::Z,
176 ..Self::default()
177 };
178 cam.set_spherical(phi, theta, distance, DVec3::ZERO);
179 cam
180 }
181
182 pub fn set_spherical(
189 &mut self,
190 phi: f64,
191 theta: f64,
192 distance: f64,
193 target: DVec3,
194 ) -> &mut Self {
195 self.pos = target
196 + DVec3::new(
197 distance * phi.sin() * theta.cos(),
198 distance * phi.sin() * theta.sin(),
199 distance * phi.cos(),
200 );
201 self.facing = (target - self.pos).normalize();
202 self.up = DVec3::Z;
203 self
204 }
205
206 pub fn look_at(&mut self, target: DVec3) -> &mut Self {
208 self.facing = (target - self.pos).normalize();
209 self
210 }
211}
212
213impl CameraFrame {
214 pub fn center_canvas_in_frame(
216 &mut self,
217 center: DVec3,
218 width: f64,
219 height: f64,
220 up: DVec3,
221 normal: DVec3,
222 aspect_ratio: f64,
223 ) -> &mut Self {
224 let canvas_ratio = height / width;
225 let up = up.normalize();
226 let normal = normal.normalize();
227
228 let height = if aspect_ratio > canvas_ratio {
229 height
230 } else {
231 width / aspect_ratio
232 };
233
234 let distance = height * 0.5 / (0.5 * self.fovy).tan();
235
236 self.up = up;
237 self.pos = center + normal * distance;
238 self.facing = -normal;
239 self
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use glam::dvec3;
247
248 #[test]
249 fn set_view_matrix_round_trips_pos_up_and_facing() {
250 let cases = [
251 CameraFrame::new(),
252 {
253 let mut camera = CameraFrame::new();
254 camera.pos = dvec3(5.0, 3.0, -2.0);
255 camera
256 },
257 {
258 let mut camera = CameraFrame::new();
259 camera.facing = dvec3(1.0, 0.0, 0.0);
260 camera.up = dvec3(0.0, 1.0, 0.0);
261 camera
262 },
263 {
264 let mut camera = CameraFrame::new();
265 camera.pos = dvec3(10.0, 5.0, 3.0);
266 camera.facing = dvec3(1.0, 0.0, 1.0).normalize();
267 camera.up = dvec3(0.0, 1.0, 0.0);
268 camera
269 },
270 ];
271
272 for (i, camera) in cases.into_iter().enumerate() {
273 let mut restored = CameraFrame::new();
274 restored.set_view_matrix(camera.view_matrix());
275
276 assert!(restored.pos.distance(camera.pos) < 1e-10, "case {i}: pos");
277 assert!(restored.up.angle_between(camera.up) < 1e-10, "case {i}: up");
278 assert!(
279 restored.facing.angle_between(camera.facing) < 1e-10,
280 "case {i}: facing"
281 );
282 }
283 }
284}