Skip to main content

bevy_camera/
projection.rs

1use core::fmt::Debug;
2use core::ops::{Deref, DerefMut};
3
4use crate::{primitives::Frustum, visibility::VisibilitySystems};
5use bevy_app::{App, Plugin, PostUpdate};
6use bevy_ecs::prelude::*;
7use bevy_math::{ops, primitives::ViewFrustum, vec4, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4};
8use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize};
9use bevy_transform::{components::GlobalTransform, TransformSystems};
10use derive_more::derive::From;
11use serde::{Deserialize, Serialize};
12
13/// Adds [`Camera`](crate::camera::Camera) driver systems for a given projection type.
14///
15/// If you are using `bevy_pbr`, then you need to add `PbrProjectionPlugin` along with this.
16#[derive(Default)]
17pub struct CameraProjectionPlugin;
18
19impl Plugin for CameraProjectionPlugin {
20    fn build(&self, app: &mut App) {
21        app.add_systems(
22            PostUpdate,
23            crate::visibility::update_frusta
24                .in_set(VisibilitySystems::UpdateFrusta)
25                .after(TransformSystems::Propagate),
26        );
27    }
28}
29
30/// Describes a type that can generate a projection matrix, allowing it to be added to a
31/// [`Camera`]'s [`Projection`] component.
32///
33/// Once implemented, the projection can be added to a camera using [`Projection::custom`].
34///
35/// The projection will be automatically updated as the render area is resized. This is useful when,
36/// for example, a projection type has a field like `fov` that should change when the window width
37/// is changed but not when the height changes.
38///
39/// This trait is implemented by bevy's built-in projections [`PerspectiveProjection`] and
40/// [`OrthographicProjection`].
41///
42/// [`Camera`]: crate::camera::Camera
43pub trait CameraProjection {
44    /// Generate the projection matrix.
45    fn get_clip_from_view(&self) -> Mat4;
46
47    /// Generate the projection matrix for a [`SubCameraView`](super::SubCameraView).
48    fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4;
49
50    /// When the area this camera renders to changes dimensions, this method will be automatically
51    /// called. Use this to update any projection properties that depend on the aspect ratio or
52    /// dimensions of the render area.
53    fn update(&mut self, width: f32, height: f32);
54
55    /// The far plane distance of the projection.
56    fn far(&self) -> f32;
57
58    /// The eight corners of the camera frustum, as defined by this projection.
59    ///
60    /// The corners should be provided in the following order: first the bottom right, top right,
61    /// top left, bottom left for the near plane, then similar for the far plane.
62    // TODO: This seems somewhat redundant with `compute_frustum`, and similarly should be possible
63    // to compute with a default impl.
64    fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8];
65
66    /// Compute camera frustum for camera with given projection and transform.
67    ///
68    /// This code is called by [`update_frusta`](crate::visibility::update_frusta) system
69    /// for each camera to update its frustum.
70    fn compute_frustum(&self, camera_transform: &GlobalTransform) -> Frustum {
71        let clip_from_world = self.get_clip_from_view() * camera_transform.affine().inverse();
72        Frustum(ViewFrustum::from_clip_from_world_custom_far(
73            &clip_from_world,
74            &camera_transform.translation(),
75            &camera_transform.back(),
76            self.far(),
77        ))
78    }
79}
80
81mod sealed {
82    use super::CameraProjection;
83
84    /// A wrapper trait to make it possible to implement Clone for boxed [`CameraProjection`](`super::CameraProjection`)
85    /// trait objects, without breaking object safety rules by making it `Sized`. Additional bounds
86    /// are included for downcasting, and fulfilling the trait bounds on `Projection`.
87    pub trait DynCameraProjection:
88        CameraProjection + core::fmt::Debug + Send + Sync + downcast_rs::Downcast
89    {
90        fn clone_box(&self) -> Box<dyn DynCameraProjection>;
91    }
92
93    downcast_rs::impl_downcast!(DynCameraProjection);
94
95    impl<T> DynCameraProjection for T
96    where
97        T: 'static + CameraProjection + core::fmt::Debug + Send + Sync + Clone,
98    {
99        fn clone_box(&self) -> Box<dyn DynCameraProjection> {
100            Box::new(self.clone())
101        }
102    }
103}
104
105/// Holds a dynamic [`CameraProjection`] trait object. Use [`Projection::custom()`] to construct a
106/// custom projection.
107///
108/// The contained dynamic object can be downcast into a static type using [`CustomProjection::get`].
109#[derive(Debug, Reflect)]
110#[reflect(Default, Clone)]
111pub struct CustomProjection {
112    #[reflect(ignore)]
113    dyn_projection: Box<dyn sealed::DynCameraProjection>,
114}
115
116impl Default for CustomProjection {
117    fn default() -> Self {
118        Self {
119            dyn_projection: Box::new(PerspectiveProjection::default()),
120        }
121    }
122}
123
124impl Clone for CustomProjection {
125    fn clone(&self) -> Self {
126        Self {
127            dyn_projection: self.dyn_projection.clone_box(),
128        }
129    }
130}
131
132impl CustomProjection {
133    /// Returns a reference to the [`CameraProjection`] `P`.
134    ///
135    /// Returns `None` if this dynamic object is not a projection of type `P`.
136    ///
137    /// ```
138    /// # use bevy_camera::{Projection, PerspectiveProjection};
139    /// // For simplicity's sake, use perspective as a custom projection:
140    /// let projection = Projection::custom(PerspectiveProjection::default());
141    /// let Projection::Custom(custom) = projection else { return };
142    ///
143    /// // At this point the projection type is erased.
144    /// // We can use `get()` if we know what kind of projection we have.
145    /// let perspective = custom.get::<PerspectiveProjection>().unwrap();
146    ///
147    /// assert_eq!(perspective.fov, PerspectiveProjection::default().fov);
148    /// ```
149    pub fn get<P>(&self) -> Option<&P>
150    where
151        P: CameraProjection + Debug + Send + Sync + Clone + 'static,
152    {
153        self.dyn_projection.downcast_ref()
154    }
155
156    /// Returns a mutable  reference to the [`CameraProjection`] `P`.
157    ///
158    /// Returns `None` if this dynamic object is not a projection of type `P`.
159    ///
160    /// ```
161    /// # use bevy_camera::{Projection, PerspectiveProjection};
162    /// // For simplicity's sake, use perspective as a custom projection:
163    /// let mut projection = Projection::custom(PerspectiveProjection::default());
164    /// let Projection::Custom(mut custom) = projection else { return };
165    ///
166    /// // At this point the projection type is erased.
167    /// // We can use `get_mut()` if we know what kind of projection we have.
168    /// let perspective = custom.get_mut::<PerspectiveProjection>().unwrap();
169    ///
170    /// assert_eq!(perspective.fov, PerspectiveProjection::default().fov);
171    /// perspective.fov = 1.0;
172    /// ```
173    pub fn get_mut<P>(&mut self) -> Option<&mut P>
174    where
175        P: CameraProjection + Debug + Send + Sync + Clone + 'static,
176    {
177        self.dyn_projection.downcast_mut()
178    }
179}
180
181impl Deref for CustomProjection {
182    type Target = dyn CameraProjection;
183
184    fn deref(&self) -> &Self::Target {
185        self.dyn_projection.as_ref()
186    }
187}
188
189impl DerefMut for CustomProjection {
190    fn deref_mut(&mut self) -> &mut Self::Target {
191        self.dyn_projection.as_mut()
192    }
193}
194
195/// Component that defines how to compute a [`Camera`]'s projection matrix.
196///
197/// Common projections, like perspective and orthographic, are provided out of the box to handle the
198/// majority of use cases. Custom projections can be added using the [`CameraProjection`] trait and
199/// the [`Projection::custom`] constructor.
200///
201/// ## What's a projection?
202///
203/// A camera projection essentially describes how 3d points from the point of view of a camera are
204/// projected onto a 2d screen. This is where properties like a camera's field of view are defined.
205/// More specifically, a projection is a 4x4 matrix that transforms points from view space (the
206/// point of view of the camera) into clip space. Clip space is almost, but not quite, equivalent to
207/// the rectangle that is rendered to your screen, with a depth axis. Any points that land outside
208/// the bounds of this cuboid are "clipped" and not rendered.
209///
210/// You can also think of the projection as the thing that describes the shape of a camera's
211/// frustum: the volume in 3d space that is visible to a camera.
212///
213/// [`Camera`]: crate::camera::Camera
214#[derive(Component, Debug, Clone, Reflect, From)]
215#[reflect(Component, Default, Debug, Clone)]
216pub enum Projection {
217    Perspective(PerspectiveProjection),
218    Orthographic(OrthographicProjection),
219    Custom(CustomProjection),
220}
221
222impl Projection {
223    /// Construct a new custom camera projection from a type that implements [`CameraProjection`].
224    pub fn custom<P>(projection: P) -> Self
225    where
226        // Implementation note: pushing these trait bounds all the way out to this function makes
227        // errors nice for users. If a trait is missing, they will get a helpful error telling them
228        // that, say, the `Debug` implementation is missing. Wrapping these traits behind a super
229        // trait or some other indirection will make the errors harder to understand.
230        //
231        // For example, we don't use the `DynCameraProjection` trait bound, because it is not the
232        // trait the user should be implementing - they only need to worry about implementing
233        // `CameraProjection`.
234        P: CameraProjection + Debug + Send + Sync + Clone + 'static,
235    {
236        Projection::Custom(CustomProjection {
237            dyn_projection: Box::new(projection),
238        })
239    }
240
241    /// Check if the projection is perspective.
242    /// For [`CustomProjection`], this checks if the projection matrix's w-axis's w is 0.0.
243    pub fn is_perspective(&self) -> bool {
244        match self {
245            Projection::Perspective(_) => true,
246            Projection::Orthographic(_) => false,
247            Projection::Custom(projection) => projection.get_clip_from_view().w_axis.w == 0.0,
248        }
249    }
250}
251
252impl Deref for Projection {
253    type Target = dyn CameraProjection;
254
255    fn deref(&self) -> &Self::Target {
256        match self {
257            Projection::Perspective(projection) => projection,
258            Projection::Orthographic(projection) => projection,
259            Projection::Custom(projection) => projection.deref(),
260        }
261    }
262}
263
264impl DerefMut for Projection {
265    fn deref_mut(&mut self) -> &mut Self::Target {
266        match self {
267            Projection::Perspective(projection) => projection,
268            Projection::Orthographic(projection) => projection,
269            Projection::Custom(projection) => projection.deref_mut(),
270        }
271    }
272}
273
274impl Default for Projection {
275    fn default() -> Self {
276        Projection::Perspective(Default::default())
277    }
278}
279
280/// A 3D camera projection in which distant objects appear smaller than close objects.
281#[derive(Debug, Clone, Reflect)]
282#[reflect(Default, Debug, Clone)]
283pub struct PerspectiveProjection {
284    /// The vertical field of view (FOV) in radians.
285    ///
286    /// Defaults to a value of π/4 radians or 45 degrees.
287    pub fov: f32,
288
289    /// The aspect ratio (width divided by height) of the viewing frustum.
290    ///
291    /// Bevy's `camera_system` automatically updates this value when the aspect ratio
292    /// of the associated window changes.
293    ///
294    /// Defaults to a value of `1.0`.
295    pub aspect_ratio: f32,
296
297    /// The distance from the camera in world units of the viewing frustum's near plane.
298    ///
299    /// Objects closer to the camera than this value will not be visible.
300    ///
301    /// Defaults to a value of `0.1`.
302    pub near: f32,
303
304    /// The distance from the camera in world units of the viewing frustum's far plane.
305    ///
306    /// Objects farther from the camera than this value will not be visible.
307    ///
308    /// Defaults to a value of `1000.0`.
309    pub far: f32,
310
311    /// The orientation of a custom clipping plane, as well as its distance from
312    /// the camera.
313    ///
314    /// If you supply a plane here, anything in front of the plane will be
315    /// clipped out. This is useful for portals and mirrors, in order to clip
316    /// any geometry that would pass through the plane of the portal or mirror.
317    ///
318    /// The X, Y, and Z components of the vector describe its normal, in view
319    /// space. This normal vector must have length 1, and it should point away
320    /// from the camera. (That is, only geometry on the side of the plane that
321    /// the normal points toward will be rendered.) The W component of the
322    /// vector must be the *negative shortest signed distance* from the camera
323    /// to the plane, again in view space.  This final component can also be
324    /// computed as -(N ·  Q), where N is the normal of the plane and Q is any
325    /// point on it.
326    ///
327    /// By default, this is (0, 0, -1, -[`Self::near`]), which describes a near
328    /// plane located [`Self::near`] meters away pointing directly away from the
329    /// camera.
330    ///
331    /// See the `calculate_mirror_camera_transform_and_projection` function in
332    /// the `mirror` example for an exhaustive example of usage.
333    pub near_clip_plane: Vec4,
334}
335
336impl CameraProjection for PerspectiveProjection {
337    fn get_clip_from_view(&self) -> Mat4 {
338        let mut matrix =
339            Mat4::perspective_infinite_reverse_rh(self.fov, self.aspect_ratio, self.near);
340        self.adjust_perspective_matrix_for_clip_plane(&mut matrix);
341        matrix
342    }
343
344    fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 {
345        let full_width = sub_view.full_size.x as f32;
346        let full_height = sub_view.full_size.y as f32;
347        let sub_width = sub_view.size.x as f32;
348        let sub_height = sub_view.size.y as f32;
349        let offset_x = sub_view.offset.x;
350        // Y-axis increases from top to bottom
351        let offset_y = full_height - (sub_view.offset.y + sub_height);
352
353        let full_aspect = full_width / full_height;
354
355        // Original frustum parameters
356        let top = self.near * ops::tan(0.5 * self.fov);
357        let bottom = -top;
358        let right = top * full_aspect;
359        let left = -right;
360
361        // Calculate scaling factors
362        let width = right - left;
363        let height = top - bottom;
364
365        // Calculate the new frustum parameters
366        let left_prime = left + (width * offset_x) / full_width;
367        let right_prime = left + (width * (offset_x + sub_width)) / full_width;
368        let bottom_prime = bottom + (height * offset_y) / full_height;
369        let top_prime = bottom + (height * (offset_y + sub_height)) / full_height;
370
371        // Compute the new projection matrix
372        let x = (2.0 * self.near) / (right_prime - left_prime);
373        let y = (2.0 * self.near) / (top_prime - bottom_prime);
374        let a = (right_prime + left_prime) / (right_prime - left_prime);
375        let b = (top_prime + bottom_prime) / (top_prime - bottom_prime);
376
377        let mut matrix = Mat4::from_cols(
378            Vec4::new(x, 0.0, 0.0, 0.0),
379            Vec4::new(0.0, y, 0.0, 0.0),
380            Vec4::new(a, b, 0.0, -1.0),
381            Vec4::new(0.0, 0.0, self.near, 0.0),
382        );
383
384        self.adjust_perspective_matrix_for_clip_plane(&mut matrix);
385        matrix
386    }
387
388    fn update(&mut self, width: f32, height: f32) {
389        self.aspect_ratio = AspectRatio::try_new(width, height)
390            .expect("Failed to update PerspectiveProjection: width and height must be positive, non-zero values")
391            .ratio();
392    }
393
394    fn far(&self) -> f32 {
395        self.far
396    }
397
398    fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
399        let tan_half_fov = ops::tan(self.fov / 2.);
400        let a = z_near.abs() * tan_half_fov;
401        let b = z_far.abs() * tan_half_fov;
402        let aspect_ratio = self.aspect_ratio;
403        // NOTE: These vertices are in the specific order required by [`calculate_cascade`].
404        [
405            Vec3A::new(a * aspect_ratio, -a, z_near),  // bottom right
406            Vec3A::new(a * aspect_ratio, a, z_near),   // top right
407            Vec3A::new(-a * aspect_ratio, a, z_near),  // top left
408            Vec3A::new(-a * aspect_ratio, -a, z_near), // bottom left
409            Vec3A::new(b * aspect_ratio, -b, z_far),   // bottom right
410            Vec3A::new(b * aspect_ratio, b, z_far),    // top right
411            Vec3A::new(-b * aspect_ratio, b, z_far),   // top left
412            Vec3A::new(-b * aspect_ratio, -b, z_far),  // bottom left
413        ]
414    }
415}
416
417impl Default for PerspectiveProjection {
418    fn default() -> Self {
419        PerspectiveProjection {
420            fov: core::f32::consts::PI / 4.0,
421            near: 0.1,
422            far: 1000.0,
423            aspect_ratio: 1.0,
424            near_clip_plane: vec4(0.0, 0.0, -1.0, -0.1),
425        }
426    }
427}
428
429impl PerspectiveProjection {
430    /// Adjusts the perspective matrix for an oblique clip plane if necessary.
431    ///
432    /// This changes the near and (infinite) far planes so that they correctly
433    /// clip everything in front of the [`Self::near_clip_plane`]. See [Lengyel
434    /// 2005] for an exhaustive treatment of the way this works. Custom near
435    /// clip planes are typically used for portals and mirrors; see
436    /// `examples/3d/mirror.rs` for an example of usage.
437    fn adjust_perspective_matrix_for_clip_plane(&self, matrix: &mut Mat4) {
438        // If we don't have an oblique clip plane, save ourselves the trouble.
439        if self.near_clip_plane.x == 0.0
440            && self.near_clip_plane.y == 0.0
441            && self.near_clip_plane.z == -1.0
442        {
443            return;
444        }
445
446        // To understand this, refer to [Lengyel 2005]. The notation follows the
447        // paper. The formulas are different because the paper uses a standard
448        // OpenGL convention (near -1, far 1), while we use a reversed Vulkan
449        // convention (near 1, far 0).
450        //
451        // [Lengyel 2005]: https://terathon.com/lengyel/Lengyel-Oblique.pdf
452        let c = self.near_clip_plane;
453
454        // First, calculate the position of Q′, the corner in clip space lying
455        // opposite from the near clip plane. This is identical to equation (7).
456        // Note that this is a point at infinity in view space, because we use
457        // an infinite far plane, but in clip space it's finite.
458        let q_prime = vec4(c.x.signum(), c.y.signum(), 0.0, 1.0);
459
460        // Now convert that point to view space. This *will* be a point at
461        // infinity, but that's OK because we're in homogeneous coordinates.
462        let q = matrix.inverse() * q_prime;
463
464        // Here we're computing the scaling factor to apply to the near plane so
465        // that the far plane will intersect Q. This one differs from the paper.
466        // Using the notation Mᵢ to mean the *i*th row of the matrix M, start by
467        // observing that the near plane (z = 1) is described by M₄ - M₃ and the
468        // far plane (z = 0) is described by simply M₃. So:
469        //
470        // * Equation (4) becomes C = M₄ - M₃.
471        // * Equation (5) becomes M′₃ = M₄ - C.
472        // * Equation (6) becomes F = M′₃ = M₄ - C.
473        // * Equation (8) becomes F = M₄ - aC.
474        // * Equation (9) becomes F ·  Q = 0 ⇒ (M₄ - aC) ·  Q = 0.
475        //
476        // And, solving the modified equation (9), we get:
477        //
478        //          M₄ ·  Q
479        //      a = ⎯⎯⎯⎯⎯⎯
480        //           C ·  Q
481        //
482        // Because M₄ = (0, 0, -1, 0) (just as it is in the paper), this reduces to:
483        //
484        //           -Qz
485        //      a = ⎯⎯⎯⎯⎯
486        //          C ·  Q
487        //
488        // Which is what we calculate here.
489        let a = -q.z / c.dot(q);
490
491        // Finally, we have the revised equation (10), which is M′₃ = M₄ - aC.
492        // Similarly to the above, this simplifies to M′₃ = (0, 0, -1, 0) - aC.
493        let m3_prime = Vec4::NEG_Z - c * a;
494
495        // We have the replacement third row; write it in.
496        matrix.x_axis.z = m3_prime.x;
497        matrix.y_axis.z = m3_prime.y;
498        matrix.z_axis.z = m3_prime.z;
499        matrix.w_axis.z = m3_prime.w;
500    }
501}
502
503/// Scaling mode for [`OrthographicProjection`].
504///
505/// The effect of these scaling modes are combined with the [`OrthographicProjection::scale`] property.
506///
507/// For example, if the scaling mode is `ScalingMode::Fixed { width: 100.0, height: 300 }` and the scale is `2.0`,
508/// the projection will be 200 world units wide and 600 world units tall.
509///
510/// # Examples
511///
512/// Configure the orthographic projection to two world units per window height:
513///
514/// ```
515/// # use bevy_camera::{OrthographicProjection, Projection, ScalingMode};
516/// let projection = Projection::Orthographic(OrthographicProjection {
517///    scaling_mode: ScalingMode::FixedVertical { viewport_height: 2.0 },
518///    ..OrthographicProjection::default_2d()
519/// });
520/// ```
521#[derive(Default, Debug, Clone, Copy, Reflect, Serialize, Deserialize)]
522#[reflect(Serialize, Deserialize, Default, Clone)]
523pub enum ScalingMode {
524    /// Match the viewport size.
525    ///
526    /// With a scale of 1, lengths in world units will map 1:1 with the number of pixels used to render it.
527    /// For example, if we have a 64x64 sprite with a [`Transform::scale`](bevy_transform::prelude::Transform) of 1.0,
528    /// no custom size and no inherited scale, the sprite will be 64 world units wide and 64 world units tall.
529    /// When rendered with [`OrthographicProjection::scaling_mode`] set to `WindowSize` when the window scale factor is 1
530    /// the sprite will be rendered at 64 pixels wide and 64 pixels tall.
531    ///
532    /// Changing any of these properties will multiplicatively affect the final size.
533    #[default]
534    WindowSize,
535    /// Manually specify the projection's size, ignoring window resizing. The image will stretch.
536    ///
537    /// Arguments describe the area of the world that is shown (in world units).
538    Fixed { width: f32, height: f32 },
539    /// Keeping the aspect ratio while the axes can't be smaller than given minimum.
540    ///
541    /// Arguments are in world units.
542    AutoMin { min_width: f32, min_height: f32 },
543    /// Keeping the aspect ratio while the axes can't be bigger than given maximum.
544    ///
545    /// Arguments are in world units.
546    AutoMax { max_width: f32, max_height: f32 },
547    /// Keep the projection's height constant; width will be adjusted to match aspect ratio.
548    ///
549    /// The argument is the desired height of the projection in world units.
550    FixedVertical { viewport_height: f32 },
551    /// Keep the projection's width constant; height will be adjusted to match aspect ratio.
552    ///
553    /// The argument is the desired width of the projection in world units.
554    FixedHorizontal { viewport_width: f32 },
555}
556
557/// Project a 3D space onto a 2D surface using parallel lines, i.e., unlike [`PerspectiveProjection`],
558/// the size of objects remains the same regardless of their distance to the camera.
559///
560/// The volume contained in the projection is called the *view frustum*. Since the viewport is rectangular
561/// and projection lines are parallel, the view frustum takes the shape of a cuboid.
562///
563/// Note that the scale of the projection and the apparent size of objects are inversely proportional.
564/// As the size of the projection increases, the size of objects decreases.
565///
566/// # Examples
567///
568/// Configure the orthographic projection to one world unit per 100 window pixels:
569///
570/// ```
571/// # use bevy_camera::{OrthographicProjection, Projection, ScalingMode};
572/// let projection = Projection::Orthographic(OrthographicProjection {
573///     scaling_mode: ScalingMode::WindowSize,
574///     scale: 0.01,
575///     ..OrthographicProjection::default_2d()
576/// });
577/// ```
578#[derive(Debug, Clone, Reflect)]
579#[reflect(Debug, FromWorld, Clone)]
580pub struct OrthographicProjection {
581    /// The distance of the near clipping plane in world units.
582    ///
583    /// Objects closer than this will not be rendered.
584    ///
585    /// Defaults to `0.0`
586    pub near: f32,
587    /// The distance of the far clipping plane in world units.
588    ///
589    /// Objects further than this will not be rendered.
590    ///
591    /// Defaults to `1000.0`
592    pub far: f32,
593    /// Specifies the origin of the viewport as a normalized position from 0 to 1, where (0, 0) is the bottom left
594    /// and (1, 1) is the top right. This determines where the camera's position sits inside the viewport.
595    ///
596    /// When the projection scales due to viewport resizing, the position of the camera, and thereby `viewport_origin`,
597    /// remains at the same relative point.
598    ///
599    /// Consequently, this is pivot point when scaling. With a bottom left pivot, the projection will expand
600    /// upwards and to the right. With a top right pivot, the projection will expand downwards and to the left.
601    /// Values in between will caused the projection to scale proportionally on each axis.
602    ///
603    /// Defaults to `(0.5, 0.5)`, which makes scaling affect opposite sides equally, keeping the center
604    /// point of the viewport centered.
605    pub viewport_origin: Vec2,
606    /// How the projection will scale to the viewport.
607    ///
608    /// Defaults to [`ScalingMode::WindowSize`],
609    /// and works in concert with [`OrthographicProjection::scale`] to determine the final effect.
610    ///
611    /// For simplicity, zooming should be done by changing [`OrthographicProjection::scale`],
612    /// rather than changing the parameters of the scaling mode.
613    pub scaling_mode: ScalingMode,
614    /// Scales the projection.
615    ///
616    /// As scale increases, the apparent size of objects decreases, and vice versa.
617    ///
618    /// Note: scaling can be set by [`scaling_mode`](Self::scaling_mode) as well.
619    /// This parameter scales on top of that.
620    ///
621    /// This property is particularly useful in implementing zoom functionality.
622    ///
623    /// Defaults to `1.0`, which under standard settings corresponds to a 1:1 mapping of world units to rendered pixels.
624    /// See [`ScalingMode::WindowSize`] for more information.
625    pub scale: f32,
626    /// The area that the projection covers relative to `viewport_origin`.
627    ///
628    /// Bevy's `camera_system` automatically
629    /// updates this value when the viewport is resized depending on `OrthographicProjection`'s other fields.
630    /// In this case, `area` should not be manually modified.
631    ///
632    /// It may be necessary to set this manually for shadow projections and such.
633    pub area: Rect,
634}
635
636impl CameraProjection for OrthographicProjection {
637    fn get_clip_from_view(&self) -> Mat4 {
638        Mat4::orthographic_rh(
639            self.area.min.x,
640            self.area.max.x,
641            self.area.min.y,
642            self.area.max.y,
643            // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0]
644            // This is for interoperability with pipelines using infinite reverse perspective projections.
645            self.far,
646            self.near,
647        )
648    }
649
650    fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 {
651        let full_width = sub_view.full_size.x as f32;
652        let full_height = sub_view.full_size.y as f32;
653        let offset_x = sub_view.offset.x;
654        let offset_y = sub_view.offset.y;
655        let sub_width = sub_view.size.x as f32;
656        let sub_height = sub_view.size.y as f32;
657
658        let full_aspect = full_width / full_height;
659
660        // Base the vertical size on self.area and adjust the horizontal size
661        let top = self.area.max.y;
662        let bottom = self.area.min.y;
663        let ortho_height = top - bottom;
664        let ortho_width = ortho_height * full_aspect;
665
666        // Center the orthographic area horizontally
667        let center_x = (self.area.max.x + self.area.min.x) / 2.0;
668        let left = center_x - ortho_width / 2.0;
669        let right = center_x + ortho_width / 2.0;
670
671        // Calculate scaling factors
672        let scale_w = (right - left) / full_width;
673        let scale_h = (top - bottom) / full_height;
674
675        // Calculate the new orthographic bounds
676        let left_prime = left + scale_w * offset_x;
677        let right_prime = left_prime + scale_w * sub_width;
678        let top_prime = top - scale_h * offset_y;
679        let bottom_prime = top_prime - scale_h * sub_height;
680
681        Mat4::orthographic_rh(
682            left_prime,
683            right_prime,
684            bottom_prime,
685            top_prime,
686            // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0]
687            // This is for interoperability with pipelines using infinite reverse perspective projections.
688            self.far,
689            self.near,
690        )
691    }
692
693    fn update(&mut self, width: f32, height: f32) {
694        let (projection_width, projection_height) = match self.scaling_mode {
695            ScalingMode::WindowSize => (width, height),
696            ScalingMode::AutoMin {
697                min_width,
698                min_height,
699            } => {
700                // Compare Pixels of current width and minimal height and Pixels of minimal width with current height.
701                // 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.
702                if width * min_height > min_width * height {
703                    (width * min_height / height, min_height)
704                } else {
705                    (min_width, height * min_width / width)
706                }
707            }
708            ScalingMode::AutoMax {
709                max_width,
710                max_height,
711            } => {
712                // Compare Pixels of current width and maximal height and Pixels of maximal width with current height.
713                // 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.
714                if width * max_height < max_width * height {
715                    (width * max_height / height, max_height)
716                } else {
717                    (max_width, height * max_width / width)
718                }
719            }
720            ScalingMode::FixedVertical { viewport_height } => {
721                (width * viewport_height / height, viewport_height)
722            }
723            ScalingMode::FixedHorizontal { viewport_width } => {
724                (viewport_width, height * viewport_width / width)
725            }
726            ScalingMode::Fixed { width, height } => (width, height),
727        };
728
729        let origin_x = projection_width * self.viewport_origin.x;
730        let origin_y = projection_height * self.viewport_origin.y;
731
732        self.area = Rect::new(
733            self.scale * -origin_x,
734            self.scale * -origin_y,
735            self.scale * (projection_width - origin_x),
736            self.scale * (projection_height - origin_y),
737        );
738    }
739
740    fn far(&self) -> f32 {
741        self.far
742    }
743
744    fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
745        let area = self.area;
746        // NOTE: These vertices are in the specific order required by [`calculate_cascade`].
747        [
748            Vec3A::new(area.max.x, area.min.y, z_near), // bottom right
749            Vec3A::new(area.max.x, area.max.y, z_near), // top right
750            Vec3A::new(area.min.x, area.max.y, z_near), // top left
751            Vec3A::new(area.min.x, area.min.y, z_near), // bottom left
752            Vec3A::new(area.max.x, area.min.y, z_far),  // bottom right
753            Vec3A::new(area.max.x, area.max.y, z_far),  // top right
754            Vec3A::new(area.min.x, area.max.y, z_far),  // top left
755            Vec3A::new(area.min.x, area.min.y, z_far),  // bottom left
756        ]
757    }
758}
759
760impl FromWorld for OrthographicProjection {
761    fn from_world(_world: &mut World) -> Self {
762        OrthographicProjection::default_3d()
763    }
764}
765
766impl OrthographicProjection {
767    /// Returns the default orthographic projection for a 2D context.
768    ///
769    /// The near plane is set to a negative value so that the camera can still
770    /// render the scene when using positive z coordinates to order foreground elements.
771    pub fn default_2d() -> Self {
772        OrthographicProjection {
773            near: -1000.0,
774            ..OrthographicProjection::default_3d()
775        }
776    }
777
778    /// Returns the default orthographic projection for a 3D context.
779    ///
780    /// The near plane is set to 0.0 so that the camera doesn't render
781    /// objects that are behind it.
782    pub fn default_3d() -> Self {
783        OrthographicProjection {
784            scale: 1.0,
785            near: 0.0,
786            far: 1000.0,
787            viewport_origin: Vec2::new(0.5, 0.5),
788            scaling_mode: ScalingMode::WindowSize,
789            area: Rect::new(-1.0, -1.0, 1.0, 1.0),
790        }
791    }
792}
793
794#[cfg(test)]
795mod tests {
796    use super::*;
797
798    /// The projection matrix must respect a custom near plane value.
799    /// A smaller near plane should produce a different matrix than the default.
800    #[test]
801    fn custom_near_plane_is_respected() {
802        let default_proj = PerspectiveProjection::default();
803        let custom_proj = PerspectiveProjection {
804            near: 0.01,
805            ..Default::default()
806        };
807
808        let default_matrix = default_proj.get_clip_from_view();
809        let custom_matrix = custom_proj.get_clip_from_view();
810
811        assert_ne!(
812            default_matrix, custom_matrix,
813            "A near plane of 0.01 should produce a different projection matrix than the default 0.1"
814        );
815
816        // The w_axis.z element of an infinite reverse perspective matrix equals
817        // the near plane distance. Verify it matches what we requested.
818        assert_eq!(custom_matrix.w_axis.z, 0.01);
819    }
820}