From bed9ddf3ceb989707c91d647a3b1c511e618d742 Mon Sep 17 00:00:00 2001 From: Aevyrie Date: Wed, 1 Jan 2025 12:44:24 -0800 Subject: [PATCH] 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()`. --- Cargo.toml | 11 + crates/bevy_animation/src/animation_curves.rs | 46 ++-- .../src/core_2d/camera_2d.rs | 12 +- crates/bevy_pbr/src/lib.rs | 24 +- crates/bevy_pbr/src/light/mod.rs | 6 +- crates/bevy_pbr/src/material.rs | 1 + .../src/meshlet/material_pipeline_prepare.rs | 1 + crates/bevy_render/src/camera/camera.rs | 14 +- crates/bevy_render/src/camera/mod.rs | 4 +- crates/bevy_render/src/camera/projection.rs | 239 ++++++++++++++---- crates/bevy_render/src/view/visibility/mod.rs | 8 +- crates/bevy_sprite/src/picking_backend.rs | 21 +- crates/bevy_ui/src/layout/mod.rs | 9 +- examples/2d/pixel_grid_snap.rs | 5 +- examples/README.md | 1 + examples/camera/custom_projection.rs | 84 ++++++ examples/camera/projection_zoom.rs | 2 + 17 files changed, 363 insertions(+), 125 deletions(-) create mode 100644 examples/camera/custom_projection.rs diff --git a/Cargo.toml b/Cargo.toml index 08692b5c18..323c8ffa04 100644 --- a/Cargo.toml +++ b/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" diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 28069c1af4..40256485eb 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.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::() -/// .ok_or( -/// AnimationEvaluationError::ComponentNotPresent( -/// TypeId::of::() -/// ) -/// )? +/// let component = entity +/// .get_mut::() +/// .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::() -/// # .ok_or( -/// # AnimationEvaluationError::ComponentNotPresent( -/// # TypeId::of::() -/// # ) -/// # )? +/// # let component = entity +/// # .get_mut::() +/// # .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::()) diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs index 9f8073e3f5..d8f5139570 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -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 { diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 6559268bb5..490c023e7f 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -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::::default(), LightmapPlugin, LightProbePlugin, - PbrProjectionPlugin::::default(), - PbrProjectionPlugin::::default(), - PbrProjectionPlugin::::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(PhantomData); -impl Plugin for PbrProjectionPlugin { +/// 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:: + build_directional_light_cascades .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) .after(clear_directional_light_cascades), ); } } -impl Default for PbrProjectionPlugin { - fn default() -> Self { - Self(Default::default()) - } -} diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index fff1181740..f88512a19c 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -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( +pub fn build_directional_light_cascades( directional_light_shadow_map: Res, - views: Query<(Entity, &GlobalTransform, &P, &Camera)>, + views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>, mut lights: Query<( &GlobalTransform, &DirectionalLight, diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index a64f9a7064..e2ccdb2a32 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -741,6 +741,7 @@ pub fn queue_material_meshes( view_key |= match projection { Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE, Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC, + Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD, }; } diff --git a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs index 3d41688add..a6053b412c 100644 --- a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs @@ -114,6 +114,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( view_key |= match projection { Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE, Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC, + Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD, }; } diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index e7c853b843..f7bb13340f 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -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>( +pub fn camera_system( mut window_resized_events: EventReader, mut window_created_events: EventReader, mut window_scale_factor_changed_events: EventReader, @@ -908,7 +902,7 @@ pub fn camera_system>( windows: Query<(Entity, &Window)>, images: Res>, manual_texture_views: Res, - mut cameras: Query<(&mut Camera, &mut T)>, + mut cameras: Query<(&mut Camera, &mut Projection)>, ) { let primary_window = primary_window.iter().next(); diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index 83c882cc3e..ee3822c2bc 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -33,9 +33,7 @@ impl Plugin for CameraPlugin { .init_resource::() .init_resource::() .add_plugins(( - CameraProjectionPlugin::::default(), - CameraProjectionPlugin::::default(), - CameraProjectionPlugin::::default(), + CameraProjectionPlugin, ExtractResourcePlugin::::default(), ExtractResourcePlugin::::default(), ExtractComponentPlugin::::default(), diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index fd3880c1db..3b04334f5b 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -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( - PhantomData, -); -impl + GetTypeRegistration> Plugin - for CameraProjectionPlugin -{ +#[derive(Default)] +pub struct CameraProjectionPlugin; + +impl Plugin for CameraProjectionPlugin { fn build(&self, app: &mut App) { - app.register_type::() + app.register_type::() + .register_type::() + .register_type::() + .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), + crate::camera::camera_system.in_set(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), - crate::view::update_frusta:: + crate::camera::camera_system.in_set(CameraUpdateSystem), + crate::view::update_frusta .in_set(VisibilitySystems::UpdateFrusta) - .after(crate::camera::camera_system::) - .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 Default for CameraProjectionPlugin { - fn default() -> Self { - Self(Default::default()) - } -} /// Label for [`camera_system`], shared across all `T`. /// @@ -64,21 +45,40 @@ impl 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; + } + + downcast_rs::impl_downcast!(DynCameraProjection); + + impl DynCameraProjection for T + where + T: 'static + CameraProjection + core::fmt::Debug + Send + Sync + Clone, + { + fn clone_box(&self) -> Box { + 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, +} + +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::().unwrap(); + /// + /// assert_eq!(perspective.fov, PerspectiveProjection::default().fov); + /// ``` + pub fn get

(&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::().unwrap(); + /// + /// assert_eq!(perspective.fov, PerspectiveProjection::default().fov); + /// perspective.fov = 1.0; + /// ``` + pub fn get_mut

(&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

(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. /// diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 3680ad0979..a004cf19e0 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -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( +pub fn update_frusta( mut views: Query< - (&GlobalTransform, &T, &mut Frustum), - Or<(Changed, Changed)>, + (&GlobalTransform, &Projection, &mut Frustum), + Or<(Changed, Changed)>, >, ) { for (transform, projection, mut frustum) in &mut views { diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index f5d7b43bdd..6d447cf7ac 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -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>, images: Res>, texture_atlas_layout: Res>, @@ -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; }; diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index a228bf8264..4c24ca322e 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -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::, + 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::, + bevy_render::camera::camera_system, update_target_camera_system, ApplyDeferred, ui_layout_system, diff --git a/examples/2d/pixel_grid_snap.rs b/examples/2d/pixel_grid_snap.rs index 9d701e003c..adb6741324 100644 --- a/examples/2d/pixel_grid_snap.rs +++ b/examples/2d/pixel_grid_snap.rs @@ -144,8 +144,11 @@ fn rotate(time: Res