retrofire_core/render/
cam.rs

1//! Cameras and camera transforms.
2
3use core::ops::Range;
4
5use crate::geom::{Tri, Vertex};
6use crate::math::{
7    angle::{spherical, turns, SphericalVec},
8    mat::{orthographic, perspective, viewport, Mat4x4, RealToReal},
9    space::Linear,
10    vary::Vary,
11    vec::{vec2, Vec3},
12};
13use crate::util::{rect::Rect, Dims};
14
15#[cfg(feature = "fp")]
16use crate::math::{
17    angle::Angle,
18    mat::{orient_z, translate},
19    vec::vec3,
20};
21
22use super::{
23    clip::ClipVec,
24    ctx::Context,
25    shader::{FragmentShader, VertexShader},
26    target::Target,
27    NdcToScreen, RealToProj, ViewToProj, World, WorldToView,
28};
29
30/// Camera movement mode.
31///
32/// TODO Rename to something more specific (e.g. `Motion`?)
33pub trait Mode {
34    /// Returns the current world-to-view matrix of this camera mode.
35    fn world_to_view(&self) -> Mat4x4<WorldToView>;
36}
37
38/// Type to manage the world-to-viewport transformation.
39#[derive(Copy, Clone, Debug, Default)]
40pub struct Camera<M> {
41    /// The movement mode of the camera.
42    pub mode: M,
43    /// Viewport width and height.
44    pub dims: Dims,
45    /// Projection matrix.
46    pub project: Mat4x4<ViewToProj>,
47    /// Viewport matrix.
48    pub viewport: Mat4x4<NdcToScreen>,
49}
50
51/// First-person camera mode.
52///
53/// This is the familiar "FPS" movement mode, based on camera
54/// position and heading (look-at vector).
55#[derive(Copy, Clone, Debug)]
56pub struct FirstPerson {
57    /// Current position of the camera in world space.
58    pub pos: Vec3,
59    /// Current heading of the camera in world space.
60    pub heading: SphericalVec,
61}
62
63//
64// Inherent impls
65//
66
67impl Camera<()> {
68    /// Creates a camera with the given resolution.
69    pub fn new(dims: Dims) -> Self {
70        Self {
71            dims,
72            viewport: viewport(vec2(0, 0)..vec2(dims.0, dims.1)),
73            ..Default::default()
74        }
75    }
76
77    pub fn mode<M: Mode>(self, mode: M) -> Camera<M> {
78        let Self { dims, project, viewport, .. } = self;
79        Camera { mode, dims, project, viewport }
80    }
81}
82
83impl<M> Camera<M> {
84    /// Sets the viewport bounds of this camera.
85    pub fn viewport(self, bounds: impl Into<Rect<u32>>) -> Self {
86        let (w, h) = self.dims;
87
88        let Rect {
89            left: Some(l),
90            top: Some(t),
91            right: Some(r),
92            bottom: Some(b),
93        } = bounds.into().intersect(&(0..w, 0..h).into())
94        else {
95            unreachable!("bounded ∩ bounded should be bounded")
96        };
97
98        Self {
99            dims: (r.abs_diff(l), b.abs_diff(t)),
100            viewport: viewport(vec2(l, t)..vec2(r, b)),
101            ..self
102        }
103    }
104
105    /// Sets up perspective projection.
106    pub fn perspective(
107        mut self,
108        focal_ratio: f32,
109        near_far: Range<f32>,
110    ) -> Self {
111        let aspect_ratio = self.dims.0 as f32 / self.dims.1 as f32;
112        self.project = perspective(focal_ratio, aspect_ratio, near_far);
113        self
114    }
115
116    /// Sets up orthographic projection.
117    pub fn orthographic(mut self, bounds: Range<Vec3>) -> Self {
118        self.project = orthographic(bounds.start, bounds.end);
119        self
120    }
121}
122
123impl<M: Mode> Camera<M> {
124    /// Returns the composed camera and projection matrix.
125    pub fn world_to_project(&self) -> Mat4x4<RealToProj<World>> {
126        self.mode.world_to_view().then(&self.project)
127    }
128
129    /// Renders the given geometry from the viewpoint of this camera.
130    pub fn render<B, Vtx: Clone, Var: Vary, Uni: Copy, Shd>(
131        &self,
132        tris: impl AsRef<[Tri<usize>]>,
133        verts: impl AsRef<[Vtx]>,
134        to_world: &Mat4x4<RealToReal<3, B, World>>,
135        shader: &Shd,
136        uniform: Uni,
137        target: &mut impl Target,
138        ctx: &Context,
139    ) where
140        Shd: for<'a> VertexShader<
141                Vtx,
142                (&'a Mat4x4<RealToProj<B>>, Uni),
143                Output = Vertex<ClipVec, Var>,
144            > + FragmentShader<Var>,
145    {
146        let tf = to_world.then(&self.world_to_project());
147
148        super::render(
149            tris.as_ref(),
150            verts.as_ref(),
151            shader,
152            (&tf, uniform),
153            self.viewport,
154            target,
155            ctx,
156        );
157    }
158}
159
160impl FirstPerson {
161    /// Creates a first-person mode with position in the origin and heading
162    /// in the direction of the positive x-axis.
163    pub fn new() -> Self {
164        Self {
165            pos: Vec3::zero(),
166            heading: spherical(1.0, turns(0.0), turns(0.0)),
167        }
168    }
169}
170
171#[cfg(feature = "fp")]
172impl FirstPerson {
173    pub fn look_at(&mut self, pt: Vec3) {
174        self.heading = (pt - self.pos).into();
175        self.heading[0] = 1.0;
176    }
177
178    pub fn rotate(&mut self, az: Angle, alt: Angle) {
179        self.rotate_to(self.heading.az() + az, self.heading.alt() + alt);
180    }
181
182    pub fn rotate_to(&mut self, az: Angle, alt: Angle) {
183        self.heading = spherical(
184            1.0,
185            az.wrap(turns(-0.5), turns(0.5)),
186            alt.clamp(turns(-0.25), turns(0.25)),
187        );
188    }
189
190    pub fn translate(&mut self, delta: Vec3) {
191        // Zero azimuth means parallel to the x-axis
192        let fwd = spherical(1.0, self.heading.az(), turns(0.0)).to_cart();
193        let up = vec3(0.0, 1.0, 0.0);
194        let right = up.cross(&fwd);
195
196        // / rx ux fx \ / dx \     / rx ry rz \ T / dx \
197        // | ry uy fy | | dy |  =  | ux uy uz |   | dy |
198        // \ rz uz fz / \ dz /     \ fx fy fz /   \ dz /
199
200        self.pos += Mat4x4::<RealToReal<3>>::from_basis(right, up, fwd)
201            .transpose()
202            .apply(&delta);
203    }
204}
205
206//
207// Local trait impls
208//
209
210#[cfg(feature = "fp")]
211impl Mode for FirstPerson {
212    fn world_to_view(&self) -> Mat4x4<WorldToView> {
213        let &Self { pos, heading: dir, .. } = self;
214        let fwd_move = spherical(1.0, dir.az(), turns(0.0));
215        let fwd = self.heading;
216        let right = vec3(0.0, 1.0, 0.0).cross(&fwd_move.to_cart());
217
218        let transl = translate(-pos);
219        let orient = orient_z(fwd.into(), right);
220
221        transl.then(&orient).to()
222    }
223}
224
225impl Mode for Mat4x4<WorldToView> {
226    fn world_to_view(&self) -> Mat4x4<WorldToView> {
227        *self
228    }
229}
230
231//
232// Foreign trait impls
233//
234
235#[cfg(feature = "fp")]
236impl Default for FirstPerson {
237    /// Returns [`FirstPerson::new`].
238    fn default() -> Self {
239        Self::new()
240    }
241}