1use core::ops::Range;
4
5#[cfg(feature = "fp")]
6use crate::math::{
7 Angle, Vec3, orient_z, rotate_x, rotate_y, spherical, translate, turns,
8};
9use crate::math::{
10 Apply, Lerp, Mat4x4, Point3, SphericalVec, Vary, mat::RealToReal,
11 orthographic, perspective, pt2, viewport,
12};
13use crate::util::{Dims, rect::Rect};
14
15use super::{
16 Clip, Context, NdcToScreen, RealToProj, Render, Shader, Target, View,
17 ViewToProj, World, WorldToView,
18};
19
20pub trait Transform {
22 fn world_to_view(&self) -> Mat4x4<WorldToView>;
24}
25
26#[derive(Copy, Clone, Debug, PartialEq)]
31pub enum Fov {
32 FocalRatio(f32),
38 Equiv35mm(f32),
45 #[cfg(feature = "fp")]
47 Horizontal(Angle),
48 #[cfg(feature = "fp")]
50 Vertical(Angle),
51 #[cfg(feature = "fp")]
53 Diagonal(Angle),
54}
55
56#[derive(Copy, Clone, Debug, Default)]
58pub struct Camera<Tf> {
59 pub transform: Tf,
61 pub dims: Dims,
63 pub project: Mat4x4<ViewToProj>,
65 pub viewport: Mat4x4<NdcToScreen>,
67}
68
69#[derive(Copy, Clone, Debug)]
74pub struct FirstPerson {
75 pub pos: Point3<World>,
77 pub heading: SphericalVec<World>,
79}
80
81pub type ViewToWorld = RealToReal<3, View, World>;
82
83#[cfg(feature = "fp")]
85fn az_alt<B>(az: Angle, alt: Angle) -> SphericalVec<B> {
86 spherical(1.0, az, alt)
87}
88#[derive(Copy, Clone, Debug)]
94pub struct Orbit {
95 pub target: Point3<World>,
97 pub dir: SphericalVec<World>,
99}
100
101impl Fov {
106 pub fn focal_ratio(self, aspect_ratio: f32) -> f32 {
108 use Fov::*;
109 #[cfg(feature = "fp")]
110 fn ratio(a: Angle) -> f32 {
111 1.0 / (a / 2.0).tan()
112 }
113 match self {
114 FocalRatio(r) => r,
115 Equiv35mm(mm) => mm / (36.0 / 2.0), #[cfg(feature = "fp")]
118 Horizontal(a) => ratio(a),
119
120 #[cfg(feature = "fp")]
121 Vertical(a) => ratio(a) / aspect_ratio,
122
123 #[cfg(feature = "fp")]
124 Diagonal(a) => {
125 use crate::math::float::f32;
126 let diag = f32::sqrt(1.0 + 1.0 / aspect_ratio / aspect_ratio);
127 ratio(a) * diag
128 }
129 }
130 }
131}
132
133impl Camera<()> {
134 pub fn new(dims: Dims) -> Self {
136 Self {
137 dims,
138 viewport: viewport(pt2(0, 0)..pt2(dims.0, dims.1)),
139 ..Self::default()
140 }
141 }
142
143 pub fn transform<T: Transform>(self, tf: T) -> Camera<T> {
145 let Self { dims, project, viewport, .. } = self;
146 Camera {
147 transform: tf,
148 dims,
149 project,
150 viewport,
151 }
152 }
153}
154
155impl<T> Camera<T> {
156 pub fn viewport(self, bounds: impl Into<Rect<u32>>) -> Self {
158 let (w, h) = self.dims;
159
160 let Rect {
161 left: Some(l),
162 top: Some(t),
163 right: Some(r),
164 bottom: Some(b),
165 } = bounds.into().intersect(&(0..w, 0..h).into())
166 else {
167 unreachable!("bounded ∩ bounded should be bounded")
168 };
169
170 Self {
171 dims: (r.abs_diff(l), b.abs_diff(t)),
172 viewport: viewport(pt2(l, t)..pt2(r, b)),
173 ..self
174 }
175 }
176
177 pub fn perspective(mut self, fov: Fov, near_far: Range<f32>) -> Self {
187 let aspect = self.dims.0 as f32 / self.dims.1 as f32;
188
189 self.project = perspective(fov.focal_ratio(aspect), aspect, near_far);
190 self
191 }
192
193 pub fn orthographic(mut self, bounds: Range<Point3>) -> Self {
195 self.project = orthographic(bounds.start, bounds.end);
196 self
197 }
198}
199
200impl<T: Transform> Camera<T> {
201 pub fn world_to_project(&self) -> Mat4x4<RealToProj<World>> {
203 self.transform.world_to_view().then(&self.project)
204 }
205
206 pub fn render<B, Prim, Vtx: Clone, Var: Lerp + Vary, Uni: Copy, Shd>(
208 &self,
209 prims: impl AsRef<[Prim]>,
210 verts: impl AsRef<[Vtx]>,
211 to_world: &Mat4x4<RealToReal<3, B, World>>,
212 shader: &Shd,
213 uniform: Uni,
214 target: &mut impl Target,
215 ctx: &Context,
216 ) where
217 Prim: Render<Var> + Clone,
218 [<Prim>::Clip]: Clip<Item = Prim::Clip>,
219 Shd: for<'a> Shader<Vtx, Var, (&'a Mat4x4<RealToProj<B>>, Uni)>,
220 {
221 let tf = to_world.then(&self.world_to_project());
222
223 super::render(
224 prims.as_ref(),
225 verts.as_ref(),
226 shader,
227 (&tf, uniform),
228 self.viewport,
229 target,
230 ctx,
231 );
232 }
233}
234
235#[cfg(feature = "fp")]
236impl FirstPerson {
237 pub fn new() -> Self {
240 Self {
241 pos: Point3::origin(),
242 heading: az_alt(turns(0.0), turns(0.0)),
243 }
244 }
245
246 pub fn look_at(&mut self, pt: Point3<World>) {
248 let head = (pt - self.pos).to_spherical();
249 self.rotate_to(head.az(), head.alt());
250 }
251
252 pub fn rotate(&mut self, delta_az: Angle, delta_alt: Angle) {
254 let head = self.heading;
255 self.rotate_to(head.az() + delta_az, head.alt() + delta_alt);
256 }
257
258 pub fn rotate_to(&mut self, az: Angle, alt: Angle) {
261 self.heading = az_alt(
262 az.wrap(turns(-0.5), turns(0.5)),
263 alt.clamp(turns(-0.25), turns(0.25)),
264 );
265 }
266
267 pub fn translate(&mut self, delta: Vec3<View>) {
270 let fwd = az_alt(self.heading.az(), turns(0.0)).to_cart();
272 let up = Vec3::Y;
273 let right = up.cross(&fwd);
274
275 let to_world = Mat4x4::from_linear(right, up, fwd);
276 self.pos += to_world.apply(&delta);
277 }
278}
279
280#[cfg(feature = "fp")]
281impl Orbit {
282 pub fn rotate(&mut self, az_delta: Angle, alt_delta: Angle) {
286 self.rotate_to(self.dir.az() + az_delta, self.dir.alt() + alt_delta);
287 }
288
289 pub fn rotate_to(&mut self, az: Angle, alt: Angle) {
293 self.dir = spherical(
294 self.dir.r(),
295 az.wrap(turns(-0.5), turns(0.5)),
296 alt.clamp(turns(-0.25), turns(0.25)),
297 );
298 }
299
300 pub fn translate(&mut self, delta: Vec3<World>) {
302 self.target += delta;
303 }
304
305 pub fn zoom(&mut self, factor: f32) {
316 assert!(factor >= 0.0, "zoom factor cannot be negative");
317 self.zoom_to(self.dir.r() * factor);
318 }
319 pub fn zoom_to(&mut self, r: f32) {
324 assert!(r >= 0.0, "camera distance cannot be negative");
325 self.dir[0] = r.max(0.0);
326 }
327}
328
329#[cfg(feature = "fp")]
334impl Transform for FirstPerson {
335 fn world_to_view(&self) -> Mat4x4<WorldToView> {
336 let &Self { pos, heading, .. } = self;
337 let fwd_move = az_alt(heading.az(), turns(0.0)).to_cart();
338 let fwd = heading.to_cart();
339 let right = Vec3::Y.cross(&fwd_move);
340
341 let transl = translate(-pos.to_vec().to());
343 let orient = orient_z(fwd.to(), right).transpose();
344
345 transl.then(&orient).to()
346 }
347}
348
349#[cfg(feature = "fp")]
350impl Transform for Orbit {
351 fn world_to_view(&self) -> Mat4x4<WorldToView> {
352 translate(self.target.to_vec().to()) .then(&rotate_y(self.dir.az())) .then(&rotate_x(self.dir.alt())) .then(&translate(self.dir.r() * Vec3::Z)) .to()
363 }
364}
365
366impl Transform for Mat4x4<WorldToView> {
367 fn world_to_view(&self) -> Mat4x4<WorldToView> {
368 *self
369 }
370}
371
372#[cfg(feature = "fp")]
377impl Default for FirstPerson {
378 fn default() -> Self {
380 Self::new()
381 }
382}
383
384#[cfg(feature = "fp")]
385impl Default for Orbit {
386 fn default() -> Self {
387 Self {
388 target: Point3::default(),
389 dir: az_alt(turns(0.0), turns(0.0)),
390 }
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 use Fov::*;
399
400 #[test]
401 fn camera_tests_here() {
402 }
404
405 #[test]
406 fn fov_focal_ratio() {
407 assert_eq!(FocalRatio(2.345).focal_ratio(1.0), 2.345);
408 assert_eq!(FocalRatio(2.345).focal_ratio(2.0), 2.345);
409
410 assert_eq!(Equiv35mm(18.0).focal_ratio(1.0), 1.0);
411 assert_eq!(Equiv35mm(36.0).focal_ratio(1.5), 2.0);
412 }
413
414 #[cfg(feature = "fp")]
415 #[test]
416 fn angle_of_view_focal_ratio_with_unit_aspect_ratio() {
417 use crate::math::degs;
418 use core::f32::consts::SQRT_2;
419 const SQRT_3: f32 = 1.7320509;
420
421 assert_eq!(Horizontal(degs(60.0)).focal_ratio(1.0), SQRT_3);
422 assert_eq!(Vertical(degs(60.0)).focal_ratio(1.0), SQRT_3);
423 assert_eq!(Diagonal(degs(60.0)).focal_ratio(1.0), SQRT_3 * SQRT_2);
424 }
425
426 #[cfg(feature = "fp")]
427 #[test]
428 fn angle_of_view_focal_ratio_with_other_aspect_ratio() {
429 use crate::math::degs;
430 const SQRT_3: f32 = 1.7320509;
431
432 assert_eq!(Horizontal(degs(60.0)).focal_ratio(SQRT_3), SQRT_3);
433 assert_eq!(Vertical(degs(60.0)).focal_ratio(SQRT_3), 1.0);
434 assert_eq!(Diagonal(degs(60.0)).focal_ratio(SQRT_3), 2.0);
435 }
436}