bevy_render/camera/
projection.rs

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