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