use std::marker::PhantomData; use bevy_app::{App, Plugin, PostStartup, PostUpdate}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; use bevy_math::{Mat4, Rect, Vec2}; use bevy_reflect::{ std_traits::ReflectDefault, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectSerialize, }; use serde::{Deserialize, Serialize}; /// Adds [`Camera`](crate::camera::Camera) driver systems for a given projection type. pub struct CameraProjectionPlugin(PhantomData); impl Default for CameraProjectionPlugin { fn default() -> Self { Self(Default::default()) } } /// Label for [`camera_system`], shared across all `T`. /// /// [`camera_system`]: crate::camera::camera_system #[derive(SystemSet, Clone, Eq, PartialEq, Hash, Debug)] pub struct CameraUpdateSystem; impl Plugin for CameraProjectionPlugin { fn build(&self, app: &mut App) { app.register_type::() .add_systems( PostStartup, crate::camera::camera_system:: .in_set(CameraUpdateSystem) // We assume that each camera will only have one projection, // so we can ignore ambiguities with all other monomorphizations. // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. .ambiguous_with(CameraUpdateSystem), ) .add_systems( PostUpdate, crate::camera::camera_system:: .in_set(CameraUpdateSystem) // We assume that each camera will only have one projection, // so we can ignore ambiguities with all other monomorphizations. // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. .ambiguous_with(CameraUpdateSystem), ); } } /// Trait to control the projection matrix of a camera. /// /// Components implementing this trait are automatically polled for changes, and used /// to recompute the camera projection matrix of the [`Camera`] component attached to /// the same entity as the component implementing this trait. /// /// [`Camera`]: crate::camera::Camera pub trait CameraProjection { fn get_projection_matrix(&self) -> Mat4; fn update(&mut self, width: f32, height: f32); fn far(&self) -> f32; } /// A configurable [`CameraProjection`] that can select its projection type at runtime. #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default)] pub enum Projection { Perspective(PerspectiveProjection), Orthographic(OrthographicProjection), } impl From for Projection { fn from(p: PerspectiveProjection) -> Self { Self::Perspective(p) } } impl From for Projection { fn from(p: OrthographicProjection) -> Self { Self::Orthographic(p) } } impl CameraProjection for Projection { fn get_projection_matrix(&self) -> Mat4 { match self { Projection::Perspective(projection) => projection.get_projection_matrix(), Projection::Orthographic(projection) => projection.get_projection_matrix(), } } fn update(&mut self, width: f32, height: f32) { match self { Projection::Perspective(projection) => projection.update(width, height), Projection::Orthographic(projection) => projection.update(width, height), } } fn far(&self) -> f32 { match self { Projection::Perspective(projection) => projection.far(), Projection::Orthographic(projection) => projection.far(), } } } impl Default for Projection { fn default() -> Self { Projection::Perspective(Default::default()) } } /// A 3D camera projection in which distant objects appear smaller than close objects. #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default)] pub struct PerspectiveProjection { /// The vertical field of view (FOV) in radians. /// /// Defaults to a value of π/4 radians or 45 degrees. pub fov: f32, /// The aspect ratio (width divided by height) of the viewing frustum. /// /// Bevy's [`camera_system`](crate::camera::camera_system) automatically /// updates this value when the aspect ratio of the associated window changes. /// /// Defaults to a value of `1.0`. pub aspect_ratio: f32, /// The distance from the camera in world units of the viewing frustum's near plane. /// /// Objects closer to the camera than this value will not be visible. /// /// Defaults to a value of `0.1`. pub near: f32, /// The distance from the camera in world units of the viewing frustum's far plane. /// /// Objects farther from the camera than this value will not be visible. /// /// Defaults to a value of `1000.0`. pub far: f32, } impl CameraProjection for PerspectiveProjection { fn get_projection_matrix(&self) -> Mat4 { Mat4::perspective_infinite_reverse_rh(self.fov, self.aspect_ratio, self.near) } fn update(&mut self, width: f32, height: f32) { self.aspect_ratio = width / height; } fn far(&self) -> f32 { self.far } } impl Default for PerspectiveProjection { fn default() -> Self { PerspectiveProjection { fov: std::f32::consts::PI / 4.0, near: 0.1, far: 1000.0, aspect_ratio: 1.0, } } } #[derive(Debug, Clone, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize)] pub enum ScalingMode { /// Manually specify the projection's size, ignoring window resizing. The image will stretch. /// Arguments are in world units. Fixed { width: f32, height: f32 }, /// Match the viewport size. /// The argument is the number of pixels that equals one world unit. WindowSize(f32), /// Keeping the aspect ratio while the axes can't be smaller than given minimum. /// Arguments are in world units. AutoMin { min_width: f32, min_height: f32 }, /// Keeping the aspect ratio while the axes can't be bigger than given maximum. /// Arguments are in world units. AutoMax { max_width: f32, max_height: f32 }, /// Keep the projection's height constant; width will be adjusted to match aspect ratio. /// The argument is the desired height of the projection in world units. FixedVertical(f32), /// Keep the projection's width constant; height will be adjusted to match aspect ratio. /// The argument is the desired width of the projection in world units. FixedHorizontal(f32), } /// Project a 3D space onto a 2D surface using parallel lines, i.e., unlike [`PerspectiveProjection`], /// the size of objects remains the same regardless of their distance to the camera. /// /// The volume contained in the projection is called the *view frustum*. Since the viewport is rectangular /// and projection lines are parallel, the view frustum takes the shape of a cuboid. /// /// Note that the scale of the projection and the apparent size of objects are inversely proportional. /// As the size of the projection increases, the size of objects decreases. #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default)] pub struct OrthographicProjection { /// The distance of the near clipping plane in world units. /// /// Objects closer than this will not be rendered. /// /// Defaults to `0.0` pub near: f32, /// The distance of the far clipping plane in world units. /// /// Objects further than this will not be rendered. /// /// Defaults to `1000.0` pub far: f32, /// Specifies the origin of the viewport as a normalized position from 0 to 1, where (0, 0) is the bottom left /// and (1, 1) is the top right. This determines where the camera's position sits inside the viewport. /// /// When the projection scales due to viewport resizing, the position of the camera, and thereby `viewport_origin`, /// remains at the same relative point. /// /// Consequently, this is pivot point when scaling. With a bottom left pivot, the projection will expand /// upwards and to the right. With a top right pivot, the projection will expand downwards and to the left. /// Values in between will caused the projection to scale proportionally on each axis. /// /// Defaults to `(0.5, 0.5)`, which makes scaling affect opposite sides equally, keeping the center /// point of the viewport centered. pub viewport_origin: Vec2, /// How the projection will scale when the viewport is resized. /// /// Defaults to `ScalingMode::WindowSize(1.0)` pub scaling_mode: ScalingMode, /// Scales the projection in world units. /// /// As scale increases, the apparent size of objects decreases, and vice versa. /// /// Defaults to `1.0` pub scale: f32, /// The area that the projection covers relative to `viewport_origin`. /// /// Bevy's [`camera_system`](crate::camera::camera_system) automatically /// updates this value when the viewport is resized depending on `OrthographicProjection`'s other fields. /// In this case, `area` should not be manually modified. /// /// It may be necessary to set this manually for shadow projections and such. pub area: Rect, } impl CameraProjection for OrthographicProjection { fn get_projection_matrix(&self) -> Mat4 { Mat4::orthographic_rh( self.area.min.x, self.area.max.x, self.area.min.y, self.area.max.y, // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0] // This is for interoperability with pipelines using infinite reverse perspective projections. self.far, self.near, ) } fn update(&mut self, width: f32, height: f32) { let (projection_width, projection_height) = match self.scaling_mode { ScalingMode::WindowSize(pixel_scale) => (width / pixel_scale, height / pixel_scale), ScalingMode::AutoMin { min_width, min_height, } => { // Compare Pixels of current width and minimal height and Pixels of minimal width with current height. // 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. if width * min_height > min_width * height { (width * min_height / height, min_height) } else { (min_width, height * min_width / width) } } ScalingMode::AutoMax { max_width, max_height, } => { // Compare Pixels of current width and maximal height and Pixels of maximal width with current height. // 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. if width * max_height < max_width * height { (width * max_height / height, max_height) } else { (max_width, height * max_width / width) } } ScalingMode::FixedVertical(viewport_height) => { (width * viewport_height / height, viewport_height) } ScalingMode::FixedHorizontal(viewport_width) => { (viewport_width, height * viewport_width / width) } ScalingMode::Fixed { width, height } => (width, height), }; let origin_x = projection_width * self.viewport_origin.x; let origin_y = projection_height * self.viewport_origin.y; self.area = Rect::new( self.scale * -origin_x, self.scale * -origin_y, self.scale * (projection_width - origin_x), self.scale * (projection_height - origin_y), ); } fn far(&self) -> f32 { self.far } } impl Default for OrthographicProjection { fn default() -> Self { OrthographicProjection { scale: 1.0, near: 0.0, far: 1000.0, viewport_origin: Vec2::new(0.5, 0.5), scaling_mode: ScalingMode::WindowSize(1.0), area: Rect::new(-1.0, -1.0, 1.0, 1.0), } } }