use std::marker::PhantomData;
use bevy_app::{App, Plugin, PostStartup, PostUpdate};
use bevy_ecs::{prelude::*, reflect::ReflectComponent};
use bevy_math::{Mat4, Rect, Vec2, Vec3A};
use bevy_reflect::{
std_traits::ReflectDefault, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectSerialize,
};
use serde::{Deserialize, Serialize};
pub struct CameraProjectionPlugin<T: CameraProjection>(PhantomData<T>);
impl<T: CameraProjection> Default for CameraProjectionPlugin<T> {
fn default() -> Self {
Self(Default::default())
}
}
#[derive(SystemSet, Clone, Eq, PartialEq, Hash, Debug)]
pub struct CameraUpdateSystem;
impl<T: CameraProjection + Component + GetTypeRegistration> Plugin for CameraProjectionPlugin<T> {
fn build(&self, app: &mut App) {
app.register_type::<T>()
.add_systems(
PostStartup,
crate::camera::camera_system::<T>
.in_set(CameraUpdateSystem)
.ambiguous_with(CameraUpdateSystem),
)
.add_systems(
PostUpdate,
crate::camera::camera_system::<T>
.in_set(CameraUpdateSystem)
.ambiguous_with(CameraUpdateSystem),
);
}
}
pub trait CameraProjection {
fn get_projection_matrix(&self) -> Mat4;
fn update(&mut self, width: f32, height: f32);
fn far(&self) -> f32;
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8];
}
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default)]
pub enum Projection {
Perspective(PerspectiveProjection),
Orthographic(OrthographicProjection),
}
impl From<PerspectiveProjection> for Projection {
fn from(p: PerspectiveProjection) -> Self {
Self::Perspective(p)
}
}
impl From<OrthographicProjection> 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(),
}
}
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
match self {
Projection::Perspective(projection) => projection.get_frustum_corners(z_near, z_far),
Projection::Orthographic(projection) => projection.get_frustum_corners(z_near, z_far),
}
}
}
impl Default for Projection {
fn default() -> Self {
Projection::Perspective(Default::default())
}
}
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default)]
pub struct PerspectiveProjection {
pub fov: f32,
pub aspect_ratio: f32,
pub near: f32,
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
}
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
let tan_half_fov = (self.fov / 2.).tan();
let a = z_near.abs() * tan_half_fov;
let b = z_far.abs() * tan_half_fov;
let aspect_ratio = self.aspect_ratio;
[
Vec3A::new(a * aspect_ratio, -a, z_near), Vec3A::new(a * aspect_ratio, a, z_near), Vec3A::new(-a * aspect_ratio, a, z_near), Vec3A::new(-a * aspect_ratio, -a, z_near), Vec3A::new(b * aspect_ratio, -b, z_far), Vec3A::new(b * aspect_ratio, b, z_far), Vec3A::new(-b * aspect_ratio, b, z_far), Vec3A::new(-b * aspect_ratio, -b, z_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 {
Fixed { width: f32, height: f32 },
WindowSize(f32),
AutoMin { min_width: f32, min_height: f32 },
AutoMax { max_width: f32, max_height: f32 },
FixedVertical(f32),
FixedHorizontal(f32),
}
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default)]
pub struct OrthographicProjection {
pub near: f32,
pub far: f32,
pub viewport_origin: Vec2,
pub scaling_mode: ScalingMode,
pub scale: f32,
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,
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,
} => {
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,
} => {
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
}
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
let area = self.area;
[
Vec3A::new(area.max.x, area.min.y, z_near), Vec3A::new(area.max.x, area.max.y, z_near), Vec3A::new(area.min.x, area.max.y, z_near), Vec3A::new(area.min.x, area.min.y, z_near), Vec3A::new(area.max.x, area.min.y, z_far), Vec3A::new(area.max.x, area.max.y, z_far), Vec3A::new(area.min.x, area.max.y, z_far), Vec3A::new(area.min.x, area.min.y, z_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),
}
}
}