diff --git a/crates/bevy_transform/Cargo.toml b/crates/bevy_transform/Cargo.toml index 9f250745232c5..7d413423e043c 100644 --- a/crates/bevy_transform/Cargo.toml +++ b/crates/bevy_transform/Cargo.toml @@ -19,6 +19,8 @@ serde = { version = "1", features = ["derive"], optional = true } [dev-dependencies] bevy_tasks = { path = "../bevy_tasks", version = "0.12.0-dev" } +approx = "0.5.1" +glam = { version = "0.24", features = ["approx"] } [features] serialize = ["dep:serde", "bevy_math/serialize"] diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 5f6f42ab0b91f..451579f6f304d 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -420,6 +420,15 @@ impl Mul for Transform { } } +impl Mul for Transform { + type Output = GlobalTransform; + + #[inline] + fn mul(self, global_transform: GlobalTransform) -> Self::Output { + GlobalTransform::from(self) * global_transform + } +} + impl Mul for Transform { type Output = Vec3; diff --git a/crates/bevy_transform/src/helper.rs b/crates/bevy_transform/src/helper.rs new file mode 100644 index 0000000000000..67a92db7328b3 --- /dev/null +++ b/crates/bevy_transform/src/helper.rs @@ -0,0 +1,142 @@ +//! System parameter for computing up-to-date [`GlobalTransform`]s. + +use bevy_ecs::{ + prelude::Entity, + query::QueryEntityError, + system::{Query, SystemParam}, +}; +use bevy_hierarchy::{HierarchyQueryExt, Parent}; + +use crate::components::{GlobalTransform, Transform}; + +/// System parameter for computing up-to-date [`GlobalTransform`]s. +/// +/// Computing an entity's [`GlobalTransform`] can be expensive so it is recommended +/// you use the [`GlobalTransform`] component stored on the entity, unless you need +/// a [`GlobalTransform`] that reflects the changes made to any [`Transform`]s since +/// the last time the transform propagation systems ran. +#[derive(SystemParam)] +pub struct TransformHelper<'w, 's> { + parent_query: Query<'w, 's, &'static Parent>, + transform_query: Query<'w, 's, &'static Transform>, +} + +impl<'w, 's> TransformHelper<'w, 's> { + /// Computes the [`GlobalTransform`] 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 = GlobalTransform::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 { + QueryEntityError::QueryDoesNotMatch(entity) => MissingTransform(entity), + QueryEntityError::NoSuchEntity(entity) => { + if ancestor { + MalformedHierarchy(entity) + } else { + NoSuchEntity(entity) + } + } + QueryEntityError::AliasedMutability(_) => unreachable!(), + } +} + +/// Error returned by [`TransformHelper::compute_global_transform`]. +#[derive(Debug)] +pub enum ComputeGlobalTransformError { + /// The entity or one of its ancestors is missing the [`Transform`] component. + MissingTransform(Entity), + /// The entity does not exist. + NoSuchEntity(Entity), + /// An ancestor is missing. + /// This probably means that your hierarchy has been improperly maintained. + MalformedHierarchy(Entity), +} + +#[cfg(test)] +mod tests { + use std::f32::consts::TAU; + + use bevy_app::App; + use bevy_ecs::system::SystemState; + use bevy_hierarchy::BuildWorldChildren; + use bevy_math::{Quat, Vec3}; + + use crate::{ + components::{GlobalTransform, Transform}, + helper::TransformHelper, + TransformBundle, TransformPlugin, + }; + + #[test] + fn match_transform_propagation_systems() { + // Single transform + match_transform_propagation_systems_inner(vec![Transform::from_translation(Vec3::X) + .with_rotation(Quat::from_rotation_y(TAU / 4.)) + .with_scale(Vec3::splat(2.))]); + + // Transform hierarchy + match_transform_propagation_systems_inner(vec![ + Transform::from_translation(Vec3::X) + .with_rotation(Quat::from_rotation_y(TAU / 4.)) + .with_scale(Vec3::splat(2.)), + Transform::from_translation(Vec3::Y) + .with_rotation(Quat::from_rotation_z(TAU / 3.)) + .with_scale(Vec3::splat(1.5)), + Transform::from_translation(Vec3::Z) + .with_rotation(Quat::from_rotation_x(TAU / 2.)) + .with_scale(Vec3::splat(0.3)), + ]); + } + + fn match_transform_propagation_systems_inner(transforms: Vec) { + let mut app = App::new(); + app.add_plugins(TransformPlugin); + + let mut entity = None; + + for transform in transforms { + let mut e = app.world.spawn(TransformBundle::from(transform)); + + if let Some(entity) = entity { + e.set_parent(entity); + } + + entity = Some(e.id()); + } + + let leaf_entity = entity.unwrap(); + + app.update(); + + let transform = *app.world.get::(leaf_entity).unwrap(); + + let mut state = SystemState::::new(&mut app.world); + let helper = state.get(&app.world); + + let computed_transform = helper.compute_global_transform(leaf_entity).unwrap(); + + approx::assert_abs_diff_eq!(transform.affine(), computed_transform.affine()); + } +} diff --git a/crates/bevy_transform/src/lib.rs b/crates/bevy_transform/src/lib.rs index a1e9747eaf129..5c176c4b8bbc2 100644 --- a/crates/bevy_transform/src/lib.rs +++ b/crates/bevy_transform/src/lib.rs @@ -6,6 +6,7 @@ pub mod commands; /// The basic components of the transform crate pub mod components; +pub mod helper; /// Systems responsible for transform propagation pub mod systems; @@ -13,8 +14,8 @@ pub mod systems; pub mod prelude { #[doc(hidden)] pub use crate::{ - commands::BuildChildrenTransformExt, components::*, TransformBundle, TransformPlugin, - TransformPoint, + commands::BuildChildrenTransformExt, components::*, helper::TransformHelper, + TransformBundle, TransformPlugin, TransformPoint, }; }