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 fcd794029b04f..0f5dfc2fdf557 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -3,6 +3,7 @@ use crate::{ tonemapping::{DebandDither, Tonemapping}, }; use bevy_ecs::prelude::*; +use bevy_math::Vec3; use bevy_reflect::Reflect; use bevy_render::{ camera::{Camera, CameraProjection, CameraRenderGraph, OrthographicProjection}, @@ -10,7 +11,7 @@ use bevy_render::{ primitives::Frustum, view::VisibleEntities, }; -use bevy_transform::prelude::{GlobalTransform, Transform}; +use bevy_transform::components::{GlobalTransform2d, Transform2d}; #[derive(Component, Default, Reflect, Clone, ExtractComponent)] #[extract_component_filter(With)] @@ -26,8 +27,8 @@ pub struct Camera2dBundle { pub projection: OrthographicProjection, pub visible_entities: VisibleEntities, pub frustum: Frustum, - pub transform: Transform, - pub global_transform: GlobalTransform, + pub transform: Transform2d, + pub global_transform: GlobalTransform2d, pub camera_2d: Camera2d, pub tonemapping: Tonemapping, pub deband_dither: DebandDither, @@ -40,13 +41,10 @@ impl Default for Camera2dBundle { near: -1000., ..Default::default() }; - let transform = Transform::default(); - let view_projection = - projection.get_projection_matrix() * transform.compute_matrix().inverse(); let frustum = Frustum::from_view_projection_custom_far( - &view_projection, - &transform.translation, - &transform.back(), + &projection.get_projection_matrix(), + &Vec3::ZERO, + &Vec3::Z, projection.far(), ); Self { @@ -54,7 +52,7 @@ impl Default for Camera2dBundle { projection, visible_entities: VisibleEntities::default(), frustum, - transform, + transform: Default::default(), global_transform: Default::default(), camera: Camera::default(), camera_2d: Camera2d::default(), @@ -78,14 +76,16 @@ impl Camera2dBundle { far, ..Default::default() }; - let transform = Transform::from_xyz(0.0, 0.0, far - 0.1); - let view_projection = - projection.get_projection_matrix() * transform.compute_matrix().inverse(); + let transform = Transform2d::from_xyz(0.0, 0.0, far - 0.1); + let view_projection = projection.get_projection_matrix() + * GlobalTransform2d::from(transform) + .compute_matrix() + .inverse(); let frustum = Frustum::from_view_projection_custom_far( &view_projection, - &transform.translation, - &transform.back(), - projection.far(), + &transform.translation.extend(transform.z_translation), + &Vec3::Z, + far, ); Self { camera_render_graph: CameraRenderGraph::new(crate::core_2d::graph::NAME), diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index aafe2b8a80c86..153063d1188a5 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -21,7 +21,7 @@ use bevy_ecs::{ use bevy_log::warn; use bevy_math::{vec2, Mat4, Ray, Rect, URect, UVec2, UVec4, Vec2, Vec3}; use bevy_reflect::prelude::*; -use bevy_transform::components::GlobalTransform; +use bevy_transform::components::{AnyGlobalTransform, GlobalTransform, GlobalTransform2d}; use bevy_utils::{HashMap, HashSet}; use bevy_window::{ NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowRef, WindowResized, @@ -300,7 +300,7 @@ impl Camera { /// May panic. See [`ndc_to_world`](Camera::ndc_to_world). pub fn viewport_to_world_2d( &self, - camera_transform: &GlobalTransform, + camera_transform: &GlobalTransform2d, mut viewport_position: Vec2, ) -> Option { let target_size = self.logical_viewport_size()?; @@ -308,7 +308,8 @@ impl Camera { viewport_position.y = target_size.y - viewport_position.y; let ndc = viewport_position * 2. / target_size - Vec2::ONE; - let world_near_plane = self.ndc_to_world(camera_transform, ndc.extend(1.))?; + let world_near_plane = + self.ndc_to_world(&GlobalTransform::from(*camera_transform), ndc.extend(1.))?; Some(world_near_plane.truncate()) } @@ -639,7 +640,7 @@ pub fn extract_cameras( Entity, &Camera, &CameraRenderGraph, - &GlobalTransform, + AnyGlobalTransform, &VisibleEntities, Option<&ColorGrading>, Option<&TemporalJitter>, @@ -699,7 +700,7 @@ pub fn extract_cameras( }, ExtractedView { projection: camera.projection_matrix(), - transform: *transform, + transform: transform.get(), view_projection: None, hdr: camera.hdr, viewport: UVec4::new( diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 7f013c0a4fb28..d4545d7afd4c6 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -33,7 +33,7 @@ pub mod prelude { color::Color, mesh::{morph::MorphWeights, shape, Mesh}, render_resource::Shader, - spatial_bundle::SpatialBundle, + spatial_bundle::{Spatial2dBundle, SpatialBundle}, texture::{Image, ImagePlugin}, view::{InheritedVisibility, Msaa, ViewVisibility, Visibility, VisibilityBundle}, ExtractSchedule, diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index 8b74959dc6c3f..b2454d4e2f67c 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -205,13 +205,14 @@ impl HalfSpace { /// The frustum component is typically added from a bundle, either the `Camera2dBundle` /// or the `Camera3dBundle`. /// It is usually updated automatically by [`update_frusta`] from the -/// [`CameraProjection`] component and [`GlobalTransform`] of the camera entity. +/// [`CameraProjection`] component and the [`GlobalTransform`] or [`GlobalTransform2d`] of the camera entity. /// /// [`Camera`]: crate::camera::Camera /// [`NoFrustumCulling`]: crate::view::visibility::NoFrustumCulling /// [`update_frusta`]: crate::view::visibility::update_frusta /// [`CameraProjection`]: crate::camera::CameraProjection /// [`GlobalTransform`]: bevy_transform::components::GlobalTransform +/// [`GlobalTransform2d`]: bevy_transform::components::GlobalTransform2d #[derive(Component, Clone, Copy, Debug, Default, Reflect)] #[reflect(Component)] pub struct Frustum { diff --git a/crates/bevy_render/src/spatial_bundle.rs b/crates/bevy_render/src/spatial_bundle.rs index 27dba5c215a77..93279c90c4428 100644 --- a/crates/bevy_render/src/spatial_bundle.rs +++ b/crates/bevy_render/src/spatial_bundle.rs @@ -1,9 +1,9 @@ use bevy_ecs::prelude::Bundle; -use bevy_transform::prelude::{GlobalTransform, Transform}; +use bevy_transform::prelude::{GlobalTransform, GlobalTransform2d, Transform, Transform2d}; use crate::view::{InheritedVisibility, ViewVisibility, Visibility}; -/// A [`Bundle`] that allows the correct positional rendering of an entity. +/// A [`Bundle`] that allows the correct positional rendering of an entity in 3D. /// /// It consists of transform components, /// controlling position, rotation and scale of the entity, @@ -63,3 +63,64 @@ impl From for SpatialBundle { Self::from_transform(transform) } } + +/// A [`Bundle`] that allows the correct positional rendering of an entity in 2D. +/// +/// It consists of transform components, +/// controlling position, rotation and scale of the entity, +/// but also visibility components, +/// which determine whether the entity is visible or not. +/// +/// Parent-child hierarchies of entities must contain +/// all the [`Component`]s in this `Bundle` +/// to be rendered correctly. +/// +/// [`Component`]: bevy_ecs::component::Component +#[derive(Bundle, Debug, Default)] +pub struct Spatial2dBundle { + /// The visibility of the entity. + pub visibility: Visibility, + /// The inherited visibility of the entity. + pub inherited_visibility: InheritedVisibility, + /// The view visibility of the entity. + pub view_visibility: ViewVisibility, + /// The transform of the entity. + pub transform: Transform2d, + /// The global transform of the entity. + pub global_transform: GlobalTransform2d, +} + +impl Spatial2dBundle { + /// Creates a new [`Spatial2dBundle`] from a [`Transform`]. + /// + /// This initializes [`GlobalTransform`] as identity, and visibility as visible + #[inline] + pub const fn from_transform(transform: Transform2d) -> Self { + Spatial2dBundle { + transform, + ..Self::INHERITED_IDENTITY + } + } + + /// A visible [`Spatial2dBundle`], with no translation, rotation, and a scale of 1 on all axes. + pub const INHERITED_IDENTITY: Self = Spatial2dBundle { + visibility: Visibility::Inherited, + inherited_visibility: InheritedVisibility::HIDDEN, + view_visibility: ViewVisibility::HIDDEN, + transform: Transform2d::IDENTITY, + global_transform: GlobalTransform2d::IDENTITY, + }; + + /// An invisible [`Spatial2dBundle`], with no translation, rotation, and a scale of 1 on all axes. + pub const HIDDEN_IDENTITY: Self = Spatial2dBundle { + visibility: Visibility::Hidden, + ..Self::INHERITED_IDENTITY + }; +} + +impl From for Spatial2dBundle { + #[inline] + fn from(transform: Transform2d) -> Self { + Self::from_transform(transform) + } +} diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index b018103afc6c5..e99018eeb3364 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -8,7 +8,10 @@ use bevy_asset::{Assets, Handle}; use bevy_ecs::prelude::*; use bevy_hierarchy::{Children, Parent}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_transform::{components::GlobalTransform, TransformSystem}; +use bevy_transform::{ + components::{AnyGlobalTransform, GlobalTransform, GlobalTransform2d}, + TransformSystem, +}; use std::cell::Cell; use thread_local::ThreadLocal; @@ -280,13 +283,18 @@ pub fn calculate_bounds( /// This system is used in system sets [`VisibilitySystems::UpdateProjectionFrusta`], /// [`VisibilitySystems::UpdatePerspectiveFrusta`], and /// [`VisibilitySystems::UpdateOrthographicFrusta`]. -pub fn update_frusta( +pub fn update_frusta( mut views: Query< - (&GlobalTransform, &T, &mut Frustum), - Or<(Changed, Changed)>, + (AnyGlobalTransform, &T, &mut Frustum), + Or<( + Changed, + Changed, + Changed, + )>, >, ) { for (transform, projection, mut frustum) in &mut views { + let transform = transform.get(); let view_projection = projection.get_projection_matrix() * transform.compute_matrix().inverse(); *frustum = Frustum::from_view_projection_custom_far( @@ -391,7 +399,7 @@ pub fn check_visibility( &mut ViewVisibility, Option<&RenderLayers>, &Aabb, - &GlobalTransform, + AnyGlobalTransform, Has, )>, mut visible_no_aabb_query: Query< @@ -432,6 +440,7 @@ pub fn check_visibility( // If we have an aabb and transform, do frustum culling if !no_frustum_culling { + let transform = transform.get(); let model = transform.affine(); let model_sphere = Sphere { center: model.transform_point3a(model_aabb.center), diff --git a/crates/bevy_sprite/src/bundle.rs b/crates/bevy_sprite/src/bundle.rs index 7013993482347..b97104fc0bacf 100644 --- a/crates/bevy_sprite/src/bundle.rs +++ b/crates/bevy_sprite/src/bundle.rs @@ -8,13 +8,13 @@ use bevy_render::{ texture::Image, view::{InheritedVisibility, ViewVisibility, Visibility}, }; -use bevy_transform::components::{GlobalTransform, Transform}; +use bevy_transform::components::{GlobalTransform2d, Transform2d}; #[derive(Bundle, Clone, Default)] pub struct SpriteBundle { pub sprite: Sprite, - pub transform: Transform, - pub global_transform: GlobalTransform, + pub transform: Transform2d, + pub global_transform: GlobalTransform2d, pub texture: Handle, /// User indication of whether an entity is visible pub visibility: Visibility, @@ -33,8 +33,8 @@ pub struct SpriteSheetBundle { /// A handle to the texture atlas that holds the sprite images pub texture_atlas: Handle, /// Data pertaining to how the sprite is drawn on the screen - pub transform: Transform, - pub global_transform: GlobalTransform, + pub transform: Transform2d, + pub global_transform: GlobalTransform2d, /// User indication of whether an entity is visible pub visibility: Visibility, pub inherited_visibility: InheritedVisibility, diff --git a/crates/bevy_sprite/src/collide_aabb.rs b/crates/bevy_sprite/src/collide_aabb.rs index 424189dca4f43..ee1491fe500db 100644 --- a/crates/bevy_sprite/src/collide_aabb.rs +++ b/crates/bevy_sprite/src/collide_aabb.rs @@ -1,6 +1,6 @@ //! Utilities for detecting if and on which side two axis-aligned bounding boxes (AABB) collide. -use bevy_math::{Vec2, Vec3}; +use bevy_math::Vec2; #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum Collision { @@ -14,19 +14,19 @@ pub enum Collision { // TODO: ideally we can remove this once bevy gets a physics system /// Axis-aligned bounding box collision with "side" detection /// * `a_pos` and `b_pos` are the center positions of the rectangles, typically obtained by -/// extracting the `translation` field from a [`Transform`](bevy_transform::components::Transform) component +/// extracting the `translation` field from a [`Transform2d`](bevy_transform::components::Transform2d) component /// * `a_size` and `b_size` are the dimensions (width and height) of the rectangles. /// /// The return value is the side of `B` that `A` has collided with. [`Collision::Left`] means that /// `A` collided with `B`'s left side. [`Collision::Top`] means that `A` collided with `B`'s top side. /// If the collision occurs on multiple sides, the side with the shallowest penetration is returned. /// If all sides are involved, [`Collision::Inside`] is returned. -pub fn collide(a_pos: Vec3, a_size: Vec2, b_pos: Vec3, b_size: Vec2) -> Option { - let a_min = a_pos.truncate() - a_size / 2.0; - let a_max = a_pos.truncate() + a_size / 2.0; +pub fn collide(a_pos: Vec2, a_size: Vec2, b_pos: Vec2, b_size: Vec2) -> Option { + let a_min = a_pos - a_size / 2.0; + let a_max = a_pos + a_size / 2.0; - let b_min = b_pos.truncate() - b_size / 2.0; - let b_max = b_pos.truncate() + b_size / 2.0; + let b_min = b_pos - b_size / 2.0; + let b_max = b_pos + b_size / 2.0; // check to see if the two rectangles are intersecting if a_min.x < b_max.x && a_max.x > b_min.x && a_min.y < b_max.y && a_max.y > b_min.y { @@ -71,9 +71,9 @@ mod test { b: (f32, f32, f32, f32), ) -> Option { collide( - Vec3::new(a.0, a.1, 0.), + Vec2::new(a.0, a.1), Vec2::new(a.2, a.3), - Vec3::new(b.0, b.1, 0.), + Vec2::new(b.0, b.1), Vec2::new(b.2, b.3), ) } diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 30593c875e208..e4c7550115c83 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -28,7 +28,7 @@ use bevy_render::{ view::{ExtractedView, InheritedVisibility, Msaa, ViewVisibility, Visibility, VisibleEntities}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_transform::components::{GlobalTransform, Transform}; +use bevy_transform::components::{GlobalTransform2d, Transform2d}; use bevy_utils::{EntityHashMap, FloatOrd, HashMap, HashSet}; use std::hash::Hash; use std::marker::PhantomData; @@ -625,8 +625,8 @@ fn prepare_material2d( pub struct MaterialMesh2dBundle { pub mesh: Mesh2dHandle, pub material: Handle, - pub transform: Transform, - pub global_transform: GlobalTransform, + pub transform: Transform2d, + pub global_transform: GlobalTransform2d, /// User indication of whether an entity is visible pub visibility: Visibility, // Inherited visibility of an entity. diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index df5e4f0594011..01e58cfab48e9 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -8,7 +8,7 @@ use bevy_ecs::{ query::{QueryItem, ROQueryItem}, system::{lifetimeless::*, SystemParamItem, SystemState}, }; -use bevy_math::{Affine3, Vec2, Vec4}; +use bevy_math::{Affine3, Affine3A, Vec2, Vec4}; use bevy_reflect::Reflect; use bevy_render::{ batching::{ @@ -29,7 +29,7 @@ use bevy_render::{ }, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_transform::components::GlobalTransform; +use bevy_transform::components::GlobalTransform2d; use bevy_utils::EntityHashMap; use crate::Material2dBindGroupId; @@ -205,7 +205,7 @@ pub fn extract_mesh2d( Query<( Entity, &ViewVisibility, - &GlobalTransform, + &GlobalTransform2d, &Mesh2dHandle, Has, )>, @@ -225,7 +225,7 @@ pub fn extract_mesh2d( entity, RenderMesh2dInstance { transforms: Mesh2dTransforms { - transform: (&transform.affine()).into(), + transform: (&Affine3A::from_mat4(transform.compute_matrix())).into(), flags: MeshFlags::empty().bits(), }, mesh_asset_id: handle.0.id(), diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index bebdbad231393..0a95a007ba656 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -32,7 +32,7 @@ use bevy_render::{ }, Extract, }; -use bevy_transform::components::GlobalTransform; +use bevy_transform::components::GlobalTransform2d; use bevy_utils::{EntityHashMap, FloatOrd, HashMap}; use bytemuck::{Pod, Zeroable}; use fixedbitset::FixedBitSet; @@ -313,7 +313,7 @@ impl SpecializedRenderPipeline for SpritePipeline { } pub struct ExtractedSprite { - pub transform: GlobalTransform, + pub transform: GlobalTransform2d, pub color: Color, /// Select an area of the texture pub rect: Option, @@ -360,7 +360,7 @@ pub fn extract_sprites( Entity, &ViewVisibility, &Sprite, - &GlobalTransform, + &GlobalTransform2d, &Handle, )>, >, @@ -369,7 +369,7 @@ pub fn extract_sprites( Entity, &ViewVisibility, &TextureAtlasSprite, - &GlobalTransform, + &GlobalTransform2d, &Handle, )>, >, @@ -708,7 +708,14 @@ pub fn prepare_sprites( if let Some(custom_size) = extracted_sprite.custom_size { quad_size = custom_size; } - let transform = extracted_sprite.transform.affine() + // let transform = extracted_sprite.transform.affine() + // * Affine2::from_scale_angle_translation( + // quad_size, + // 0., + // quad_size * (-extracted_sprite.anchor - Vec2::splat(0.5)), + // ); + + let transform = Affine3A::from_mat4(extracted_sprite.transform.compute_matrix()) * Affine3A::from_scale_rotation_translation( quad_size.extend(1.0), Quat::IDENTITY, diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 213264a2737d6..0a89a88c0deab 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -23,7 +23,7 @@ pub struct Sprite { pub anchor: Anchor, } -/// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform). +/// How a sprite is positioned relative to its [`Transform2d`](bevy_transform::components::Transform2d). /// It defaults to `Anchor::Center`. #[derive(Component, Debug, Clone, Copy, Default, Reflect)] #[doc(alias = "pivot")] diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index ffb285deba577..1257261669958 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -22,7 +22,7 @@ use bevy_render::{ Extract, }; use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, TextureAtlas}; -use bevy_transform::prelude::{GlobalTransform, Transform}; +use bevy_transform::prelude::{GlobalTransform2d, Transform2d}; use bevy_utils::HashSet; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; @@ -65,9 +65,9 @@ pub struct Text2dBundle { /// The maximum width and height of the text. pub text_2d_bounds: Text2dBounds, /// The transform of the text. - pub transform: Transform, + pub transform: Transform2d, /// The global transform of the text. - pub global_transform: GlobalTransform, + pub global_transform: GlobalTransform2d, /// The visibility properties of the text. pub visibility: Visibility, /// Inherited visibility of an entity. @@ -90,7 +90,7 @@ pub fn extract_text2d_sprite( &Text, &TextLayoutInfo, &Anchor, - &GlobalTransform, + &GlobalTransform2d, )>, >, ) { @@ -99,7 +99,7 @@ pub fn extract_text2d_sprite( .get_single() .map(|window| window.resolution.scale_factor() as f32) .unwrap_or(1.0); - let scaling = GlobalTransform::from_scale(Vec2::splat(scale_factor.recip()).extend(1.)); + let scaling = GlobalTransform2d::from_scale(Vec2::splat(scale_factor.recip())); for (original_entity, view_visibility, text, text_layout_info, anchor, global_transform) in text2d_query.iter() @@ -110,9 +110,7 @@ pub fn extract_text2d_sprite( let text_anchor = -(anchor.as_vec() + 0.5); let alignment_translation = text_layout_info.logical_size * text_anchor; - let transform = *global_transform - * GlobalTransform::from_translation(alignment_translation.extend(0.)) - * scaling; + let transform = *global_transform * scaling; let mut color = Color::WHITE; let mut current_section = usize::MAX; for PositionedGlyph { @@ -132,7 +130,8 @@ pub fn extract_text2d_sprite( extracted_sprites.sprites.insert( entity, ExtractedSprite { - transform: transform * GlobalTransform::from_translation(position.extend(0.)), + transform: transform + * GlobalTransform2d::from_translation(*position + alignment_translation), color, rect: Some(atlas.textures[atlas_info.glyph_index]), custom_size: None, diff --git a/crates/bevy_transform/src/components/global_transform.rs b/crates/bevy_transform/src/components/global_transform.rs index 4e1480c44b0b5..d6b11a075d199 100644 --- a/crates/bevy_transform/src/components/global_transform.rs +++ b/crates/bevy_transform/src/components/global_transform.rs @@ -1,11 +1,15 @@ use std::ops::Mul; +use crate::prelude::GlobalTransform2d; + use super::Transform; use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_math::{Affine3A, Mat4, Quat, Vec3, Vec3A}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +#[cfg(feature = "serialize")] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -/// Describe the position of an entity relative to the reference frame. +/// Describe the 3D position of an entity relative to the reference frame. /// /// * To place or move an entity, you should set its [`Transform`]. /// * [`GlobalTransform`] is fully managed by bevy, you cannot mutate it, use @@ -34,7 +38,11 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// /// [`transform`]: https://github.com/bevyengine/bevy/blob/latest/examples/transforms/transform.rs #[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] #[reflect(Component, Default, PartialEq)] pub struct GlobalTransform(Affine3A); @@ -185,7 +193,7 @@ impl GlobalTransform { /// Transforms the given `point`, applying shear, scale, rotation and translation. /// - /// This moves `point` into the local space of this [`GlobalTransform`]. + /// This moves the `point` from the local space of this entity into global space. #[inline] pub fn transform_point(&self, point: Vec3) -> Vec3 { self.0.transform_point3(point) @@ -223,6 +231,12 @@ impl From for GlobalTransform { } } +impl From for GlobalTransform { + fn from(transform: GlobalTransform2d) -> Self { + Self::from(transform.compute_matrix()) + } +} + impl Mul for GlobalTransform { type Output = GlobalTransform; diff --git a/crates/bevy_transform/src/components/global_transform2d.rs b/crates/bevy_transform/src/components/global_transform2d.rs new file mode 100644 index 0000000000000..92bee86c121e7 --- /dev/null +++ b/crates/bevy_transform/src/components/global_transform2d.rs @@ -0,0 +1,180 @@ +use std::ops::Mul; + +use super::Transform2d; +use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; +use bevy_math::{Affine2, Mat4, Vec2, Vec3}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectFromReflect}; +#[cfg(feature = "serialize")] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +/// Describe the 2D position of an entity relative to the reference frame. +/// +/// * To place or move an entity, you should set its [`Transform2d`]. +/// * [`GlobalTransform2d`] is fully managed by bevy, you cannot mutate it, use +/// [`Transform2d`] instead. +/// * To get the global transform of an entity, you should get its [`GlobalTransform2d`]. +/// * For transform hierarchies to work correctly, you must have both a [`Transform2d`] and a [`GlobalTransform2d`]. +/// * You may use the [`TransformBundle`](crate::TransformBundle) to guarantee this. +/// +/// ## [`Transform2d`] and [`GlobalTransform2d`] +/// +/// [`Transform2d`] is the position of an entity relative to its parent position, or the reference +/// frame if it doesn't have a [`Parent`](bevy_hierarchy::Parent). +/// +/// [`GlobalTransform2d`] is the position of an entity relative to the reference frame. +/// +/// [`GlobalTransform2d`] is updated from [`Transform2d`] by systems in the system set +/// [`TransformPropagate`](crate::TransformSystem::TransformPropagate). +/// +/// This system runs during [`PostUpdate`](bevy_app::PostUpdate). If you +/// update the [`Transform2d`] of an entity in this schedule or after, you will notice a 1 frame lag +/// before the [`GlobalTransform2d`] is updated. +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Component, Default, PartialEq, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct GlobalTransform2d { + affine: Affine2, + z_translation: f32, +} + +impl Default for GlobalTransform2d { + fn default() -> Self { + Self::IDENTITY + } +} + +impl GlobalTransform2d { + /// An identity [`GlobalTransform2d`] that maps all points in space to themselves. + pub const IDENTITY: Self = GlobalTransform2d { + affine: Affine2::IDENTITY, + z_translation: 0.0, + }; + + #[doc(hidden)] + #[inline] + pub fn from_translation(translation: Vec2) -> Self { + GlobalTransform2d { + affine: Affine2::from_translation(translation), + z_translation: 0., + } + } + + #[doc(hidden)] + #[inline] + pub fn from_translation_3d(translation: Vec3) -> Self { + GlobalTransform2d { + affine: Affine2::from_translation(translation.truncate()), + z_translation: translation.z, + } + } + + #[doc(hidden)] + #[inline] + pub fn from_rotation(rotation: f32) -> Self { + GlobalTransform2d { + affine: Affine2::from_angle_translation(rotation, Vec2::ZERO), + z_translation: 0.0, + } + } + + #[doc(hidden)] + #[inline] + pub fn from_scale(scale: Vec2) -> Self { + GlobalTransform2d { + affine: Affine2::from_scale(scale), + z_translation: 0.0, + } + } + + /// Get the translation as a [`Vec3`]. + #[inline] + pub fn translation(&self) -> Vec3 { + self.affine.translation.extend(self.z_translation) + } + + /// Transforms the given `point`, applying shear, scale, rotation and translation. + /// + /// This moves the `point` from the local space of this entity into global space. + #[inline] + pub fn transform_point(&self, point: Vec3) -> Vec3 { + let xy = point.truncate(); + + self.affine + .transform_point2(xy) + .extend(point.z + self.z_translation) + } + + /// Returns the 2d affine transformation matrix as a [`Mat4`]. + #[inline] + pub fn compute_matrix(&self) -> Mat4 { + let translation = self.affine.translation.extend(self.z_translation); + Mat4::from_cols_array_2d(&[ + self.affine.matrix2.x_axis.extend(0.).extend(0.).to_array(), + self.affine.matrix2.y_axis.extend(0.).extend(0.).to_array(), + [0., 0., 1., 0.], + translation.extend(1.).to_array(), + ]) + } + + /// Returns the 2d affine transformation matrix as an [`Affine2`]. + #[inline] + pub fn affine(&self) -> Affine2 { + self.affine + } + + /// Returns the translation on the Z axis. + #[inline] + pub fn z_translation(&self) -> f32 { + self.z_translation + } + + /// Returns the transformation as a [`Transform2d`]. + /// + /// The transform is expected to be non-degenerate and without shearing, or the output + /// will be invalid. + #[inline] + pub fn compute_transform(&self) -> Transform2d { + let (scale, rotation, translation) = self.affine.to_scale_angle_translation(); + + Transform2d { + translation, + rotation, + scale, + z_translation: 0., + } + } +} + +impl From for GlobalTransform2d { + fn from(transform: Transform2d) -> Self { + Self { + affine: transform.compute_affine(), + z_translation: transform.z_translation, + } + } +} + +impl Mul for GlobalTransform2d { + type Output = GlobalTransform2d; + + #[inline] + fn mul(self, other: GlobalTransform2d) -> Self::Output { + GlobalTransform2d { + affine: self.affine * other.affine, + z_translation: self.z_translation + other.z_translation, + } + } +} + +impl From for GlobalTransform2d { + fn from(affine: Affine2) -> Self { + GlobalTransform2d { + affine, + z_translation: 0., + } + } +} diff --git a/crates/bevy_transform/src/components/mod.rs b/crates/bevy_transform/src/components/mod.rs index 10d3f72e3a463..f051a7d7fa797 100644 --- a/crates/bevy_transform/src/components/mod.rs +++ b/crates/bevy_transform/src/components/mod.rs @@ -1,5 +1,28 @@ mod global_transform; +mod global_transform2d; mod transform; +mod transform2d; pub use global_transform::*; +pub use global_transform2d::*; pub use transform::*; +pub use transform2d::*; + +use bevy_ecs::query::{AnyOf, WorldQuery}; + +/// A [`WorldQuery`] that returns the [`GlobalTransform`] if present, otherwise returns the [`GlobalTransform2d`] as a [`GlobalTransform`]. +#[derive(WorldQuery)] +pub struct AnyGlobalTransform { + transforms: AnyOf<(&'static GlobalTransform, &'static GlobalTransform2d)>, +} + +impl AnyGlobalTransformItem<'_> { + /// Returns the [`GlobalTransform`] if present, otherwise returns the [`GlobalTransform2d`] as a [`GlobalTransform`]. + pub fn get(&self) -> GlobalTransform { + match self.transforms { + (Some(&transform_3d), _) => transform_3d, + (None, Some(&transform_2d)) => transform_2d.into(), + (None, None) => unreachable!(), + } + } +} diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 451579f6f304d..168e01a089447 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -1,11 +1,13 @@ -use super::GlobalTransform; +use std::ops::Mul; + +use super::{GlobalTransform, Transform2d}; use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_math::{Affine3A, Mat3, Mat4, Quat, Vec3}; -use bevy_reflect::prelude::*; -use bevy_reflect::Reflect; -use std::ops::Mul; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +#[cfg(feature = "serialize")] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -/// Describe the position of an entity. If the entity has a parent, the position is relative +/// Describe the position of an entity in 3D. If the entity has a parent, the position is relative /// to its parent position. /// /// * To place or move an entity, you should set its [`Transform`]. @@ -36,7 +38,11 @@ use std::ops::Mul; /// [`transform`]: https://github.com/bevyengine/bevy/blob/latest/examples/transforms/transform.rs /// [`Transform`]: super::Transform #[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] #[reflect(Component, Default, PartialEq)] pub struct Transform { /// Position of the entity. In 2d, the last value of the `Vec3` is used for z-ordering. @@ -67,9 +73,7 @@ impl Transform { scale: Vec3::ONE, }; - /// Creates a new [`Transform`] at the position `(x, y, z)`. In 2d, the `z` component - /// is used for z-ordering elements: higher `z`-value will be in front of lower - /// `z`-value. + /// Creates a new [`Transform`] at the position `(x, y, z)`. #[inline] pub const fn from_xyz(x: f32, y: f32, z: f32) -> Self { Self::from_translation(Vec3::new(x, y, z)) @@ -412,6 +416,16 @@ impl From for Transform { } } +impl From for Transform { + fn from(transform: Transform2d) -> Self { + Transform { + translation: transform.translation.extend(transform.z_translation), + rotation: Quat::from_rotation_z(transform.rotation), + scale: transform.scale.extend(1.), + } + } +} + impl Mul for Transform { type Output = Transform; diff --git a/crates/bevy_transform/src/components/transform2d.rs b/crates/bevy_transform/src/components/transform2d.rs new file mode 100644 index 0000000000000..f2518ac93b285 --- /dev/null +++ b/crates/bevy_transform/src/components/transform2d.rs @@ -0,0 +1,390 @@ +use std::ops::Mul; + +use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; +use bevy_math::{Affine2, Mat2, Mat3, Vec2, Vec3}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectFromReflect}; +#[cfg(feature = "serialize")] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +use crate::prelude::GlobalTransform2d; + +/// Describe the position of an entity in 2D. If the entity has a parent, the position is relative +/// to its parent position. +/// +/// * To place or move an entity, you should set its [`Transform2d`]. +/// * To get the global transform of an entity, you should get its [`GlobalTransform2d`]. +/// * To be displayed, an entity must have both a [`Transform2d`] and a [`GlobalTransform2d`]. +/// * You may use the [`TransformBundle`](crate::TransformBundle) to guarantee this. +/// +/// ## [`Transform2d`] and [`GlobalTransform2d`] +/// +/// [`Transform2d`] is the position of an entity relative to its parent position, or the reference +/// frame if it doesn't have a [`Parent`](bevy_hierarchy::Parent). +/// +/// [`GlobalTransform2d`] is the position of an entity relative to the reference frame. +/// +/// [`GlobalTransform2d`] is updated from [`Transform2d`] by systems in the system set +/// [`TransformPropagate`](crate::TransformSystem::TransformPropagate). +/// +/// This system runs during [`PostUpdate`](bevy_app::PostUpdate). If you +/// update the [`Transform2d`] of an entity during this set or after, you will notice a 1 frame lag +/// before the [`GlobalTransform2d`] is updated. +/// +/// [`GlobalTransform2d`]: super::GlobalTransform2d +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Component, Default, PartialEq, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct Transform2d { + /// The translation along the `X` and `Y` axes. + pub translation: Vec2, + /// The rotation in radians. Positive values rotate anti-clockwise. + pub rotation: f32, + /// The scale along the `X` and `Y` axes. + pub scale: Vec2, + /// The translation along the `Z` axis. + /// + /// You might be surprised that 2D entities have a translation along the Z axis, + /// but this third dimension is used when rendering to decide what should appear in front or behind. + /// A higher translation on the Z axis puts the entity closer to the camera, and thus in front of entities with a lower Z translation. + /// + /// Keep in mind that this is relative to the [`Parent`](bevy_hierarchy::Parent)'s `z_translation`. + /// The other fields on [`Transform2d`] don't affect this because they are strictly 2D. + pub z_translation: f32, +} + +impl Default for Transform2d { + fn default() -> Self { + Transform2d::IDENTITY + } +} + +impl Transform2d { + /// Creates a new identity [`Transform2d`], with no translation, rotation, and a scale of 1 on all axes. + /// + /// Translation is `Vec2::ZERO`, rotation is `0.`, scale is `Vec2::ONE` and `z_translation` is `0.`. + pub const IDENTITY: Self = Transform2d { + translation: Vec2::ZERO, + rotation: 0., + scale: Vec2::ONE, + z_translation: 0., + }; + + /// Creates a new [`Transform2d`] at the position `(x, y)`. + /// + /// Rotation will be `0.`, scale will be `Vec2::ONE` and `z_translation` will be `0.`. + #[inline] + pub const fn from_xy(x: f32, y: f32) -> Self { + Transform2d::from_translation(Vec2::new(x, y)) + } + + /// Creates a new [`Transform2d`] at the position `(x, y, z)`. The `z` component + /// is used for z-ordering elements: higher `z`-value will be in front of lower + /// `z`-value. + #[inline] + pub const fn from_xyz(x: f32, y: f32, z: f32) -> Self { + Self::from_translation(Vec2::new(x, y)).with_z_translation(z) + } + + /// Creates a new [`Transform2d`] with `translation`. + /// + /// Rotation will be `0.`, scale will be `Vec2::ONE` and `z_translation` will be `0.`. + #[inline] + pub const fn from_translation(translation: Vec2) -> Self { + Transform2d { + translation, + ..Self::IDENTITY + } + } + + /// Creates a new [`Transform2d`] with `translation`. + /// + /// Rotation will be `0.` and scale will be `Vec2::ONE` + #[inline] + pub const fn from_translation_3d(Vec3 { x, y, z }: Vec3) -> Self { + Transform2d { + translation: Vec2 { x, y }, + z_translation: z, + ..Self::IDENTITY + } + } + + /// Creates a new [`Transform2d`] with `rotation`. + /// + /// Translation will be `Vec2::ZERO`, scale will be `Vec2::ONE` and `z_translation` will be `0.`. + #[inline] + pub const fn from_rotation(rotation: f32) -> Self { + Transform2d { + rotation, + ..Self::IDENTITY + } + } + + /// Creates a new [`Transform2d`] with `scale`. + /// + /// Translation will be `Vec2::ZERO`, rotation will be `0.` and `z_translation` will be `0.` + #[inline] // Hmm const + pub const fn from_scale(scale: Vec2) -> Self { + Transform2d { + scale, + ..Self::IDENTITY + } + } + + /// Returns this [`Transform2d`] with a new translation. + #[must_use] + #[inline] + pub const fn with_translation(mut self, translation: Vec2) -> Self { + self.translation = translation; + self + } + + /// Returns this [`Transform2d`] with a new rotation. + #[must_use] + #[inline] + pub const fn with_rotation(mut self, rotation: f32) -> Self { + self.rotation = rotation; + self + } + + /// Returns this [`Transform2d`] with a new scale. + #[must_use] + #[inline] + pub const fn with_scale(mut self, scale: Vec2) -> Self { + self.scale = scale; + self + } + + /// Returns this [`Transform2d`] with a new Z translation. + #[must_use] + #[inline] + pub const fn with_z_translation(mut self, z_translation: f32) -> Self { + self.z_translation = z_translation; + self + } + + /// Returns this [`Transform2d`] rotated so the local `direction` points in the given `target_direction`. + /// + /// # Example + /// ``` + /// # use bevy_transform::prelude::*; + /// # use bevy_math::prelude::*; + /// // Create a transform rotated so that up(Vec2::Y) points to the right. + /// let transform = Transform2d::IDENTITY.pointed_to(Vec2::Y, Vec2::X); + /// + /// approx::assert_abs_diff_eq!(transform.up(), Vec2::X); + /// ``` + /// + /// If this [`Transform2d`] has a parent, the `point` is relative to the [`Transform2d`] of the parent. + #[inline] + pub fn pointed_to(mut self, direction: Vec2, target_direction: Vec2) -> Self { + self.point_to(direction, target_direction); + self + } + + /// Returns this [`Transform2d`] rotated so the local `direction` points at the given `target_position`. + /// + /// # Example + /// ``` + /// # use bevy_transform::prelude::*; + /// # use bevy_math::prelude::*; + /// // Create a transform that is translated to Vec2::ONE and then rotated so that up(Vec2::Y) points to the origin. + /// let transform = Transform2d::from_translation(Vec2::ONE) + /// .pointed_at(Vec2::Y, Vec2::ZERO); + /// + /// approx::assert_abs_diff_eq!(transform.up(), Vec2::NEG_ONE.normalize()); + /// ``` + /// + /// If this [`Transform2d`] has a parent, the `point` is relative to the [`Transform2d`] of the parent. + #[inline] + pub fn pointed_at(mut self, direction: Vec2, target_position: Vec2) -> Self { + self.point_at(direction, target_position); + self + } + + /// Rotates this [`Transform2d`] so the local `direction` points in the given `target_direction`. + /// + /// # Example + /// ``` + /// # use bevy_transform::prelude::*; + /// # use bevy_math::prelude::*; + /// let mut transform = Transform2d::IDENTITY; + /// + /// // Rotate the transform so that up(Vec2::Y) points to the right. + /// transform.point_to(Vec2::Y, Vec2::X); + /// + /// approx::assert_abs_diff_eq!(transform.up(), Vec2::X); + /// ``` + /// + /// If this [`Transform2d`] has a parent, the `point` is relative to the [`Transform2d`] of the parent. + #[inline] + pub fn point_to(&mut self, direction: Vec2, target_direction: Vec2) { + self.rotation = Vec2::angle_between(direction, target_direction); + } + + /// Rotates this [`Transform2d`] so the local `direction` points at the given `target_position`. + /// + /// # Example + /// ``` + /// # use bevy_transform::prelude::*; + /// # use bevy_math::prelude::*; + /// let mut transform = Transform2d::from_translation(Vec2::ONE); + /// + /// // Rotate the transform so that `up`/local_y points to the origin. + /// transform.point_at(Vec2::Y, Vec2::ZERO); + /// + /// approx::assert_abs_diff_eq!(transform.up(), Vec2::NEG_ONE.normalize()); + /// ``` + /// + /// If this [`Transform2d`] has a parent, the `point` is relative to the [`Transform2d`] of the parent. + #[inline] + pub fn point_at(&mut self, direction: Vec2, target_position: Vec2) { + self.point_to(direction, target_position - self.translation); + } + + /// Get the unit vector in the local `X` direction. + #[inline] + pub fn local_x(&self) -> Vec2 { + let (sin, cos) = self.rotation.sin_cos(); + (cos, sin).into() + } + + #[inline] + /// Equivalent to [`-local_x()`][Self::local_x()] + pub fn left(&self) -> Vec2 { + -self.local_x() + } + + #[inline] + /// Equivalent to [`local_x()`][Self::local_x()] + pub fn right(&self) -> Vec2 { + self.local_x() + } + + /// Get the unit vector in the local `Y` direction. + #[inline] + pub fn local_y(&self) -> Vec2 { + let (sin, cos) = self.rotation.sin_cos(); + (-sin, cos).into() + } + + /// Equivalent to [`local_y()`][Self::local_y] + #[inline] + pub fn up(&self) -> Vec2 { + self.local_y() + } + + /// Equivalent to [`-local_y()`][Self::local_y] + #[inline] + pub fn down(&self) -> Vec2 { + -self.local_y() + } + + /// Returns the rotation matrix from this transforms rotation. + #[inline] + pub fn rotation_matrix(&self) -> Mat2 { + Mat2::from_angle(self.rotation) + } + + /// Computes the affine transformation matrix of this transform. + #[inline] + pub fn compute_matrix(&self) -> Mat3 { + Mat3::from_scale_angle_translation(self.scale, self.rotation, self.translation) + } + + /// Computes the affine transform of this transform. + #[inline] + pub fn compute_affine(&self) -> Affine2 { + Affine2::from_scale_angle_translation(self.scale, self.rotation, self.translation) + } + + /// Translates this [`Transform2d`] around a `point` in space. + /// + /// If this [`Transform2d`] has a parent, the `point` is relative to the [`Transform2d`] of the parent. + #[inline] + pub fn translate_around(&mut self, point: Vec2, angle: f32) { + self.translation = point + Mat2::from_angle(angle) * (self.translation - point); + } + + /// Rotates this [`Transform2d`] around a `point` in space. + /// + /// If this [`Transform2d`] has a parent, the `point` is relative to the [`Transform2d`] of the parent. + #[inline] + pub fn rotate_around(&mut self, point: Vec2, angle: f32) { + self.translate_around(point, angle); + self.rotation += angle; + } + + /// Transforms the given `point`, applying scale, rotation and translation. + /// `z_translation` is ignored. + /// + /// If this [`Transform2d`] has a parent, this will transform a `point` that is + /// relative to the parent's [`Transform2d`] into one relative to this [`Transform2d`]. + /// + /// If this [`Transform2d`] does not have a parent, this will transform a `point` + /// that is in global space into one relative to this [`Transform2d`]. + /// + /// If you want to transform a `point` in global space to the local space of this [`Transform2d`], + /// consider using [`GlobalTransform2d::transform_point()`](super::GlobalTransform2d::transform_point) instead. + #[inline] + pub fn transform_point(&self, mut point: Vec2) -> Vec2 { + point *= self.scale; + point = self.rotation_matrix() * point; + point += self.translation; + point + } + + /// Multiplies `self` with `transform` component by component, returning the + /// resulting [`Transform2d`] + #[inline] + #[must_use] + pub fn mul_transform(&self, transform: Transform2d) -> Self { + let translation = self.transform_point(transform.translation); + let rotation = self.rotation + transform.rotation; + let scale = self.scale * transform.scale; + let z_translation = self.z_translation + transform.z_translation; + Transform2d { + translation, + rotation, + scale, + z_translation, + } + } +} + +impl Mul for Transform2d { + type Output = Transform2d; + + fn mul(self, transform: Transform2d) -> Self::Output { + self.mul_transform(transform) + } +} + +impl Mul for Transform2d { + type Output = GlobalTransform2d; + + #[inline] + fn mul(self, global_transform: GlobalTransform2d) -> Self::Output { + GlobalTransform2d::from(self) * global_transform + } +} + +#[cfg(test)] +mod tests { + use std::f32::consts::TAU; + + use super::*; + + #[test] + fn local_vectors() { + let mut transform = Transform2d::from_rotation(TAU / 2.44); + assert_eq!(transform.local_y(), transform.rotation_matrix() * Vec2::Y); + assert_eq!(transform.local_x(), transform.rotation_matrix() * Vec2::X); + transform.rotation = TAU / -0.56; + assert_eq!(transform.local_y(), transform.rotation_matrix() * Vec2::Y); + assert_eq!(transform.local_x(), transform.rotation_matrix() * Vec2::X); + } +} diff --git a/crates/bevy_transform/src/helper.rs b/crates/bevy_transform/src/helper.rs index 67a92db7328b3..f1728de105163 100644 --- a/crates/bevy_transform/src/helper.rs +++ b/crates/bevy_transform/src/helper.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ }; use bevy_hierarchy::{HierarchyQueryExt, Parent}; -use crate::components::{GlobalTransform, Transform}; +use crate::components::{GlobalTransform, GlobalTransform2d, Transform, Transform2d}; /// System parameter for computing up-to-date [`GlobalTransform`]s. /// @@ -47,6 +47,44 @@ impl<'w, 's> TransformHelper<'w, 's> { } } +/// System parameter for computing up-to-date [`GlobalTransform2d`]s. +/// +/// Computing an entity's [`GlobalTransform2d`] can be expensive so it is recommended +/// you use the [`GlobalTransform2d`] component stored on the entity, unless you need +/// a [`GlobalTransform2d`] that reflects the changes made to any [`Transform`]s since +/// the last time the transform propagation systems ran. +#[derive(SystemParam)] +pub struct Transform2dHelper<'w, 's> { + parent_query: Query<'w, 's, &'static Parent>, + transform_query: Query<'w, 's, &'static Transform2d>, +} + +impl<'w, 's> Transform2dHelper<'w, 's> { + /// Computes the [`GlobalTransform2d`] of the given entity from the [`Transform`] component on it and its ancestors. + pub fn compute_global_transform( + &self, + entity: Entity, + ) -> Result { + let transform = self + .transform_query + .get(entity) + .map_err(|err| map_error(err, false))?; + + let mut global_transform = GlobalTransform2d::from(*transform); + + for entity in self.parent_query.iter_ancestors(entity) { + let transform = self + .transform_query + .get(entity) + .map_err(|err| map_error(err, true))?; + + global_transform = *transform * global_transform; + } + + Ok(global_transform) + } +} + fn map_error(err: QueryEntityError, ancestor: bool) -> ComputeGlobalTransformError { use ComputeGlobalTransformError::*; match err { @@ -65,7 +103,7 @@ fn map_error(err: QueryEntityError, ancestor: bool) -> ComputeGlobalTransformErr /// Error returned by [`TransformHelper::compute_global_transform`]. #[derive(Debug)] pub enum ComputeGlobalTransformError { - /// The entity or one of its ancestors is missing the [`Transform`] component. + /// The entity or one of its ancestors is missing the [`Transform`] or [`Transform2d`] component. MissingTransform(Entity), /// The entity does not exist. NoSuchEntity(Entity), diff --git a/crates/bevy_transform/src/lib.rs b/crates/bevy_transform/src/lib.rs index 5c176c4b8bbc2..bddd9dc3744c8 100644 --- a/crates/bevy_transform/src/lib.rs +++ b/crates/bevy_transform/src/lib.rs @@ -15,7 +15,7 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ commands::BuildChildrenTransformExt, components::*, helper::TransformHelper, - TransformBundle, TransformPlugin, TransformPoint, + Transform2dBundle, TransformBundle, TransformPlugin, TransformPoint, }; } @@ -24,7 +24,7 @@ use bevy_ecs::prelude::*; use bevy_hierarchy::ValidParentCheckPlugin; use bevy_math::{Affine3A, Mat4, Vec3}; -use prelude::{GlobalTransform, Transform}; +use components::{GlobalTransform, GlobalTransform2d, Transform, Transform2d}; use systems::{propagate_transforms, sync_simple_transforms}; /// A [`Bundle`] of the [`Transform`] and [`GlobalTransform`] @@ -82,6 +82,63 @@ impl From for TransformBundle { Self::from_transform(transform) } } + +/// A [`Bundle`] of the [`Transform2d`] and [`GlobalTransform2d`] +/// [`Component`](bevy_ecs::component::Component)s, which describe the position of an entity. +/// +/// * To place or move an entity, you should set its [`Transform2d`]. +/// * To get the global transform of an entity, you should get its [`GlobalTransform2d`]. +/// * For transform hierarchies to work correctly, you must have both a [`Transform2d`] and a [`GlobalTransform2d`]. +/// * You may use the [`TransformBundle`] to guarantee this. +/// +/// ## [`Transform2d`] and [`GlobalTransform2d`] +/// +/// [`Transform2d`] is the position of an entity relative to its parent position, or the reference +/// frame if it doesn't have a parent. +/// +/// [`GlobalTransform2d`] is the position of an entity relative to the reference frame. +/// +/// [`GlobalTransform2d`] is updated from [`Transform2d`] by systems in the system set +/// [`TransformPropagate`](crate::TransformSystem::TransformPropagate). +/// +/// This system runs during [`PostUpdate`](bevy_app::PostUpdate). If you +/// update the [`Transform2d`] of an entity in this schedule or after, you will notice a 1 frame lag +/// before the [`GlobalTransform2d`] is updated. +#[derive(Bundle, Clone, Copy, Debug, Default)] +pub struct Transform2dBundle { + /// The transform of the entity. + pub local: Transform2d, + /// The global transform of the entity. + pub global: GlobalTransform2d, +} + +impl Transform2dBundle { + /// An identity [`Transform2dBundle`] with no translation, rotation, and a scale of 1 on all axes. + pub const IDENTITY: Self = Transform2dBundle { + local: Transform2d::IDENTITY, + global: GlobalTransform2d::IDENTITY, + }; + + /// Creates a new [`Transform2dBundle`] from a [`Transform2d`]. + /// + /// This initializes [`GlobalTransform2d`] as identity, to be updated later by the + /// [`PostUpdate`](bevy_app::PostUpdate) schedule. + #[inline] + pub const fn from_transform(transform: Transform2d) -> Self { + Transform2dBundle { + local: transform, + ..Self::IDENTITY + } + } +} + +impl From for Transform2dBundle { + #[inline] + fn from(transform: Transform2d) -> Self { + Self::from_transform(transform) + } +} + /// Set enum for the systems relating to transform propagation #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum TransformSystem { @@ -101,8 +158,13 @@ impl Plugin for TransformPlugin { struct PropagateTransformsSet; app.register_type::() + .register_type::() .register_type::() - .add_plugins(ValidParentCheckPlugin::::default()) + .register_type::() + .add_plugins(( + ValidParentCheckPlugin::::default(), + ValidParentCheckPlugin::::default(), + )) .configure_sets( PostStartup, PropagateTransformsSet.in_set(TransformSystem::TransformPropagate), @@ -111,13 +173,19 @@ impl Plugin for TransformPlugin { .add_systems( PostStartup, ( - sync_simple_transforms + // FIXME: https://github.com/bevyengine/bevy/issues/4381 + // These systems cannot access the same entities, + // due to subtle query filtering that is not yet correctly computed in the ambiguity detector + sync_simple_transforms:: .in_set(TransformSystem::TransformPropagate) - // FIXME: https://github.com/bevyengine/bevy/issues/4381 - // These systems cannot access the same entities, - // due to subtle query filtering that is not yet correctly computed in the ambiguity detector .ambiguous_with(PropagateTransformsSet), - propagate_transforms.in_set(PropagateTransformsSet), + propagate_transforms:: + .in_set(PropagateTransformsSet), + sync_simple_transforms:: + .in_set(TransformSystem::TransformPropagate) + .ambiguous_with(PropagateTransformsSet), + propagate_transforms:: + .in_set(PropagateTransformsSet), ), ) .configure_sets( @@ -127,10 +195,16 @@ impl Plugin for TransformPlugin { .add_systems( PostUpdate, ( - sync_simple_transforms + sync_simple_transforms:: + .in_set(TransformSystem::TransformPropagate) + .ambiguous_with(PropagateTransformsSet), + propagate_transforms:: + .in_set(PropagateTransformsSet), + sync_simple_transforms:: .in_set(TransformSystem::TransformPropagate) .ambiguous_with(PropagateTransformsSet), - propagate_transforms.in_set(PropagateTransformsSet), + propagate_transforms:: + .in_set(PropagateTransformsSet), ), ); } diff --git a/crates/bevy_transform/src/systems.rs b/crates/bevy_transform/src/systems.rs index b25784c3889fe..17d286dbe0cc0 100644 --- a/crates/bevy_transform/src/systems.rs +++ b/crates/bevy_transform/src/systems.rs @@ -1,61 +1,64 @@ -use crate::components::{GlobalTransform, Transform}; +use std::ops::Mul; + use bevy_ecs::{ change_detection::Ref, - prelude::{Changed, DetectChanges, Entity, Query, With, Without}, + prelude::{Changed, Component, DetectChanges, Entity, Query, With, Without}, query::{Added, Or}, removal_detection::RemovedComponents, system::{Local, ParamSet}, }; use bevy_hierarchy::{Children, Parent}; -/// Update [`GlobalTransform`] component of entities that aren't in the hierarchy +/// Update [`GlobalTransform`](super::GlobalTransform) component of entities that aren't in the hierarchy /// /// Third party plugins should ensure that this is used in concert with [`propagate_transforms`]. -pub fn sync_simple_transforms( +pub fn sync_simple_transforms( mut query: ParamSet<( Query< - (&Transform, &mut GlobalTransform), + (&A, &mut B), ( - Or<(Changed, Added)>, + Or<(Changed, Added)>, Without, Without, ), >, - Query<(Ref, &mut GlobalTransform), (Without, Without)>, + Query<(Ref, &mut B), (Without, Without)>, )>, mut orphaned: RemovedComponents, -) { - // Update changed entities. +) where + A: Component + Copy + Into, + B: Component + Copy + Mul, +{ query .p0() .par_iter_mut() .for_each(|(transform, mut global_transform)| { - *global_transform = GlobalTransform::from(*transform); + *global_transform = (*transform).into(); }); // Update orphaned entities. let mut query = query.p1(); let mut iter = query.iter_many_mut(orphaned.read()); while let Some((transform, mut global_transform)) = iter.fetch_next() { if !transform.is_changed() && !global_transform.is_added() { - *global_transform = GlobalTransform::from(*transform); + *global_transform = (*transform).into(); } } } -/// Update [`GlobalTransform`] component of entities based on entity hierarchy and -/// [`Transform`] component. +/// Update [`GlobalTransform`](super::GlobalTransform) component of entities based on entity hierarchy and +/// [`Transform`](super::Transform) component. /// /// Third party plugins should ensure that this is used in concert with [`sync_simple_transforms`]. -pub fn propagate_transforms( - mut root_query: Query< - (Entity, &Children, Ref, &mut GlobalTransform), - Without, - >, +pub fn propagate_transforms( + mut root_query: Query<(Entity, &Children, Ref, &mut B), Without>, mut orphaned: RemovedComponents, - transform_query: Query<(Ref, &mut GlobalTransform, Option<&Children>), With>, + transform_query: Query<(Ref, &mut B, Option<&Children>), With>, parent_query: Query<(Entity, Ref)>, mut orphaned_entities: Local>, -) { +) where + A: Component + Copy, + B: Component + Copy + From + Mul, +{ orphaned_entities.clear(); orphaned_entities.extend(orphaned.read()); orphaned_entities.sort_unstable(); @@ -63,7 +66,7 @@ pub fn propagate_transforms( |(entity, children, transform, mut global_transform)| { let changed = transform.is_changed() || global_transform.is_added() || orphaned_entities.binary_search(&entity).is_ok(); if changed { - *global_transform = GlobalTransform::from(*transform); + *global_transform = (*transform).into(); } for (child, actual_parent) in parent_query.iter_many(children) { @@ -81,7 +84,7 @@ pub fn propagate_transforms( // - Since this is the only place where `transform_query` gets used, there will be no conflicting fetches elsewhere. unsafe { propagate_recursive( - &global_transform, + &*global_transform, &transform_query, &parent_query, child, @@ -106,16 +109,16 @@ pub fn propagate_transforms( /// nor any of its descendants. /// - The caller must ensure that the hierarchy leading to `entity` /// is well-formed and must remain as a tree or a forest. Each entity must have at most one parent. -unsafe fn propagate_recursive( - parent: &GlobalTransform, - transform_query: &Query< - (Ref, &mut GlobalTransform, Option<&Children>), - With, - >, +unsafe fn propagate_recursive( + parent: &B, + transform_query: &Query<(Ref, &mut B, Option<&Children>), With>, parent_query: &Query<(Entity, Ref)>, entity: Entity, mut changed: bool, -) { +) where + A: Component + Copy, + B: Component + Copy + From + Mul, +{ let (global_matrix, children) = { let Ok((transform, mut global_transform, children)) = // SAFETY: This call cannot create aliased mutable references. @@ -150,7 +153,7 @@ unsafe fn propagate_recursive( changed |= transform.is_changed() || global_transform.is_added(); if changed { - *global_transform = parent.mul_transform(*transform); + *global_transform = *parent * B::from(*transform); } (*global_transform, children) }; @@ -201,7 +204,10 @@ mod test { |offset| TransformBundle::from_transform(Transform::from_xyz(offset, offset, offset)); let mut schedule = Schedule::default(); - schedule.add_systems((sync_simple_transforms, propagate_transforms)); + schedule.add_systems(( + sync_simple_transforms::, + propagate_transforms::, + )); let mut command_queue = CommandQueue::default(); let mut commands = Commands::new(&mut command_queue, &world); @@ -252,7 +258,10 @@ mod test { let mut world = World::default(); let mut schedule = Schedule::default(); - schedule.add_systems((sync_simple_transforms, propagate_transforms)); + schedule.add_systems(( + sync_simple_transforms::, + propagate_transforms::, + )); // Root entity world.spawn(TransformBundle::from(Transform::from_xyz(1.0, 0.0, 0.0))); @@ -290,7 +299,10 @@ mod test { let mut world = World::default(); let mut schedule = Schedule::default(); - schedule.add_systems((sync_simple_transforms, propagate_transforms)); + schedule.add_systems(( + sync_simple_transforms::, + propagate_transforms::, + )); // Root entity let mut queue = CommandQueue::default(); @@ -330,7 +342,10 @@ mod test { let mut world = World::default(); let mut schedule = Schedule::default(); - schedule.add_systems((sync_simple_transforms, propagate_transforms)); + schedule.add_systems(( + sync_simple_transforms::, + propagate_transforms::, + )); // Add parent entities let mut children = Vec::new(); @@ -406,7 +421,13 @@ mod test { let mut app = App::new(); ComputeTaskPool::init(TaskPool::default); - app.add_systems(Update, (sync_simple_transforms, propagate_transforms)); + app.add_systems( + Update, + ( + sync_simple_transforms::, + propagate_transforms::, + ), + ); let translation = vec3(1.0, 0.0, 0.0); @@ -452,7 +473,13 @@ mod test { let mut temp = World::new(); let mut app = App::new(); - app.add_systems(Update, (propagate_transforms, sync_simple_transforms)); + app.add_systems( + Update, + ( + propagate_transforms::, + sync_simple_transforms::, + ), + ); fn setup_world(world: &mut World) -> (Entity, Entity) { let mut grandchild = Entity::from_raw(0); @@ -489,7 +516,10 @@ mod test { // Create transform propagation schedule let mut schedule = Schedule::default(); - schedule.add_systems((sync_simple_transforms, propagate_transforms)); + schedule.add_systems(( + sync_simple_transforms::, + propagate_transforms::, + )); // Spawn a `TransformBundle` entity with a local translation of `Vec3::ONE` let mut spawn_transform_bundle = || { diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index 7fa168fe697c1..2327eef605fb1 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -20,7 +20,7 @@ fn setup( commands.spawn(MaterialMesh2dBundle { mesh: meshes.add(shape::Circle::new(50.).into()).into(), material: materials.add(ColorMaterial::from(Color::PURPLE)), - transform: Transform::from_translation(Vec3::new(-150., 0., 0.)), + transform: Transform2d::from_translation(Vec2::new(-150., 0.)), ..default() }); @@ -31,7 +31,7 @@ fn setup( custom_size: Some(Vec2::new(50.0, 100.0)), ..default() }, - transform: Transform::from_translation(Vec3::new(-50., 0., 0.)), + transform: Transform2d::from_translation(Vec2::new(-50., 0.)), ..default() }); @@ -41,7 +41,7 @@ fn setup( .add(shape::Quad::new(Vec2::new(50., 100.)).into()) .into(), material: materials.add(ColorMaterial::from(Color::LIME_GREEN)), - transform: Transform::from_translation(Vec3::new(50., 0., 0.)), + transform: Transform2d::from_translation(Vec2::new(50., 0.)), ..default() }); @@ -49,7 +49,7 @@ fn setup( commands.spawn(MaterialMesh2dBundle { mesh: meshes.add(shape::RegularPolygon::new(50., 6).into()).into(), material: materials.add(ColorMaterial::from(Color::TURQUOISE)), - transform: Transform::from_translation(Vec3::new(150., 0., 0.)), + transform: Transform2d::from_translation(Vec2::new(150., 0.)), ..default() }); } diff --git a/examples/2d/2d_viewport_to_world.rs b/examples/2d/2d_viewport_to_world.rs index 8ed4f881f993f..7df3cc1b433ee 100644 --- a/examples/2d/2d_viewport_to_world.rs +++ b/examples/2d/2d_viewport_to_world.rs @@ -11,7 +11,7 @@ fn main() { } fn draw_cursor( - camera_query: Query<(&Camera, &GlobalTransform)>, + camera_query: Query<(&Camera, &GlobalTransform2d)>, windows: Query<&Window>, mut gizmos: Gizmos, ) { diff --git a/examples/2d/bloom_2d.rs b/examples/2d/bloom_2d.rs index 108cc94c571f1..2919b6c841ab6 100644 --- a/examples/2d/bloom_2d.rs +++ b/examples/2d/bloom_2d.rs @@ -52,7 +52,7 @@ fn setup( mesh: meshes.add(shape::Circle::new(100.).into()).into(), // 4. Put something bright in a dark environment to see the effect material: materials.add(ColorMaterial::from(Color::rgb(7.5, 0.0, 7.5))), - transform: Transform::from_translation(Vec3::new(-200., 0., 0.)), + transform: Transform2d::from_translation(Vec2::new(-200., 0.)), ..default() }); @@ -63,7 +63,7 @@ fn setup( .into(), // 4. Put something bright in a dark environment to see the effect material: materials.add(ColorMaterial::from(Color::rgb(6.25, 9.4, 9.1))), - transform: Transform::from_translation(Vec3::new(200., 0., 0.)), + transform: Transform2d::from_translation(Vec2::new(200., 0.)), ..default() }); diff --git a/examples/2d/custom_gltf_vertex_attribute.rs b/examples/2d/custom_gltf_vertex_attribute.rs index 2f602d92b64fa..2aa75ed32f145 100644 --- a/examples/2d/custom_gltf_vertex_attribute.rs +++ b/examples/2d/custom_gltf_vertex_attribute.rs @@ -46,7 +46,7 @@ fn setup( commands.spawn(MaterialMesh2dBundle { mesh: Mesh2dHandle(mesh), material: materials.add(CustomMaterial {}), - transform: Transform::from_scale(150.0 * Vec3::ONE), + transform: Transform2d::from_scale(Vec2::splat(150.)), ..default() }); diff --git a/examples/2d/mesh2d.rs b/examples/2d/mesh2d.rs index 4b10c0413cc81..801c24771608c 100644 --- a/examples/2d/mesh2d.rs +++ b/examples/2d/mesh2d.rs @@ -17,7 +17,7 @@ fn setup( commands.spawn(Camera2dBundle::default()); commands.spawn(MaterialMesh2dBundle { mesh: meshes.add(Mesh::from(shape::Quad::default())).into(), - transform: Transform::default().with_scale(Vec3::splat(128.)), + transform: Transform2d::from_scale(Vec2::splat(128.)), material: materials.add(ColorMaterial::from(Color::PURPLE)), ..default() }); diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 0b8856c4132de..67a321ea7ee4e 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -102,7 +102,7 @@ fn star( // The `Handle` needs to be wrapped in a `Mesh2dHandle` to use 2d rendering instead of 3d Mesh2dHandle(meshes.add(star)), // This bundle's components are needed for something to be rendered - SpatialBundle::INHERITED_IDENTITY, + Spatial2dBundle::INHERITED_IDENTITY, )); // Spawn the camera diff --git a/examples/2d/mesh2d_vertex_color_texture.rs b/examples/2d/mesh2d_vertex_color_texture.rs index 5e431f454f749..b95e9cf86cc02 100644 --- a/examples/2d/mesh2d_vertex_color_texture.rs +++ b/examples/2d/mesh2d_vertex_color_texture.rs @@ -41,8 +41,7 @@ fn setup( // Spawn the quad with vertex colors commands.spawn(MaterialMesh2dBundle { mesh: mesh_handle.clone(), - transform: Transform::from_translation(Vec3::new(-96., 0., 0.)) - .with_scale(Vec3::splat(128.)), + transform: Transform2d::from_translation(Vec2::new(-96., 0.)).with_scale(Vec2::splat(128.)), material: materials.add(ColorMaterial::default()), ..default() }); @@ -50,8 +49,7 @@ fn setup( // Spawning the quad with vertex colors and a texture results in tinting commands.spawn(MaterialMesh2dBundle { mesh: mesh_handle, - transform: Transform::from_translation(Vec3::new(96., 0., 0.)) - .with_scale(Vec3::splat(128.)), + transform: Transform2d::from_translation(Vec2::new(96., 0.)).with_scale(Vec2::splat(128.)), material: materials.add(ColorMaterial::from(texture_handle)), ..default() }); diff --git a/examples/2d/move_sprite.rs b/examples/2d/move_sprite.rs index 6f0efa53cbe76..a68cce1e347e3 100644 --- a/examples/2d/move_sprite.rs +++ b/examples/2d/move_sprite.rs @@ -21,7 +21,7 @@ fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(( SpriteBundle { texture: asset_server.load("branding/icon.png"), - transform: Transform::from_xyz(100., 0., 0.), + transform: Transform2d::from_xy(100., 0.), ..default() }, Direction::Up, @@ -30,7 +30,10 @@ fn setup(mut commands: Commands, asset_server: Res) { /// The sprite is animated by changing its translation depending on the time that has passed since /// the last frame. -fn sprite_movement(time: Res