bevy_render/camera/
projection.rs

1use std::marker::PhantomData;
2use std::ops::{Div, DivAssign, Mul, MulAssign};
3
4use crate::primitives::Frustum;
5use crate::view::VisibilitySystems;
6use bevy_app::{App, Plugin, PostStartup, PostUpdate};
7use bevy_ecs::prelude::*;
8use bevy_math::{AspectRatio, Mat4, Rect, Vec2, Vec3A};
9use bevy_reflect::{
10    std_traits::ReflectDefault, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectSerialize,
11};
12use bevy_transform::components::GlobalTransform;
13use bevy_transform::TransformSystem;
14use serde::{Deserialize, Serialize};
15
16/// Adds [`Camera`](crate::camera::Camera) driver systems for a given projection type.
17///
18/// If you are using `bevy_pbr`, then you need to add `PbrProjectionPlugin` along with this.
19pub struct CameraProjectionPlugin<T: CameraProjection + Component + GetTypeRegistration>(
20    PhantomData<T>,
21);
22impl<T: CameraProjection + Component + GetTypeRegistration> Plugin for CameraProjectionPlugin<T> {
23    fn build(&self, app: &mut App) {
24        app.register_type::<T>()
25            .add_systems(
26                PostStartup,
27                crate::camera::camera_system::<T>
28                    .in_set(CameraUpdateSystem)
29                    // We assume that each camera will only have one projection,
30                    // so we can ignore ambiguities with all other monomorphizations.
31                    // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481.
32                    .ambiguous_with(CameraUpdateSystem),
33            )
34            .add_systems(
35                PostUpdate,
36                (
37                    crate::camera::camera_system::<T>
38                        .in_set(CameraUpdateSystem)
39                        // We assume that each camera will only have one projection,
40                        // so we can ignore ambiguities with all other monomorphizations.
41                        // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481.
42                        .ambiguous_with(CameraUpdateSystem),
43                    crate::view::update_frusta::<T>
44                        .in_set(VisibilitySystems::UpdateFrusta)
45                        .after(crate::camera::camera_system::<T>)
46                        .after(TransformSystem::TransformPropagate)
47                        // We assume that no camera will have more than one projection component,
48                        // so these systems will run independently of one another.
49                        // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481.
50                        .ambiguous_with(VisibilitySystems::UpdateFrusta),
51                ),
52            );
53    }
54}
55impl<T: CameraProjection + Component + GetTypeRegistration> Default for CameraProjectionPlugin<T> {
56    fn default() -> Self {
57        Self(Default::default())
58    }
59}
60
61/// Label for [`camera_system<T>`], shared across all `T`.
62///
63/// [`camera_system<T>`]: crate::camera::camera_system
64#[derive(SystemSet, Clone, Eq, PartialEq, Hash, Debug)]
65pub struct CameraUpdateSystem;
66
67/// Trait to control the projection matrix of a camera.
68///
69/// Components implementing this trait are automatically polled for changes, and used
70/// to recompute the camera projection matrix of the [`Camera`] component attached to
71/// the same entity as the component implementing this trait.
72///
73/// Use the plugins [`CameraProjectionPlugin`] and `bevy::pbr::PbrProjectionPlugin` to setup the
74/// systems for your [`CameraProjection`] implementation.
75///
76/// [`Camera`]: crate::camera::Camera
77pub trait CameraProjection {
78    fn get_clip_from_view(&self) -> Mat4;
79    fn update(&mut self, width: f32, height: f32);
80    fn far(&self) -> f32;
81    fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8];
82
83    /// Compute camera frustum for camera with given projection and transform.
84    ///
85    /// This code is called by [`update_frusta`](crate::view::visibility::update_frusta) system
86    /// for each camera to update its frustum.
87    fn compute_frustum(&self, camera_transform: &GlobalTransform) -> Frustum {
88        let clip_from_world =
89            self.get_clip_from_view() * camera_transform.compute_matrix().inverse();
90        Frustum::from_clip_from_world_custom_far(
91            &clip_from_world,
92            &camera_transform.translation(),
93            &camera_transform.back(),
94            self.far(),
95        )
96    }
97}
98
99/// A configurable [`CameraProjection`] that can select its projection type at runtime.
100#[derive(Component, Debug, Clone, Reflect)]
101#[reflect(Component, Default)]
102pub enum Projection {
103    Perspective(PerspectiveProjection),
104    Orthographic(OrthographicProjection),
105}
106
107impl From<PerspectiveProjection> for Projection {
108    fn from(p: PerspectiveProjection) -> Self {
109        Self::Perspective(p)
110    }
111}
112
113impl From<OrthographicProjection> for Projection {
114    fn from(p: OrthographicProjection) -> Self {
115        Self::Orthographic(p)
116    }
117}
118
119impl CameraProjection for Projection {
120    fn get_clip_from_view(&self) -> Mat4 {
121        match self {
122            Projection::Perspective(projection) => projection.get_clip_from_view(),
123            Projection::Orthographic(projection) => projection.get_clip_from_view(),
124        }
125    }
126
127    fn update(&mut self, width: f32, height: f32) {
128        match self {
129            Projection::Perspective(projection) => projection.update(width, height),
130            Projection::Orthographic(projection) => projection.update(width, height),
131        }
132    }
133
134    fn far(&self) -> f32 {
135        match self {
136            Projection::Perspective(projection) => projection.far(),
137            Projection::Orthographic(projection) => projection.far(),
138        }
139    }
140
141    fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
142        match self {
143            Projection::Perspective(projection) => projection.get_frustum_corners(z_near, z_far),
144            Projection::Orthographic(projection) => projection.get_frustum_corners(z_near, z_far),
145        }
146    }
147}
148
149impl Default for Projection {
150    fn default() -> Self {
151        Projection::Perspective(Default::default())
152    }
153}
154
155/// A 3D camera projection in which distant objects appear smaller than close objects.
156#[derive(Component, Debug, Clone, Reflect)]
157#[reflect(Component, Default)]
158pub struct PerspectiveProjection {
159    /// The vertical field of view (FOV) in radians.
160    ///
161    /// Defaults to a value of π/4 radians or 45 degrees.
162    pub fov: f32,
163
164    /// The aspect ratio (width divided by height) of the viewing frustum.
165    ///
166    /// Bevy's [`camera_system`](crate::camera::camera_system) automatically
167    /// updates this value when the aspect ratio of the associated window changes.
168    ///
169    /// Defaults to a value of `1.0`.
170    pub aspect_ratio: f32,
171
172    /// The distance from the camera in world units of the viewing frustum's near plane.
173    ///
174    /// Objects closer to the camera than this value will not be visible.
175    ///
176    /// Defaults to a value of `0.1`.
177    pub near: f32,
178
179    /// The distance from the camera in world units of the viewing frustum's far plane.
180    ///
181    /// Objects farther from the camera than this value will not be visible.
182    ///
183    /// Defaults to a value of `1000.0`.
184    pub far: f32,
185}
186
187impl CameraProjection for PerspectiveProjection {
188    fn get_clip_from_view(&self) -> Mat4 {
189        Mat4::perspective_infinite_reverse_rh(self.fov, self.aspect_ratio, self.near)
190    }
191
192    fn update(&mut self, width: f32, height: f32) {
193        self.aspect_ratio = AspectRatio::new(width, height).into();
194    }
195
196    fn far(&self) -> f32 {
197        self.far
198    }
199
200    fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
201        let tan_half_fov = (self.fov / 2.).tan();
202        let a = z_near.abs() * tan_half_fov;
203        let b = z_far.abs() * tan_half_fov;
204        let aspect_ratio = self.aspect_ratio;
205        // NOTE: These vertices are in the specific order required by [`calculate_cascade`].
206        [
207            Vec3A::new(a * aspect_ratio, -a, z_near),  // bottom right
208            Vec3A::new(a * aspect_ratio, a, z_near),   // top right
209            Vec3A::new(-a * aspect_ratio, a, z_near),  // top left
210            Vec3A::new(-a * aspect_ratio, -a, z_near), // bottom left
211            Vec3A::new(b * aspect_ratio, -b, z_far),   // bottom right
212            Vec3A::new(b * aspect_ratio, b, z_far),    // top right
213            Vec3A::new(-b * aspect_ratio, b, z_far),   // top left
214            Vec3A::new(-b * aspect_ratio, -b, z_far),  // bottom left
215        ]
216    }
217}
218
219impl Default for PerspectiveProjection {
220    fn default() -> Self {
221        PerspectiveProjection {
222            fov: std::f32::consts::PI / 4.0,
223            near: 0.1,
224            far: 1000.0,
225            aspect_ratio: 1.0,
226        }
227    }
228}
229
230/// Scaling mode for [`OrthographicProjection`].
231///
232/// # Examples
233///
234/// Configure the orthographic projection to two world units per window height:
235///
236/// ```
237/// # use bevy_render::camera::{OrthographicProjection, Projection, ScalingMode};
238/// let projection = Projection::Orthographic(OrthographicProjection {
239///    scaling_mode: ScalingMode::FixedVertical(2.0),
240///    ..OrthographicProjection::default()
241/// });
242/// ```
243#[derive(Debug, Clone, Copy, Reflect, Serialize, Deserialize)]
244#[reflect(Serialize, Deserialize)]
245pub enum ScalingMode {
246    /// Manually specify the projection's size, ignoring window resizing. The image will stretch.
247    /// Arguments are in world units.
248    Fixed { width: f32, height: f32 },
249    /// Match the viewport size.
250    /// The argument is the number of pixels that equals one world unit.
251    WindowSize(f32),
252    /// Keeping the aspect ratio while the axes can't be smaller than given minimum.
253    /// Arguments are in world units.
254    AutoMin { min_width: f32, min_height: f32 },
255    /// Keeping the aspect ratio while the axes can't be bigger than given maximum.
256    /// Arguments are in world units.
257    AutoMax { max_width: f32, max_height: f32 },
258    /// Keep the projection's height constant; width will be adjusted to match aspect ratio.
259    /// The argument is the desired height of the projection in world units.
260    FixedVertical(f32),
261    /// Keep the projection's width constant; height will be adjusted to match aspect ratio.
262    /// The argument is the desired width of the projection in world units.
263    FixedHorizontal(f32),
264}
265
266impl Mul<f32> for ScalingMode {
267    type Output = ScalingMode;
268
269    /// Scale the `ScalingMode`. For example, multiplying by 2 makes the viewport twice as large.
270    fn mul(self, rhs: f32) -> ScalingMode {
271        match self {
272            ScalingMode::Fixed { width, height } => ScalingMode::Fixed {
273                width: width * rhs,
274                height: height * rhs,
275            },
276            ScalingMode::WindowSize(pixels_per_world_unit) => {
277                ScalingMode::WindowSize(pixels_per_world_unit / rhs)
278            }
279            ScalingMode::AutoMin {
280                min_width,
281                min_height,
282            } => ScalingMode::AutoMin {
283                min_width: min_width * rhs,
284                min_height: min_height * rhs,
285            },
286            ScalingMode::AutoMax {
287                max_width,
288                max_height,
289            } => ScalingMode::AutoMax {
290                max_width: max_width * rhs,
291                max_height: max_height * rhs,
292            },
293            ScalingMode::FixedVertical(size) => ScalingMode::FixedVertical(size * rhs),
294            ScalingMode::FixedHorizontal(size) => ScalingMode::FixedHorizontal(size * rhs),
295        }
296    }
297}
298
299impl MulAssign<f32> for ScalingMode {
300    fn mul_assign(&mut self, rhs: f32) {
301        *self = *self * rhs;
302    }
303}
304
305impl Div<f32> for ScalingMode {
306    type Output = ScalingMode;
307
308    /// Scale the `ScalingMode`. For example, dividing by 2 makes the viewport half as large.
309    fn div(self, rhs: f32) -> ScalingMode {
310        self * (1.0 / rhs)
311    }
312}
313
314impl DivAssign<f32> for ScalingMode {
315    fn div_assign(&mut self, rhs: f32) {
316        *self = *self / rhs;
317    }
318}
319
320/// Project a 3D space onto a 2D surface using parallel lines, i.e., unlike [`PerspectiveProjection`],
321/// the size of objects remains the same regardless of their distance to the camera.
322///
323/// The volume contained in the projection is called the *view frustum*. Since the viewport is rectangular
324/// and projection lines are parallel, the view frustum takes the shape of a cuboid.
325///
326/// Note that the scale of the projection and the apparent size of objects are inversely proportional.
327/// As the size of the projection increases, the size of objects decreases.
328///
329/// # Examples
330///
331/// Configure the orthographic projection to one world unit per 100 window pixels:
332///
333/// ```
334/// # use bevy_render::camera::{OrthographicProjection, Projection, ScalingMode};
335/// let projection = Projection::Orthographic(OrthographicProjection {
336///     scaling_mode: ScalingMode::WindowSize(100.0),
337///     ..OrthographicProjection::default()
338/// });
339/// ```
340#[derive(Component, Debug, Clone, Reflect)]
341#[reflect(Component, Default)]
342pub struct OrthographicProjection {
343    /// The distance of the near clipping plane in world units.
344    ///
345    /// Objects closer than this will not be rendered.
346    ///
347    /// Defaults to `0.0`
348    pub near: f32,
349    /// The distance of the far clipping plane in world units.
350    ///
351    /// Objects further than this will not be rendered.
352    ///
353    /// Defaults to `1000.0`
354    pub far: f32,
355    /// Specifies the origin of the viewport as a normalized position from 0 to 1, where (0, 0) is the bottom left
356    /// and (1, 1) is the top right. This determines where the camera's position sits inside the viewport.
357    ///
358    /// When the projection scales due to viewport resizing, the position of the camera, and thereby `viewport_origin`,
359    /// remains at the same relative point.
360    ///
361    /// Consequently, this is pivot point when scaling. With a bottom left pivot, the projection will expand
362    /// upwards and to the right. With a top right pivot, the projection will expand downwards and to the left.
363    /// Values in between will caused the projection to scale proportionally on each axis.
364    ///
365    /// Defaults to `(0.5, 0.5)`, which makes scaling affect opposite sides equally, keeping the center
366    /// point of the viewport centered.
367    pub viewport_origin: Vec2,
368    /// How the projection will scale to the viewport.
369    ///
370    /// Defaults to `ScalingMode::WindowSize(1.0)`
371    pub scaling_mode: ScalingMode,
372    /// Scales the projection.
373    ///
374    /// As scale increases, the apparent size of objects decreases, and vice versa.
375    ///
376    /// Note: scaling can be set by [`scaling_mode`](Self::scaling_mode) as well.
377    /// This parameter scales on top of that.
378    ///
379    /// This property is particularly useful in implementing zoom functionality.
380    ///
381    /// Defaults to `1.0`.
382    pub scale: f32,
383    /// The area that the projection covers relative to `viewport_origin`.
384    ///
385    /// Bevy's [`camera_system`](crate::camera::camera_system) automatically
386    /// updates this value when the viewport is resized depending on `OrthographicProjection`'s other fields.
387    /// In this case, `area` should not be manually modified.
388    ///
389    /// It may be necessary to set this manually for shadow projections and such.
390    pub area: Rect,
391}
392
393impl CameraProjection for OrthographicProjection {
394    fn get_clip_from_view(&self) -> Mat4 {
395        Mat4::orthographic_rh(
396            self.area.min.x,
397            self.area.max.x,
398            self.area.min.y,
399            self.area.max.y,
400            // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0]
401            // This is for interoperability with pipelines using infinite reverse perspective projections.
402            self.far,
403            self.near,
404        )
405    }
406
407    fn update(&mut self, width: f32, height: f32) {
408        let (projection_width, projection_height) = match self.scaling_mode {
409            ScalingMode::WindowSize(pixel_scale) => (width / pixel_scale, height / pixel_scale),
410            ScalingMode::AutoMin {
411                min_width,
412                min_height,
413            } => {
414                // Compare Pixels of current width and minimal height and Pixels of minimal width with current height.
415                // Then use bigger (min_height when true) as what it refers to (height when true) and calculate rest so it can't get under minimum.
416                if width * min_height > min_width * height {
417                    (width * min_height / height, min_height)
418                } else {
419                    (min_width, height * min_width / width)
420                }
421            }
422            ScalingMode::AutoMax {
423                max_width,
424                max_height,
425            } => {
426                // Compare Pixels of current width and maximal height and Pixels of maximal width with current height.
427                // Then use smaller (max_height when true) as what it refers to (height when true) and calculate rest so it can't get over maximum.
428                if width * max_height < max_width * height {
429                    (width * max_height / height, max_height)
430                } else {
431                    (max_width, height * max_width / width)
432                }
433            }
434            ScalingMode::FixedVertical(viewport_height) => {
435                (width * viewport_height / height, viewport_height)
436            }
437            ScalingMode::FixedHorizontal(viewport_width) => {
438                (viewport_width, height * viewport_width / width)
439            }
440            ScalingMode::Fixed { width, height } => (width, height),
441        };
442
443        let mut origin_x = projection_width * self.viewport_origin.x;
444        let mut origin_y = projection_height * self.viewport_origin.y;
445
446        // If projection is based on window pixels,
447        // ensure we don't end up with fractional pixels!
448        if let ScalingMode::WindowSize(pixel_scale) = self.scaling_mode {
449            // round to nearest multiple of `pixel_scale`
450            origin_x = (origin_x * pixel_scale).round() / pixel_scale;
451            origin_y = (origin_y * pixel_scale).round() / pixel_scale;
452        }
453
454        self.area = Rect::new(
455            self.scale * -origin_x,
456            self.scale * -origin_y,
457            self.scale * (projection_width - origin_x),
458            self.scale * (projection_height - origin_y),
459        );
460    }
461
462    fn far(&self) -> f32 {
463        self.far
464    }
465
466    fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
467        let area = self.area;
468        // NOTE: These vertices are in the specific order required by [`calculate_cascade`].
469        [
470            Vec3A::new(area.max.x, area.min.y, z_near), // bottom right
471            Vec3A::new(area.max.x, area.max.y, z_near), // top right
472            Vec3A::new(area.min.x, area.max.y, z_near), // top left
473            Vec3A::new(area.min.x, area.min.y, z_near), // bottom left
474            Vec3A::new(area.max.x, area.min.y, z_far),  // bottom right
475            Vec3A::new(area.max.x, area.max.y, z_far),  // top right
476            Vec3A::new(area.min.x, area.max.y, z_far),  // top left
477            Vec3A::new(area.min.x, area.min.y, z_far),  // bottom left
478        ]
479    }
480}
481
482impl Default for OrthographicProjection {
483    fn default() -> Self {
484        OrthographicProjection {
485            scale: 1.0,
486            near: 0.0,
487            far: 1000.0,
488            viewport_origin: Vec2::new(0.5, 0.5),
489            scaling_mode: ScalingMode::WindowSize(1.0),
490            area: Rect::new(-1.0, -1.0, 1.0, 1.0),
491        }
492    }
493}