use core::fmt::Debug;1use core::ops::{Deref, DerefMut};23use crate::{primitives::Frustum, visibility::VisibilitySystems};4use bevy_app::{App, Plugin, PostUpdate};5use bevy_ecs::prelude::*;6use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4};7use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize};8use bevy_transform::{components::GlobalTransform, TransformSystems};9use derive_more::derive::From;10use serde::{Deserialize, Serialize};1112/// Adds [`Camera`](crate::camera::Camera) driver systems for a given projection type.13///14/// If you are using `bevy_pbr`, then you need to add `PbrProjectionPlugin` along with this.15#[derive(Default)]16pub struct CameraProjectionPlugin;1718impl Plugin for CameraProjectionPlugin {19fn build(&self, app: &mut App) {20app.add_systems(21PostUpdate,22crate::visibility::update_frusta23.in_set(VisibilitySystems::UpdateFrusta)24.after(TransformSystems::Propagate),25);26}27}2829/// Describes a type that can generate a projection matrix, allowing it to be added to a30/// [`Camera`]'s [`Projection`] component.31///32/// Once implemented, the projection can be added to a camera using [`Projection::custom`].33///34/// The projection will be automatically updated as the render area is resized. This is useful when,35/// for example, a projection type has a field like `fov` that should change when the window width36/// is changed but not when the height changes.37///38/// This trait is implemented by bevy's built-in projections [`PerspectiveProjection`] and39/// [`OrthographicProjection`].40///41/// [`Camera`]: crate::camera::Camera42pub trait CameraProjection {43/// Generate the projection matrix.44fn get_clip_from_view(&self) -> Mat4;4546/// Generate the projection matrix for a [`SubCameraView`](super::SubCameraView).47fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4;4849/// When the area this camera renders to changes dimensions, this method will be automatically50/// called. Use this to update any projection properties that depend on the aspect ratio or51/// dimensions of the render area.52fn update(&mut self, width: f32, height: f32);5354/// The far plane distance of the projection.55fn far(&self) -> f32;5657/// The eight corners of the camera frustum, as defined by this projection.58///59/// The corners should be provided in the following order: first the bottom right, top right,60/// top left, bottom left for the near plane, then similar for the far plane.61// TODO: This seems somewhat redundant with `compute_frustum`, and similarly should be possible62// to compute with a default impl.63fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8];6465/// Compute camera frustum for camera with given projection and transform.66///67/// This code is called by [`update_frusta`](crate::visibility::update_frusta) system68/// for each camera to update its frustum.69fn compute_frustum(&self, camera_transform: &GlobalTransform) -> Frustum {70let clip_from_world = self.get_clip_from_view() * camera_transform.affine().inverse();71Frustum::from_clip_from_world_custom_far(72&clip_from_world,73&camera_transform.translation(),74&camera_transform.back(),75self.far(),76)77}78}7980mod sealed {81use super::CameraProjection;8283/// A wrapper trait to make it possible to implement Clone for boxed [`CameraProjection`][`super::CameraProjection`]84/// trait objects, without breaking object safety rules by making it `Sized`. Additional bounds85/// are included for downcasting, and fulfilling the trait bounds on `Projection`.86pub trait DynCameraProjection:87CameraProjection + core::fmt::Debug + Send + Sync + downcast_rs::Downcast88{89fn clone_box(&self) -> Box<dyn DynCameraProjection>;90}9192downcast_rs::impl_downcast!(DynCameraProjection);9394impl<T> DynCameraProjection for T95where96T: 'static + CameraProjection + core::fmt::Debug + Send + Sync + Clone,97{98fn clone_box(&self) -> Box<dyn DynCameraProjection> {99Box::new(self.clone())100}101}102}103104/// Holds a dynamic [`CameraProjection`] trait object. Use [`Projection::custom()`] to construct a105/// custom projection.106///107/// The contained dynamic object can be downcast into a static type using [`CustomProjection::get`].108#[derive(Debug, Reflect)]109#[reflect(Default, Clone)]110pub struct CustomProjection {111#[reflect(ignore)]112dyn_projection: Box<dyn sealed::DynCameraProjection>,113}114115impl Default for CustomProjection {116fn default() -> Self {117Self {118dyn_projection: Box::new(PerspectiveProjection::default()),119}120}121}122123impl Clone for CustomProjection {124fn clone(&self) -> Self {125Self {126dyn_projection: self.dyn_projection.clone_box(),127}128}129}130131impl CustomProjection {132/// Returns a reference to the [`CameraProjection`] `P`.133///134/// Returns `None` if this dynamic object is not a projection of type `P`.135///136/// ```137/// # use bevy_camera::{Projection, PerspectiveProjection};138/// // For simplicity's sake, use perspective as a custom projection:139/// let projection = Projection::custom(PerspectiveProjection::default());140/// let Projection::Custom(custom) = projection else { return };141///142/// // At this point the projection type is erased.143/// // We can use `get()` if we know what kind of projection we have.144/// let perspective = custom.get::<PerspectiveProjection>().unwrap();145///146/// assert_eq!(perspective.fov, PerspectiveProjection::default().fov);147/// ```148pub fn get<P>(&self) -> Option<&P>149where150P: CameraProjection + Debug + Send + Sync + Clone + 'static,151{152self.dyn_projection.downcast_ref()153}154155/// Returns a mutable reference to the [`CameraProjection`] `P`.156///157/// Returns `None` if this dynamic object is not a projection of type `P`.158///159/// ```160/// # use bevy_camera::{Projection, PerspectiveProjection};161/// // For simplicity's sake, use perspective as a custom projection:162/// let mut projection = Projection::custom(PerspectiveProjection::default());163/// let Projection::Custom(mut custom) = projection else { return };164///165/// // At this point the projection type is erased.166/// // We can use `get_mut()` if we know what kind of projection we have.167/// let perspective = custom.get_mut::<PerspectiveProjection>().unwrap();168///169/// assert_eq!(perspective.fov, PerspectiveProjection::default().fov);170/// perspective.fov = 1.0;171/// ```172pub fn get_mut<P>(&mut self) -> Option<&mut P>173where174P: CameraProjection + Debug + Send + Sync + Clone + 'static,175{176self.dyn_projection.downcast_mut()177}178}179180impl Deref for CustomProjection {181type Target = dyn CameraProjection;182183fn deref(&self) -> &Self::Target {184self.dyn_projection.as_ref()185}186}187188impl DerefMut for CustomProjection {189fn deref_mut(&mut self) -> &mut Self::Target {190self.dyn_projection.as_mut()191}192}193194/// Component that defines how to compute a [`Camera`]'s projection matrix.195///196/// Common projections, like perspective and orthographic, are provided out of the box to handle the197/// majority of use cases. Custom projections can be added using the [`CameraProjection`] trait and198/// the [`Projection::custom`] constructor.199///200/// ## What's a projection?201///202/// A camera projection essentially describes how 3d points from the point of view of a camera are203/// projected onto a 2d screen. This is where properties like a camera's field of view are defined.204/// More specifically, a projection is a 4x4 matrix that transforms points from view space (the205/// point of view of the camera) into clip space. Clip space is almost, but not quite, equivalent to206/// the rectangle that is rendered to your screen, with a depth axis. Any points that land outside207/// the bounds of this cuboid are "clipped" and not rendered.208///209/// You can also think of the projection as the thing that describes the shape of a camera's210/// frustum: the volume in 3d space that is visible to a camera.211///212/// [`Camera`]: crate::camera::Camera213#[derive(Component, Debug, Clone, Reflect, From)]214#[reflect(Component, Default, Debug, Clone)]215pub enum Projection {216Perspective(PerspectiveProjection),217Orthographic(OrthographicProjection),218Custom(CustomProjection),219}220221impl Projection {222/// Construct a new custom camera projection from a type that implements [`CameraProjection`].223pub fn custom<P>(projection: P) -> Self224where225// Implementation note: pushing these trait bounds all the way out to this function makes226// errors nice for users. If a trait is missing, they will get a helpful error telling them227// that, say, the `Debug` implementation is missing. Wrapping these traits behind a super228// trait or some other indirection will make the errors harder to understand.229//230// For example, we don't use the `DynCameraProjection` trait bound, because it is not the231// trait the user should be implementing - they only need to worry about implementing232// `CameraProjection`.233P: CameraProjection + Debug + Send + Sync + Clone + 'static,234{235Projection::Custom(CustomProjection {236dyn_projection: Box::new(projection),237})238}239240/// Check if the projection is perspective.241/// For [`CustomProjection`], this checks if the projection matrix's w-axis's w is 0.0.242pub fn is_perspective(&self) -> bool {243match self {244Projection::Perspective(_) => true,245Projection::Orthographic(_) => false,246Projection::Custom(projection) => projection.get_clip_from_view().w_axis.w == 0.0,247}248}249}250251impl Deref for Projection {252type Target = dyn CameraProjection;253254fn deref(&self) -> &Self::Target {255match self {256Projection::Perspective(projection) => projection,257Projection::Orthographic(projection) => projection,258Projection::Custom(projection) => projection.deref(),259}260}261}262263impl DerefMut for Projection {264fn deref_mut(&mut self) -> &mut Self::Target {265match self {266Projection::Perspective(projection) => projection,267Projection::Orthographic(projection) => projection,268Projection::Custom(projection) => projection.deref_mut(),269}270}271}272273impl Default for Projection {274fn default() -> Self {275Projection::Perspective(Default::default())276}277}278279/// A 3D camera projection in which distant objects appear smaller than close objects.280#[derive(Debug, Clone, Reflect)]281#[reflect(Default, Debug, Clone)]282pub struct PerspectiveProjection {283/// The vertical field of view (FOV) in radians.284///285/// Defaults to a value of π/4 radians or 45 degrees.286pub fov: f32,287288/// The aspect ratio (width divided by height) of the viewing frustum.289///290/// Bevy's `camera_system` automatically updates this value when the aspect ratio291/// of the associated window changes.292///293/// Defaults to a value of `1.0`.294pub aspect_ratio: f32,295296/// The distance from the camera in world units of the viewing frustum's near plane.297///298/// Objects closer to the camera than this value will not be visible.299///300/// Defaults to a value of `0.1`.301pub near: f32,302303/// The distance from the camera in world units of the viewing frustum's far plane.304///305/// Objects farther from the camera than this value will not be visible.306///307/// Defaults to a value of `1000.0`.308pub far: f32,309}310311impl CameraProjection for PerspectiveProjection {312fn get_clip_from_view(&self) -> Mat4 {313Mat4::perspective_infinite_reverse_rh(self.fov, self.aspect_ratio, self.near)314}315316fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 {317let full_width = sub_view.full_size.x as f32;318let full_height = sub_view.full_size.y as f32;319let sub_width = sub_view.size.x as f32;320let sub_height = sub_view.size.y as f32;321let offset_x = sub_view.offset.x;322// Y-axis increases from top to bottom323let offset_y = full_height - (sub_view.offset.y + sub_height);324325let full_aspect = full_width / full_height;326327// Original frustum parameters328let top = self.near * ops::tan(0.5 * self.fov);329let bottom = -top;330let right = top * full_aspect;331let left = -right;332333// Calculate scaling factors334let width = right - left;335let height = top - bottom;336337// Calculate the new frustum parameters338let left_prime = left + (width * offset_x) / full_width;339let right_prime = left + (width * (offset_x + sub_width)) / full_width;340let bottom_prime = bottom + (height * offset_y) / full_height;341let top_prime = bottom + (height * (offset_y + sub_height)) / full_height;342343// Compute the new projection matrix344let x = (2.0 * self.near) / (right_prime - left_prime);345let y = (2.0 * self.near) / (top_prime - bottom_prime);346let a = (right_prime + left_prime) / (right_prime - left_prime);347let b = (top_prime + bottom_prime) / (top_prime - bottom_prime);348349Mat4::from_cols(350Vec4::new(x, 0.0, 0.0, 0.0),351Vec4::new(0.0, y, 0.0, 0.0),352Vec4::new(a, b, 0.0, -1.0),353Vec4::new(0.0, 0.0, self.near, 0.0),354)355}356357fn update(&mut self, width: f32, height: f32) {358self.aspect_ratio = AspectRatio::try_new(width, height)359.expect("Failed to update PerspectiveProjection: width and height must be positive, non-zero values")360.ratio();361}362363fn far(&self) -> f32 {364self.far365}366367fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {368let tan_half_fov = ops::tan(self.fov / 2.);369let a = z_near.abs() * tan_half_fov;370let b = z_far.abs() * tan_half_fov;371let aspect_ratio = self.aspect_ratio;372// NOTE: These vertices are in the specific order required by [`calculate_cascade`].373[374Vec3A::new(a * aspect_ratio, -a, z_near), // bottom right375Vec3A::new(a * aspect_ratio, a, z_near), // top right376Vec3A::new(-a * aspect_ratio, a, z_near), // top left377Vec3A::new(-a * aspect_ratio, -a, z_near), // bottom left378Vec3A::new(b * aspect_ratio, -b, z_far), // bottom right379Vec3A::new(b * aspect_ratio, b, z_far), // top right380Vec3A::new(-b * aspect_ratio, b, z_far), // top left381Vec3A::new(-b * aspect_ratio, -b, z_far), // bottom left382]383}384}385386impl Default for PerspectiveProjection {387fn default() -> Self {388PerspectiveProjection {389fov: core::f32::consts::PI / 4.0,390near: 0.1,391far: 1000.0,392aspect_ratio: 1.0,393}394}395}396397/// Scaling mode for [`OrthographicProjection`].398///399/// The effect of these scaling modes are combined with the [`OrthographicProjection::scale`] property.400///401/// For example, if the scaling mode is `ScalingMode::Fixed { width: 100.0, height: 300 }` and the scale is `2.0`,402/// the projection will be 200 world units wide and 600 world units tall.403///404/// # Examples405///406/// Configure the orthographic projection to two world units per window height:407///408/// ```409/// # use bevy_camera::{OrthographicProjection, Projection, ScalingMode};410/// let projection = Projection::Orthographic(OrthographicProjection {411/// scaling_mode: ScalingMode::FixedVertical { viewport_height: 2.0 },412/// ..OrthographicProjection::default_2d()413/// });414/// ```415#[derive(Default, Debug, Clone, Copy, Reflect, Serialize, Deserialize)]416#[reflect(Serialize, Deserialize, Default, Clone)]417pub enum ScalingMode {418/// Match the viewport size.419///420/// With a scale of 1, lengths in world units will map 1:1 with the number of pixels used to render it.421/// For example, if we have a 64x64 sprite with a [`Transform::scale`](bevy_transform::prelude::Transform) of 1.0,422/// no custom size and no inherited scale, the sprite will be 64 world units wide and 64 world units tall.423/// When rendered with [`OrthographicProjection::scaling_mode`] set to `WindowSize` when the window scale factor is 1424/// the sprite will be rendered at 64 pixels wide and 64 pixels tall.425///426/// Changing any of these properties will multiplicatively affect the final size.427#[default]428WindowSize,429/// Manually specify the projection's size, ignoring window resizing. The image will stretch.430///431/// Arguments describe the area of the world that is shown (in world units).432Fixed { width: f32, height: f32 },433/// Keeping the aspect ratio while the axes can't be smaller than given minimum.434///435/// Arguments are in world units.436AutoMin { min_width: f32, min_height: f32 },437/// Keeping the aspect ratio while the axes can't be bigger than given maximum.438///439/// Arguments are in world units.440AutoMax { max_width: f32, max_height: f32 },441/// Keep the projection's height constant; width will be adjusted to match aspect ratio.442///443/// The argument is the desired height of the projection in world units.444FixedVertical { viewport_height: f32 },445/// Keep the projection's width constant; height will be adjusted to match aspect ratio.446///447/// The argument is the desired width of the projection in world units.448FixedHorizontal { viewport_width: f32 },449}450451/// Project a 3D space onto a 2D surface using parallel lines, i.e., unlike [`PerspectiveProjection`],452/// the size of objects remains the same regardless of their distance to the camera.453///454/// The volume contained in the projection is called the *view frustum*. Since the viewport is rectangular455/// and projection lines are parallel, the view frustum takes the shape of a cuboid.456///457/// Note that the scale of the projection and the apparent size of objects are inversely proportional.458/// As the size of the projection increases, the size of objects decreases.459///460/// # Examples461///462/// Configure the orthographic projection to one world unit per 100 window pixels:463///464/// ```465/// # use bevy_camera::{OrthographicProjection, Projection, ScalingMode};466/// let projection = Projection::Orthographic(OrthographicProjection {467/// scaling_mode: ScalingMode::WindowSize,468/// scale: 0.01,469/// ..OrthographicProjection::default_2d()470/// });471/// ```472#[derive(Debug, Clone, Reflect)]473#[reflect(Debug, FromWorld, Clone)]474pub struct OrthographicProjection {475/// The distance of the near clipping plane in world units.476///477/// Objects closer than this will not be rendered.478///479/// Defaults to `0.0`480pub near: f32,481/// The distance of the far clipping plane in world units.482///483/// Objects further than this will not be rendered.484///485/// Defaults to `1000.0`486pub far: f32,487/// Specifies the origin of the viewport as a normalized position from 0 to 1, where (0, 0) is the bottom left488/// and (1, 1) is the top right. This determines where the camera's position sits inside the viewport.489///490/// When the projection scales due to viewport resizing, the position of the camera, and thereby `viewport_origin`,491/// remains at the same relative point.492///493/// Consequently, this is pivot point when scaling. With a bottom left pivot, the projection will expand494/// upwards and to the right. With a top right pivot, the projection will expand downwards and to the left.495/// Values in between will caused the projection to scale proportionally on each axis.496///497/// Defaults to `(0.5, 0.5)`, which makes scaling affect opposite sides equally, keeping the center498/// point of the viewport centered.499pub viewport_origin: Vec2,500/// How the projection will scale to the viewport.501///502/// Defaults to [`ScalingMode::WindowSize`],503/// and works in concert with [`OrthographicProjection::scale`] to determine the final effect.504///505/// For simplicity, zooming should be done by changing [`OrthographicProjection::scale`],506/// rather than changing the parameters of the scaling mode.507pub scaling_mode: ScalingMode,508/// Scales the projection.509///510/// As scale increases, the apparent size of objects decreases, and vice versa.511///512/// Note: scaling can be set by [`scaling_mode`](Self::scaling_mode) as well.513/// This parameter scales on top of that.514///515/// This property is particularly useful in implementing zoom functionality.516///517/// Defaults to `1.0`, which under standard settings corresponds to a 1:1 mapping of world units to rendered pixels.518/// See [`ScalingMode::WindowSize`] for more information.519pub scale: f32,520/// The area that the projection covers relative to `viewport_origin`.521///522/// Bevy's `camera_system` automatically523/// updates this value when the viewport is resized depending on `OrthographicProjection`'s other fields.524/// In this case, `area` should not be manually modified.525///526/// It may be necessary to set this manually for shadow projections and such.527pub area: Rect,528}529530impl CameraProjection for OrthographicProjection {531fn get_clip_from_view(&self) -> Mat4 {532Mat4::orthographic_rh(533self.area.min.x,534self.area.max.x,535self.area.min.y,536self.area.max.y,537// NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0]538// This is for interoperability with pipelines using infinite reverse perspective projections.539self.far,540self.near,541)542}543544fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 {545let full_width = sub_view.full_size.x as f32;546let full_height = sub_view.full_size.y as f32;547let offset_x = sub_view.offset.x;548let offset_y = sub_view.offset.y;549let sub_width = sub_view.size.x as f32;550let sub_height = sub_view.size.y as f32;551552let full_aspect = full_width / full_height;553554// Base the vertical size on self.area and adjust the horizontal size555let top = self.area.max.y;556let bottom = self.area.min.y;557let ortho_height = top - bottom;558let ortho_width = ortho_height * full_aspect;559560// Center the orthographic area horizontally561let center_x = (self.area.max.x + self.area.min.x) / 2.0;562let left = center_x - ortho_width / 2.0;563let right = center_x + ortho_width / 2.0;564565// Calculate scaling factors566let scale_w = (right - left) / full_width;567let scale_h = (top - bottom) / full_height;568569// Calculate the new orthographic bounds570let left_prime = left + scale_w * offset_x;571let right_prime = left_prime + scale_w * sub_width;572let top_prime = top - scale_h * offset_y;573let bottom_prime = top_prime - scale_h * sub_height;574575Mat4::orthographic_rh(576left_prime,577right_prime,578bottom_prime,579top_prime,580// NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0]581// This is for interoperability with pipelines using infinite reverse perspective projections.582self.far,583self.near,584)585}586587fn update(&mut self, width: f32, height: f32) {588let (projection_width, projection_height) = match self.scaling_mode {589ScalingMode::WindowSize => (width, height),590ScalingMode::AutoMin {591min_width,592min_height,593} => {594// Compare Pixels of current width and minimal height and Pixels of minimal width with current height.595// 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.596if width * min_height > min_width * height {597(width * min_height / height, min_height)598} else {599(min_width, height * min_width / width)600}601}602ScalingMode::AutoMax {603max_width,604max_height,605} => {606// Compare Pixels of current width and maximal height and Pixels of maximal width with current height.607// 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.608if width * max_height < max_width * height {609(width * max_height / height, max_height)610} else {611(max_width, height * max_width / width)612}613}614ScalingMode::FixedVertical { viewport_height } => {615(width * viewport_height / height, viewport_height)616}617ScalingMode::FixedHorizontal { viewport_width } => {618(viewport_width, height * viewport_width / width)619}620ScalingMode::Fixed { width, height } => (width, height),621};622623let origin_x = projection_width * self.viewport_origin.x;624let origin_y = projection_height * self.viewport_origin.y;625626self.area = Rect::new(627self.scale * -origin_x,628self.scale * -origin_y,629self.scale * (projection_width - origin_x),630self.scale * (projection_height - origin_y),631);632}633634fn far(&self) -> f32 {635self.far636}637638fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {639let area = self.area;640// NOTE: These vertices are in the specific order required by [`calculate_cascade`].641[642Vec3A::new(area.max.x, area.min.y, z_near), // bottom right643Vec3A::new(area.max.x, area.max.y, z_near), // top right644Vec3A::new(area.min.x, area.max.y, z_near), // top left645Vec3A::new(area.min.x, area.min.y, z_near), // bottom left646Vec3A::new(area.max.x, area.min.y, z_far), // bottom right647Vec3A::new(area.max.x, area.max.y, z_far), // top right648Vec3A::new(area.min.x, area.max.y, z_far), // top left649Vec3A::new(area.min.x, area.min.y, z_far), // bottom left650]651}652}653654impl FromWorld for OrthographicProjection {655fn from_world(_world: &mut World) -> Self {656OrthographicProjection::default_3d()657}658}659660impl OrthographicProjection {661/// Returns the default orthographic projection for a 2D context.662///663/// The near plane is set to a negative value so that the camera can still664/// render the scene when using positive z coordinates to order foreground elements.665pub fn default_2d() -> Self {666OrthographicProjection {667near: -1000.0,668..OrthographicProjection::default_3d()669}670}671672/// Returns the default orthographic projection for a 3D context.673///674/// The near plane is set to 0.0 so that the camera doesn't render675/// objects that are behind it.676pub fn default_3d() -> Self {677OrthographicProjection {678scale: 1.0,679near: 0.0,680far: 1000.0,681viewport_origin: Vec2::new(0.5, 0.5),682scaling_mode: ScalingMode::WindowSize,683area: Rect::new(-1.0, -1.0, 1.0, 1.0),684}685}686}687688689