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:
Aevyrie 2025-01-01 12:44:24 -08:00 committed by GitHub
parent 294e0db719
commit bed9ddf3ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 363 additions and 125 deletions

View File

@ -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"

View File

@ -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>())

View File

@ -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 {

View File

@ -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())
}
}

View File

@ -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,

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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();

View File

@ -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(),

View File

@ -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.
///

View File

@ -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 {

View File

@ -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;
};

View File

@ -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,

View File

@ -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;

View File

@ -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

View 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),
));
}

View File

@ -126,6 +126,7 @@ fn switch_projection(
},
..OrthographicProjection::default_3d()
}),
_ => return,
}
}
}
@ -162,5 +163,6 @@ fn zoom(
camera_settings.perspective_zoom_range.end,
);
}
_ => (),
}
}