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}