Refactor and simplify custom projections (#17063)
# Objective - Fixes https://github.com/bevyengine/bevy/issues/16556 - Closes https://github.com/bevyengine/bevy/issues/11807 ## Solution - Simplify custom projections by using a single source of truth - `Projection`, removing all existing generic systems and types. - Existing perspective and orthographic structs are no longer components - I could dissolve these to simplify further, but keeping them around was the fast way to implement this. - Instead of generics, introduce a third variant, with a trait object. - Do an object safety dance with an intermediate trait to allow cloning boxed camera projections. This is a normal rust polymorphism papercut. You can do this with a crate but a manual impl is short and sweet. ## Testing - Added a custom projection example --- ## Showcase - Custom projections and projection handling has been simplified. - Projection systems are no longer generic, with the potential for many different projection components on the same camera. - Instead `Projection` is now the single source of truth for camera projections, and is the only projection component. - Custom projections are still supported, and can be constructed with `Projection::custom()`. ## Migration Guide - `PerspectiveProjection` and `OrthographicProjection` are no longer components. Use `Projection` instead. - Custom projections should no longer be inserted as a component. Instead, simply set the custom projection as a value of `Projection` with `Projection::custom()`.
This commit is contained in:
parent
294e0db719
commit
bed9ddf3ce
11
Cargo.toml
11
Cargo.toml
@ -3571,6 +3571,17 @@ description = "A 2D top-down camera smoothly following player movements"
|
||||
category = "Camera"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_projection"
|
||||
path = "examples/camera/custom_projection.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.custom_projection]
|
||||
name = "Custom Projection"
|
||||
description = "Shows how to create custom camera projections."
|
||||
category = "Camera"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "first_person_view_model"
|
||||
path = "examples/camera/first_person_view_model.rs"
|
||||
|
@ -117,22 +117,27 @@ use downcast_rs::{impl_downcast, Downcast};
|
||||
/// # use bevy_animation::{prelude::AnimatableProperty, AnimationEntityMut, AnimationEvaluationError, animation_curves::EvaluatorId};
|
||||
/// # use bevy_reflect::Reflect;
|
||||
/// # use std::any::TypeId;
|
||||
/// # use bevy_render::camera::PerspectiveProjection;
|
||||
/// # use bevy_render::camera::{Projection, PerspectiveProjection};
|
||||
/// #[derive(Reflect)]
|
||||
/// struct FieldOfViewProperty;
|
||||
///
|
||||
/// impl AnimatableProperty for FieldOfViewProperty {
|
||||
/// type Property = f32;
|
||||
/// fn get_mut<'a>(&self, entity: &'a mut AnimationEntityMut) -> Result<&'a mut Self::Property, AnimationEvaluationError> {
|
||||
/// let component = entity
|
||||
/// .get_mut::<PerspectiveProjection>()
|
||||
/// .ok_or(
|
||||
/// AnimationEvaluationError::ComponentNotPresent(
|
||||
/// TypeId::of::<PerspectiveProjection>()
|
||||
/// )
|
||||
/// )?
|
||||
/// let component = entity
|
||||
/// .get_mut::<Projection>()
|
||||
/// .ok_or(AnimationEvaluationError::ComponentNotPresent(TypeId::of::<
|
||||
/// Projection,
|
||||
/// >(
|
||||
/// )))?
|
||||
/// .into_inner();
|
||||
/// Ok(&mut component.fov)
|
||||
/// match component {
|
||||
/// Projection::Perspective(perspective) => Ok(&mut perspective.fov),
|
||||
/// _ => Err(AnimationEvaluationError::PropertyNotPresent(TypeId::of::<
|
||||
/// PerspectiveProjection,
|
||||
/// >(
|
||||
/// ))),
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn evaluator_id(&self) -> EvaluatorId {
|
||||
@ -146,7 +151,7 @@ use downcast_rs::{impl_downcast, Downcast};
|
||||
/// # use bevy_animation::prelude::{AnimatableProperty, AnimatableKeyframeCurve, AnimatableCurve};
|
||||
/// # use bevy_ecs::name::Name;
|
||||
/// # use bevy_reflect::Reflect;
|
||||
/// # use bevy_render::camera::PerspectiveProjection;
|
||||
/// # use bevy_render::camera::{Projection, PerspectiveProjection};
|
||||
/// # use std::any::TypeId;
|
||||
/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test"));
|
||||
/// # #[derive(Reflect, Clone)]
|
||||
@ -154,15 +159,20 @@ use downcast_rs::{impl_downcast, Downcast};
|
||||
/// # impl AnimatableProperty for FieldOfViewProperty {
|
||||
/// # type Property = f32;
|
||||
/// # fn get_mut<'a>(&self, entity: &'a mut AnimationEntityMut) -> Result<&'a mut Self::Property, AnimationEvaluationError> {
|
||||
/// # let component = entity
|
||||
/// # .get_mut::<PerspectiveProjection>()
|
||||
/// # .ok_or(
|
||||
/// # AnimationEvaluationError::ComponentNotPresent(
|
||||
/// # TypeId::of::<PerspectiveProjection>()
|
||||
/// # )
|
||||
/// # )?
|
||||
/// # let component = entity
|
||||
/// # .get_mut::<Projection>()
|
||||
/// # .ok_or(AnimationEvaluationError::ComponentNotPresent(TypeId::of::<
|
||||
/// # Projection,
|
||||
/// # >(
|
||||
/// # )))?
|
||||
/// # .into_inner();
|
||||
/// # Ok(&mut component.fov)
|
||||
/// # match component {
|
||||
/// # Projection::Perspective(perspective) => Ok(&mut perspective.fov),
|
||||
/// # _ => Err(AnimationEvaluationError::PropertyNotPresent(TypeId::of::<
|
||||
/// # PerspectiveProjection,
|
||||
/// # >(
|
||||
/// # ))),
|
||||
/// # }
|
||||
/// # }
|
||||
/// # fn evaluator_id(&self) -> EvaluatorId {
|
||||
/// # EvaluatorId::Type(TypeId::of::<Self>())
|
||||
|
@ -10,7 +10,7 @@ use bevy_render::sync_world::SyncToRenderWorld;
|
||||
use bevy_render::{
|
||||
camera::{
|
||||
Camera, CameraMainTextureUsages, CameraProjection, CameraRenderGraph,
|
||||
OrthographicProjection,
|
||||
OrthographicProjection, Projection,
|
||||
},
|
||||
extract_component::ExtractComponent,
|
||||
prelude::Msaa,
|
||||
@ -27,7 +27,7 @@ use bevy_transform::prelude::{GlobalTransform, Transform};
|
||||
Camera,
|
||||
DebandDither,
|
||||
CameraRenderGraph(|| CameraRenderGraph::new(Core2d)),
|
||||
OrthographicProjection(OrthographicProjection::default_2d),
|
||||
Projection(|| Projection::Orthographic(OrthographicProjection::default_2d())),
|
||||
Frustum(|| OrthographicProjection::default_2d().compute_frustum(&GlobalTransform::from(Transform::default()))),
|
||||
Tonemapping(|| Tonemapping::None),
|
||||
)]
|
||||
@ -41,7 +41,7 @@ pub struct Camera2d;
|
||||
pub struct Camera2dBundle {
|
||||
pub camera: Camera,
|
||||
pub camera_render_graph: CameraRenderGraph,
|
||||
pub projection: OrthographicProjection,
|
||||
pub projection: Projection,
|
||||
pub visible_entities: VisibleEntities,
|
||||
pub frustum: Frustum,
|
||||
pub transform: Transform,
|
||||
@ -57,7 +57,7 @@ pub struct Camera2dBundle {
|
||||
|
||||
impl Default for Camera2dBundle {
|
||||
fn default() -> Self {
|
||||
let projection = OrthographicProjection::default_2d();
|
||||
let projection = Projection::Orthographic(OrthographicProjection::default_2d());
|
||||
let transform = Transform::default();
|
||||
let frustum = projection.compute_frustum(&GlobalTransform::from(transform));
|
||||
Self {
|
||||
@ -88,10 +88,10 @@ impl Camera2dBundle {
|
||||
pub fn new_with_far(far: f32) -> Self {
|
||||
// we want 0 to be "closest" and +far to be "farthest" in 2d, so we offset
|
||||
// the camera's translation by far and use a right handed coordinate system
|
||||
let projection = OrthographicProjection {
|
||||
let projection = Projection::Orthographic(OrthographicProjection {
|
||||
far,
|
||||
..OrthographicProjection::default_2d()
|
||||
};
|
||||
});
|
||||
let transform = Transform::from_xyz(0.0, 0.0, far - 0.1);
|
||||
let frustum = projection.compute_frustum(&GlobalTransform::from(transform));
|
||||
Self {
|
||||
|
@ -46,7 +46,6 @@ mod volumetric_fog;
|
||||
use crate::material_bind_groups::FallbackBindlessResources;
|
||||
|
||||
use bevy_color::{Color, LinearRgba};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
pub use bundle::*;
|
||||
pub use cluster::*;
|
||||
@ -121,10 +120,7 @@ use bevy_ecs::prelude::*;
|
||||
use bevy_image::Image;
|
||||
use bevy_render::{
|
||||
alpha::AlphaMode,
|
||||
camera::{
|
||||
CameraProjection, CameraUpdateSystem, OrthographicProjection, PerspectiveProjection,
|
||||
Projection,
|
||||
},
|
||||
camera::{CameraUpdateSystem, Projection},
|
||||
extract_component::ExtractComponentPlugin,
|
||||
extract_resource::ExtractResourcePlugin,
|
||||
render_asset::prepare_assets,
|
||||
@ -341,9 +337,7 @@ impl Plugin for PbrPlugin {
|
||||
ExtractComponentPlugin::<ShadowFilteringMethod>::default(),
|
||||
LightmapPlugin,
|
||||
LightProbePlugin,
|
||||
PbrProjectionPlugin::<Projection>::default(),
|
||||
PbrProjectionPlugin::<PerspectiveProjection>::default(),
|
||||
PbrProjectionPlugin::<OrthographicProjection>::default(),
|
||||
PbrProjectionPlugin,
|
||||
GpuMeshPreprocessPlugin {
|
||||
use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder,
|
||||
},
|
||||
@ -480,20 +474,16 @@ impl Plugin for PbrPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`CameraProjection`] specific PBR functionality.
|
||||
pub struct PbrProjectionPlugin<T: CameraProjection + Component>(PhantomData<T>);
|
||||
impl<T: CameraProjection + Component> Plugin for PbrProjectionPlugin<T> {
|
||||
/// Camera projection PBR functionality.
|
||||
#[derive(Default)]
|
||||
pub struct PbrProjectionPlugin;
|
||||
impl Plugin for PbrProjectionPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
build_directional_light_cascades::<T>
|
||||
build_directional_light_cascades
|
||||
.in_set(SimulationLightSystems::UpdateDirectionalLightCascades)
|
||||
.after(clear_directional_light_cascades),
|
||||
);
|
||||
}
|
||||
}
|
||||
impl<T: CameraProjection + Component> Default for PbrProjectionPlugin<T> {
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use bevy_ecs::{
|
||||
use bevy_math::{ops, Mat4, Vec3A, Vec4};
|
||||
use bevy_reflect::prelude::*;
|
||||
use bevy_render::{
|
||||
camera::{Camera, CameraProjection},
|
||||
camera::{Camera, CameraProjection, Projection},
|
||||
extract_component::ExtractComponent,
|
||||
extract_resource::ExtractResource,
|
||||
mesh::Mesh3d,
|
||||
@ -305,9 +305,9 @@ pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &m
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_directional_light_cascades<P: CameraProjection + Component>(
|
||||
pub fn build_directional_light_cascades(
|
||||
directional_light_shadow_map: Res<DirectionalLightShadowMap>,
|
||||
views: Query<(Entity, &GlobalTransform, &P, &Camera)>,
|
||||
views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>,
|
||||
mut lights: Query<(
|
||||
&GlobalTransform,
|
||||
&DirectionalLight,
|
||||
|
@ -741,6 +741,7 @@ pub fn queue_material_meshes<M: Material>(
|
||||
view_key |= match projection {
|
||||
Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE,
|
||||
Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC,
|
||||
Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -114,6 +114,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass<M: Material>(
|
||||
view_key |= match projection {
|
||||
Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE,
|
||||
Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC,
|
||||
Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
change_detection::DetectChanges,
|
||||
component::{Component, ComponentId, Mutable},
|
||||
component::{Component, ComponentId},
|
||||
entity::{Entity, EntityBorrow},
|
||||
event::EventReader,
|
||||
prelude::{require, With},
|
||||
@ -883,13 +883,7 @@ impl NormalizedRenderTarget {
|
||||
/// System in charge of updating a [`Camera`] when its window or projection changes.
|
||||
///
|
||||
/// The system detects window creation, resize, and scale factor change events to update the camera
|
||||
/// projection if needed. It also queries any [`CameraProjection`] component associated with the same
|
||||
/// entity as the [`Camera`] one, to automatically update the camera projection matrix.
|
||||
///
|
||||
/// The system function is generic over the camera projection type, and only instances of
|
||||
/// [`OrthographicProjection`] and [`PerspectiveProjection`] are automatically added to
|
||||
/// the app, as well as the runtime-selected [`Projection`].
|
||||
/// The system runs during [`PostUpdate`](bevy_app::PostUpdate).
|
||||
/// [`Projection`] if needed.
|
||||
///
|
||||
/// ## World Resources
|
||||
///
|
||||
@ -899,7 +893,7 @@ impl NormalizedRenderTarget {
|
||||
/// [`OrthographicProjection`]: crate::camera::OrthographicProjection
|
||||
/// [`PerspectiveProjection`]: crate::camera::PerspectiveProjection
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn camera_system<T: CameraProjection + Component<Mutability = Mutable>>(
|
||||
pub fn camera_system(
|
||||
mut window_resized_events: EventReader<WindowResized>,
|
||||
mut window_created_events: EventReader<WindowCreated>,
|
||||
mut window_scale_factor_changed_events: EventReader<WindowScaleFactorChanged>,
|
||||
@ -908,7 +902,7 @@ pub fn camera_system<T: CameraProjection + Component<Mutability = Mutable>>(
|
||||
windows: Query<(Entity, &Window)>,
|
||||
images: Res<Assets<Image>>,
|
||||
manual_texture_views: Res<ManualTextureViews>,
|
||||
mut cameras: Query<(&mut Camera, &mut T)>,
|
||||
mut cameras: Query<(&mut Camera, &mut Projection)>,
|
||||
) {
|
||||
let primary_window = primary_window.iter().next();
|
||||
|
||||
|
@ -33,9 +33,7 @@ impl Plugin for CameraPlugin {
|
||||
.init_resource::<ManualTextureViews>()
|
||||
.init_resource::<ClearColor>()
|
||||
.add_plugins((
|
||||
CameraProjectionPlugin::<Projection>::default(),
|
||||
CameraProjectionPlugin::<OrthographicProjection>::default(),
|
||||
CameraProjectionPlugin::<PerspectiveProjection>::default(),
|
||||
CameraProjectionPlugin,
|
||||
ExtractResourcePlugin::<ManualTextureViews>::default(),
|
||||
ExtractResourcePlugin::<ClearColor>::default(),
|
||||
ExtractComponentPlugin::<CameraMainTextureUsages>::default(),
|
||||
|
@ -1,12 +1,11 @@
|
||||
use core::marker::PhantomData;
|
||||
use core::fmt::Debug;
|
||||
|
||||
use crate::{primitives::Frustum, view::VisibilitySystems};
|
||||
use bevy_app::{App, Plugin, PostStartup, PostUpdate};
|
||||
use bevy_ecs::{component::Mutable, prelude::*};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4};
|
||||
use bevy_reflect::{
|
||||
std_traits::ReflectDefault, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectSerialize,
|
||||
};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize};
|
||||
use bevy_transform::{components::GlobalTransform, TransformSystem};
|
||||
use derive_more::derive::From;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -14,49 +13,31 @@ use serde::{Deserialize, Serialize};
|
||||
/// Adds [`Camera`](crate::camera::Camera) driver systems for a given projection type.
|
||||
///
|
||||
/// If you are using `bevy_pbr`, then you need to add `PbrProjectionPlugin` along with this.
|
||||
pub struct CameraProjectionPlugin<T: CameraProjection + Component + GetTypeRegistration>(
|
||||
PhantomData<T>,
|
||||
);
|
||||
impl<T: CameraProjection + Component<Mutability = Mutable> + GetTypeRegistration> Plugin
|
||||
for CameraProjectionPlugin<T>
|
||||
{
|
||||
#[derive(Default)]
|
||||
pub struct CameraProjectionPlugin;
|
||||
|
||||
impl Plugin for CameraProjectionPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<T>()
|
||||
app.register_type::<Projection>()
|
||||
.register_type::<PerspectiveProjection>()
|
||||
.register_type::<OrthographicProjection>()
|
||||
.register_type::<CustomProjection>()
|
||||
.add_systems(
|
||||
PostStartup,
|
||||
crate::camera::camera_system::<T>
|
||||
.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),
|
||||
crate::camera::camera_system.in_set(CameraUpdateSystem),
|
||||
)
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
crate::camera::camera_system::<T>
|
||||
.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),
|
||||
crate::view::update_frusta::<T>
|
||||
crate::camera::camera_system.in_set(CameraUpdateSystem),
|
||||
crate::view::update_frusta
|
||||
.in_set(VisibilitySystems::UpdateFrusta)
|
||||
.after(crate::camera::camera_system::<T>)
|
||||
.after(TransformSystem::TransformPropagate)
|
||||
// We assume that no camera will have more than one projection component,
|
||||
// so these systems will run independently of one another.
|
||||
// FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481.
|
||||
.ambiguous_with(VisibilitySystems::UpdateFrusta),
|
||||
.after(crate::camera::camera_system)
|
||||
.after(TransformSystem::TransformPropagate),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
impl<T: CameraProjection + Component + GetTypeRegistration> Default for CameraProjectionPlugin<T> {
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Label for [`camera_system<T>`], shared across all `T`.
|
||||
///
|
||||
@ -64,21 +45,40 @@ impl<T: CameraProjection + Component + GetTypeRegistration> Default for CameraPr
|
||||
#[derive(SystemSet, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub struct CameraUpdateSystem;
|
||||
|
||||
/// Trait to control the projection matrix of a camera.
|
||||
/// Describes a type that can generate a projection matrix, allowing it to be added to a
|
||||
/// [`Camera`]'s [`Projection`] component.
|
||||
///
|
||||
/// 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.
|
||||
/// Once implemented, the projection can be added to a camera using [`Projection::custom`].
|
||||
///
|
||||
/// Use the plugins [`CameraProjectionPlugin`] and `bevy::pbr::PbrProjectionPlugin` to setup the
|
||||
/// systems for your [`CameraProjection`] implementation.
|
||||
/// The projection will be automatically updated as the render area is resized. This is useful when,
|
||||
/// for example, a projection type has a field like `fov` that should change when the window width
|
||||
/// is changed but not when the height changes.
|
||||
///
|
||||
/// This trait is implemented by bevy's built-in projections [`PerspectiveProjection`] and
|
||||
/// [`OrthographicProjection`].
|
||||
///
|
||||
/// [`Camera`]: crate::camera::Camera
|
||||
pub trait CameraProjection {
|
||||
/// Generate the projection matrix.
|
||||
fn get_clip_from_view(&self) -> Mat4;
|
||||
|
||||
/// Generate the projection matrix for a [`SubCameraView`](super::SubCameraView).
|
||||
fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4;
|
||||
|
||||
/// When the area this camera renders to changes dimensions, this method will be automatically
|
||||
/// called. Use this to update any projection properties that depend on the aspect ratio or
|
||||
/// dimensions of the render area.
|
||||
fn update(&mut self, width: f32, height: f32);
|
||||
|
||||
/// The far plane distance of the projection.
|
||||
fn far(&self) -> f32;
|
||||
|
||||
/// The eight corners of the camera frustum, as defined by this projection.
|
||||
///
|
||||
/// The corners should be provided in the following order: first the bottom right, top right,
|
||||
/// top left, bottom left for the near plane, then similar for the far plane.
|
||||
// TODO: This seems somewhat redundant with `compute_frustum`, and similarly should be possible
|
||||
// to compute with a default impl.
|
||||
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8];
|
||||
|
||||
/// Compute camera frustum for camera with given projection and transform.
|
||||
@ -97,12 +97,152 @@ pub trait CameraProjection {
|
||||
}
|
||||
}
|
||||
|
||||
/// A configurable [`CameraProjection`] that can select its projection type at runtime.
|
||||
mod sealed {
|
||||
use super::CameraProjection;
|
||||
|
||||
/// A wrapper trait to make it possible to implement Clone for boxed [`super::CameraProjection`]
|
||||
/// trait objects, without breaking object safety rules by making it `Sized`. Additional bounds
|
||||
/// are included for downcasting, and fulfilling the trait bounds on `Projection`.
|
||||
pub trait DynCameraProjection:
|
||||
CameraProjection + core::fmt::Debug + Send + Sync + downcast_rs::Downcast
|
||||
{
|
||||
fn clone_box(&self) -> Box<dyn DynCameraProjection>;
|
||||
}
|
||||
|
||||
downcast_rs::impl_downcast!(DynCameraProjection);
|
||||
|
||||
impl<T> DynCameraProjection for T
|
||||
where
|
||||
T: 'static + CameraProjection + core::fmt::Debug + Send + Sync + Clone,
|
||||
{
|
||||
fn clone_box(&self) -> Box<dyn DynCameraProjection> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds a dynamic [`CameraProjection`] trait object. Use [`Projection::custom()`] to construct a
|
||||
/// custom projection.
|
||||
///
|
||||
/// The contained dynamic object can be downcast into a static type using [`CustomProjection::get`].
|
||||
#[derive(Component, Debug, Reflect, Deref, DerefMut)]
|
||||
#[reflect(Default)]
|
||||
pub struct CustomProjection {
|
||||
#[reflect(ignore)]
|
||||
#[deref]
|
||||
dyn_projection: Box<dyn sealed::DynCameraProjection>,
|
||||
}
|
||||
|
||||
impl Default for CustomProjection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dyn_projection: Box::new(PerspectiveProjection::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for CustomProjection {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
dyn_projection: self.dyn_projection.clone_box(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomProjection {
|
||||
/// Returns a reference to the [`CameraProjection`] `P`.
|
||||
///
|
||||
/// Returns `None` if this dynamic object is not a projection of type `P`.
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_render::prelude::{Projection, PerspectiveProjection};
|
||||
/// // For simplicity's sake, use perspective as a custom projection:
|
||||
/// let projection = Projection::custom(PerspectiveProjection::default());
|
||||
/// let Projection::Custom(custom) = projection else { return };
|
||||
///
|
||||
/// // At this point the projection type is erased.
|
||||
/// // We can use `get()` if we know what kind of projection we have.
|
||||
/// let perspective = custom.get::<PerspectiveProjection>().unwrap();
|
||||
///
|
||||
/// assert_eq!(perspective.fov, PerspectiveProjection::default().fov);
|
||||
/// ```
|
||||
pub fn get<P>(&self) -> Option<&P>
|
||||
where
|
||||
P: CameraProjection + Debug + Send + Sync + Clone + 'static,
|
||||
{
|
||||
self.dyn_projection.downcast_ref()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the [`CameraProjection`] `P`.
|
||||
///
|
||||
/// Returns `None` if this dynamic object is not a projection of type `P`.
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_render::prelude::{Projection, PerspectiveProjection};
|
||||
/// // For simplicity's sake, use perspective as a custom projection:
|
||||
/// let mut projection = Projection::custom(PerspectiveProjection::default());
|
||||
/// let Projection::Custom(mut custom) = projection else { return };
|
||||
///
|
||||
/// // At this point the projection type is erased.
|
||||
/// // We can use `get_mut()` if we know what kind of projection we have.
|
||||
/// let perspective = custom.get_mut::<PerspectiveProjection>().unwrap();
|
||||
///
|
||||
/// assert_eq!(perspective.fov, PerspectiveProjection::default().fov);
|
||||
/// perspective.fov = 1.0;
|
||||
/// ```
|
||||
pub fn get_mut<P>(&mut self) -> Option<&mut P>
|
||||
where
|
||||
P: CameraProjection + Debug + Send + Sync + Clone + 'static,
|
||||
{
|
||||
self.dyn_projection.downcast_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// Component that defines how to compute a [`Camera`]'s projection matrix.
|
||||
///
|
||||
/// Common projections, like perspective and orthographic, are provided out of the box to handle the
|
||||
/// majority of use cases. Custom projections can be added using the [`CameraProjection`] trait and
|
||||
/// the [`Projection::custom`] constructor.
|
||||
///
|
||||
/// ## What's a projection?
|
||||
///
|
||||
/// A camera projection essentially describes how 3d points from the point of view of a camera are
|
||||
/// projected onto a 2d screen. This is where properties like a camera's field of view are defined.
|
||||
/// More specifically, a projection is a 4x4 matrix that transforms points from view space (the
|
||||
/// point of view of the camera) into clip space. Clip space is almost, but not quite, equivalent to
|
||||
/// the rectangle that is rendered to your screen, with a depth axis. Any points that land outside
|
||||
/// the bounds of this cuboid are "clipped" and not rendered.
|
||||
///
|
||||
/// You can also think of the projection as the thing that describes the shape of a camera's
|
||||
/// frustum: the volume in 3d space that is visible to a camera.
|
||||
///
|
||||
/// [`Camera`]: crate::camera::Camera
|
||||
#[derive(Component, Debug, Clone, Reflect, From)]
|
||||
#[reflect(Component, Default, Debug)]
|
||||
pub enum Projection {
|
||||
Perspective(PerspectiveProjection),
|
||||
Orthographic(OrthographicProjection),
|
||||
Custom(CustomProjection),
|
||||
}
|
||||
|
||||
impl Projection {
|
||||
/// Construct a new custom camera projection from a type that implements [`CameraProjection`].
|
||||
pub fn custom<P>(projection: P) -> Self
|
||||
where
|
||||
// Implementation note: pushing these trait bounds all the way out to this function makes
|
||||
// errors nice for users. If a trait is missing, they will get a helpful error telling them
|
||||
// that, say, the `Debug` implementation is missing. Wrapping these traits behind a super
|
||||
// trait or some other indirection will make the errors harder to understand.
|
||||
//
|
||||
// For example, we don't use the `DynCameraProjection`` trait bound, because it is not the
|
||||
// trait the user should be implementing - they only need to worry about implementing
|
||||
// `CameraProjection`.
|
||||
P: CameraProjection + Debug + Send + Sync + Clone + 'static,
|
||||
{
|
||||
Projection::Custom(CustomProjection {
|
||||
dyn_projection: Box::new(projection),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CameraProjection for Projection {
|
||||
@ -110,6 +250,7 @@ impl CameraProjection for Projection {
|
||||
match self {
|
||||
Projection::Perspective(projection) => projection.get_clip_from_view(),
|
||||
Projection::Orthographic(projection) => projection.get_clip_from_view(),
|
||||
Projection::Custom(projection) => projection.get_clip_from_view(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,6 +258,7 @@ impl CameraProjection for Projection {
|
||||
match self {
|
||||
Projection::Perspective(projection) => projection.get_clip_from_view_for_sub(sub_view),
|
||||
Projection::Orthographic(projection) => projection.get_clip_from_view_for_sub(sub_view),
|
||||
Projection::Custom(projection) => projection.get_clip_from_view_for_sub(sub_view),
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +266,7 @@ impl CameraProjection for Projection {
|
||||
match self {
|
||||
Projection::Perspective(projection) => projection.update(width, height),
|
||||
Projection::Orthographic(projection) => projection.update(width, height),
|
||||
Projection::Custom(projection) => projection.update(width, height),
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +274,7 @@ impl CameraProjection for Projection {
|
||||
match self {
|
||||
Projection::Perspective(projection) => projection.far(),
|
||||
Projection::Orthographic(projection) => projection.far(),
|
||||
Projection::Custom(projection) => projection.far(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,6 +282,7 @@ impl CameraProjection for Projection {
|
||||
match self {
|
||||
Projection::Perspective(projection) => projection.get_frustum_corners(z_near, z_far),
|
||||
Projection::Orthographic(projection) => projection.get_frustum_corners(z_near, z_far),
|
||||
Projection::Custom(projection) => projection.get_frustum_corners(z_near, z_far),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -149,8 +294,8 @@ impl Default for Projection {
|
||||
}
|
||||
|
||||
/// A 3D camera projection in which distant objects appear smaller than close objects.
|
||||
#[derive(Component, Debug, Clone, Reflect)]
|
||||
#[reflect(Component, Default, Debug)]
|
||||
#[derive(Debug, Clone, Reflect)]
|
||||
#[reflect(Default, Debug)]
|
||||
pub struct PerspectiveProjection {
|
||||
/// The vertical field of view (FOV) in radians.
|
||||
///
|
||||
@ -341,8 +486,8 @@ pub enum ScalingMode {
|
||||
/// ..OrthographicProjection::default_2d()
|
||||
/// });
|
||||
/// ```
|
||||
#[derive(Component, Debug, Clone, Reflect)]
|
||||
#[reflect(Component, Debug, FromWorld)]
|
||||
#[derive(Debug, Clone, Reflect)]
|
||||
#[reflect(Debug, FromWorld)]
|
||||
pub struct OrthographicProjection {
|
||||
/// The distance of the near clipping plane in world units.
|
||||
///
|
||||
|
@ -22,7 +22,7 @@ use bevy_utils::{Parallel, TypeIdMap};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::NoCpuCulling;
|
||||
use crate::sync_world::MainEntity;
|
||||
use crate::{camera::Projection, sync_world::MainEntity};
|
||||
use crate::{
|
||||
camera::{Camera, CameraProjection},
|
||||
mesh::{Mesh, Mesh3d, MeshAabb},
|
||||
@ -398,10 +398,10 @@ pub fn calculate_bounds(
|
||||
/// Updates [`Frustum`].
|
||||
///
|
||||
/// This system is used in [`CameraProjectionPlugin`](crate::camera::CameraProjectionPlugin).
|
||||
pub fn update_frusta<T: Component + CameraProjection + Send + Sync + 'static>(
|
||||
pub fn update_frusta(
|
||||
mut views: Query<
|
||||
(&GlobalTransform, &T, &mut Frustum),
|
||||
Or<(Changed<GlobalTransform>, Changed<T>)>,
|
||||
(&GlobalTransform, &Projection, &mut Frustum),
|
||||
Or<(Changed<GlobalTransform>, Changed<Projection>)>,
|
||||
>,
|
||||
) {
|
||||
for (transform, projection, mut frustum) in &mut views {
|
||||
|
@ -59,7 +59,7 @@ impl Plugin for SpritePickingPlugin {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn sprite_picking(
|
||||
pointers: Query<(&PointerId, &PointerLocation)>,
|
||||
cameras: Query<(Entity, &Camera, &GlobalTransform, &OrthographicProjection)>,
|
||||
cameras: Query<(Entity, &Camera, &GlobalTransform, &Projection)>,
|
||||
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||
images: Res<Assets<Image>>,
|
||||
texture_atlas_layout: Res<Assets<TextureAtlasLayout>>,
|
||||
@ -91,15 +91,16 @@ fn sprite_picking(
|
||||
pointer_location.location().map(|loc| (pointer, loc))
|
||||
}) {
|
||||
let mut blocked = false;
|
||||
let Some((cam_entity, camera, cam_transform, cam_ortho)) = cameras
|
||||
.iter()
|
||||
.filter(|(_, camera, _, _)| camera.is_active)
|
||||
.find(|(_, camera, _, _)| {
|
||||
camera
|
||||
.target
|
||||
.normalize(primary_window)
|
||||
.is_some_and(|x| x == location.target)
|
||||
})
|
||||
let Some((cam_entity, camera, cam_transform, Projection::Orthographic(cam_ortho))) =
|
||||
cameras
|
||||
.iter()
|
||||
.filter(|(_, camera, _, _)| camera.is_active)
|
||||
.find(|(_, camera, _, _)| {
|
||||
camera
|
||||
.target
|
||||
.normalize(primary_window)
|
||||
.is_some_and(|x| x == location.target)
|
||||
})
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
@ -488,10 +488,7 @@ mod tests {
|
||||
};
|
||||
use bevy_image::Image;
|
||||
use bevy_math::{Rect, UVec2, Vec2};
|
||||
use bevy_render::{
|
||||
camera::{ManualTextureViews, OrthographicProjection},
|
||||
prelude::Camera,
|
||||
};
|
||||
use bevy_render::{camera::ManualTextureViews, prelude::Camera};
|
||||
use bevy_transform::{
|
||||
prelude::GlobalTransform,
|
||||
systems::{propagate_transforms, sync_simple_transforms},
|
||||
@ -543,7 +540,7 @@ mod tests {
|
||||
ui_schedule.add_systems(
|
||||
(
|
||||
// UI is driven by calculated camera target info, so we need to run the camera system first
|
||||
bevy_render::camera::camera_system::<OrthographicProjection>,
|
||||
bevy_render::camera::camera_system,
|
||||
update_target_camera_system,
|
||||
ApplyDeferred,
|
||||
ui_layout_system,
|
||||
@ -1187,7 +1184,7 @@ mod tests {
|
||||
ui_schedule.add_systems(
|
||||
(
|
||||
// UI is driven by calculated camera target info, so we need to run the camera system first
|
||||
bevy_render::camera::camera_system::<OrthographicProjection>,
|
||||
bevy_render::camera::camera_system,
|
||||
update_target_camera_system,
|
||||
ApplyDeferred,
|
||||
ui_layout_system,
|
||||
|
@ -144,8 +144,11 @@ fn rotate(time: Res<Time>, mut transforms: Query<&mut Transform, With<Rotate>>)
|
||||
/// Scales camera projection to fit the window (integer multiples only).
|
||||
fn fit_canvas(
|
||||
mut resize_events: EventReader<WindowResized>,
|
||||
mut projection: Single<&mut OrthographicProjection, With<OuterCamera>>,
|
||||
mut projection: Single<&mut Projection, With<OuterCamera>>,
|
||||
) {
|
||||
let Projection::Orthographic(projection) = &mut **projection else {
|
||||
return;
|
||||
};
|
||||
for event in resize_events.read() {
|
||||
let h_scale = event.width / RES_WIDTH as f32;
|
||||
let v_scale = event.height / RES_HEIGHT as f32;
|
||||
|
@ -270,6 +270,7 @@ Example | Description
|
||||
--- | ---
|
||||
[2D top-down camera](../examples/camera/2d_top_down_camera.rs) | A 2D top-down camera smoothly following player movements
|
||||
[Camera Orbit](../examples/camera/camera_orbit.rs) | Shows how to orbit a static scene using pitch, yaw, and roll.
|
||||
[Custom Projection](../examples/camera/custom_projection.rs) | Shows how to create custom camera projections.
|
||||
[First person view model](../examples/camera/first_person_view_model.rs) | A first-person camera that uses a world model and a view model with different field of views (FOV)
|
||||
[Projection Zoom](../examples/camera/projection_zoom.rs) | Shows how to zoom orthographic and perspective projection cameras.
|
||||
[Screen Shake](../examples/camera/2d_screen_shake.rs) | A simple 2D screen shake effect
|
||||
|
84
examples/camera/custom_projection.rs
Normal file
84
examples/camera/custom_projection.rs
Normal file
@ -0,0 +1,84 @@
|
||||
//! Demonstrates how to define and use custom camera projections.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::camera::CameraProjection;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_systems(Startup, setup)
|
||||
.run();
|
||||
}
|
||||
|
||||
/// Like a perspective projection, but the vanishing point is not centered.
|
||||
#[derive(Debug, Clone)]
|
||||
struct ObliquePerspectiveProjection {
|
||||
horizontal_obliqueness: f32,
|
||||
vertical_obliqueness: f32,
|
||||
perspective: PerspectiveProjection,
|
||||
}
|
||||
|
||||
/// Implement the [`CameraProjection`] trait for our custom projection:
|
||||
impl CameraProjection for ObliquePerspectiveProjection {
|
||||
fn get_clip_from_view(&self) -> Mat4 {
|
||||
let mut mat = self.perspective.get_clip_from_view();
|
||||
mat.col_mut(2)[0] = self.horizontal_obliqueness;
|
||||
mat.col_mut(2)[1] = self.vertical_obliqueness;
|
||||
mat
|
||||
}
|
||||
|
||||
fn get_clip_from_view_for_sub(&self, sub_view: &bevy_render::camera::SubCameraView) -> Mat4 {
|
||||
let mut mat = self.perspective.get_clip_from_view_for_sub(sub_view);
|
||||
mat.col_mut(2)[0] = self.horizontal_obliqueness;
|
||||
mat.col_mut(2)[1] = self.vertical_obliqueness;
|
||||
mat
|
||||
}
|
||||
|
||||
fn update(&mut self, width: f32, height: f32) {
|
||||
self.perspective.update(width, height);
|
||||
}
|
||||
|
||||
fn far(&self) -> f32 {
|
||||
self.perspective.far
|
||||
}
|
||||
|
||||
fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
|
||||
self.perspective.get_frustum_corners(z_near, z_far)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
commands.spawn((
|
||||
Camera3d::default(),
|
||||
// Use our custom projection:
|
||||
Projection::custom(ObliquePerspectiveProjection {
|
||||
horizontal_obliqueness: 0.2,
|
||||
vertical_obliqueness: 0.6,
|
||||
perspective: PerspectiveProjection::default(),
|
||||
}),
|
||||
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
));
|
||||
|
||||
// Scene setup
|
||||
commands.spawn((
|
||||
Mesh3d(meshes.add(Circle::new(4.0))),
|
||||
MeshMaterial3d(materials.add(Color::WHITE)),
|
||||
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
|
||||
));
|
||||
commands.spawn((
|
||||
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
|
||||
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
|
||||
Transform::from_xyz(0.0, 0.5, 0.0),
|
||||
));
|
||||
commands.spawn((
|
||||
PointLight {
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
Transform::from_xyz(4.0, 8.0, 4.0),
|
||||
));
|
||||
}
|
@ -126,6 +126,7 @@ fn switch_projection(
|
||||
},
|
||||
..OrthographicProjection::default_3d()
|
||||
}),
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -162,5 +163,6 @@ fn zoom(
|
||||
camera_settings.perspective_zoom_range.end,
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user