avian3d/physics_transform/
mod.rs

1//! Manages physics transforms and synchronizes them with [`Transform`].
2//!
3//! See [`PhysicsTransformPlugin`].
4
5mod transform;
6pub use transform::{Position, PreSolveDeltaPosition, PreSolveDeltaRotation, Rotation};
7#[allow(unused_imports)]
8pub(crate) use transform::{RotationValue, init_physics_transform};
9
10mod helper;
11pub use helper::PhysicsTransformHelper;
12
13#[cfg(test)]
14mod tests;
15
16use crate::{
17    prelude::*,
18    schedule::{LastPhysicsTick, is_changed_after_tick},
19};
20use approx::AbsDiffEq;
21use bevy::{
22    ecs::{component::Tick, intern::Interned, schedule::ScheduleLabel, system::SystemChangeTick},
23    prelude::*,
24    transform::systems::{mark_dirty_trees, propagate_parent_transforms, sync_simple_transforms},
25};
26
27/// Manages physics transforms and synchronizes them with [`Transform`].
28///
29/// # Syncing Between [`Position`]/[`Rotation`] and [`Transform`]
30///
31/// By default, each body's `Transform` will be updated when [`Position`] or [`Rotation`]
32/// change, and vice versa. This means that you can use any of these components to move
33/// or position bodies, and the changes be reflected in the other components.
34///
35/// You can configure what data is synchronized and how it is synchronized
36/// using the [`PhysicsTransformConfig`] resource.
37///
38/// # `Transform` Hierarchies
39///
40/// When synchronizing changes in [`Position`] or [`Rotation`] to `Transform`,
41/// the engine treats nested [rigid bodies](RigidBody) as a flat structure. This means that
42/// the bodies move independently of the parents, and moving the parent will not affect the child.
43///
44/// If you would like a child entity to be rigidly attached to its parent, you could use a [`FixedJoint`]
45/// or write your own system to handle hierarchies differently.
46pub struct PhysicsTransformPlugin {
47    schedule: Interned<dyn ScheduleLabel>,
48}
49
50impl PhysicsTransformPlugin {
51    /// Creates a [`PhysicsTransformPlugin`] with the schedule that is used for running the [`PhysicsSchedule`].
52    ///
53    /// The default schedule is `FixedPostUpdate`.
54    pub fn new(schedule: impl ScheduleLabel) -> Self {
55        Self {
56            schedule: schedule.intern(),
57        }
58    }
59}
60
61impl Default for PhysicsTransformPlugin {
62    fn default() -> Self {
63        Self::new(FixedPostUpdate)
64    }
65}
66
67impl Plugin for PhysicsTransformPlugin {
68    fn build(&self, app: &mut App) {
69        app.init_resource::<PhysicsTransformConfig>();
70
71        if app
72            .world()
73            .resource::<PhysicsTransformConfig>()
74            .position_to_transform
75        {
76            app.register_required_components::<Position, Transform>();
77            app.register_required_components::<Rotation, Transform>();
78        }
79
80        // Run transform propagation and transform-to-position synchronization before physics.
81        app.configure_sets(
82            self.schedule,
83            (
84                PhysicsTransformSystems::Propagate,
85                PhysicsTransformSystems::TransformToPosition,
86            )
87                .chain()
88                .in_set(PhysicsSystems::Prepare),
89        );
90        app.add_systems(
91            self.schedule,
92            (
93                mark_dirty_trees,
94                propagate_parent_transforms,
95                sync_simple_transforms,
96            )
97                .chain()
98                .in_set(PhysicsTransformSystems::Propagate)
99                .run_if(|config: Res<PhysicsTransformConfig>| config.propagate_before_physics),
100        );
101        app.add_systems(
102            self.schedule,
103            transform_to_position
104                .in_set(PhysicsTransformSystems::TransformToPosition)
105                .run_if(|config: Res<PhysicsTransformConfig>| config.transform_to_position),
106        );
107
108        // Run position-to-transform synchronization after physics.
109        app.configure_sets(
110            self.schedule,
111            PhysicsTransformSystems::PositionToTransform.in_set(PhysicsSystems::Writeback),
112        );
113        app.add_systems(
114            self.schedule,
115            position_to_transform
116                .in_set(PhysicsTransformSystems::PositionToTransform)
117                .run_if(|config: Res<PhysicsTransformConfig>| config.position_to_transform),
118        );
119    }
120}
121
122/// Configures how physics transforms are managed and synchronized with [`Transform`].
123#[derive(Resource, Reflect, Clone, Debug, PartialEq, Eq)]
124#[reflect(Resource)]
125pub struct PhysicsTransformConfig {
126    /// If true, [`Transform`] is propagated before stepping physics to ensure that
127    /// [`GlobalTransform`] is up-to-date.
128    ///
129    /// Default: `true`
130    pub propagate_before_physics: bool,
131    /// Updates [`Position`] and [`Rotation`] based on [`Transform`] changes
132    /// in [`PhysicsTransformSystems::TransformToPosition`],
133    ///
134    /// This allows using transforms for moving and positioning bodies,
135    ///
136    /// Default: `true`
137    pub transform_to_position: bool,
138    /// Updates [`Transform`] based on [`Position`] and [`Rotation`] changes
139    /// in [`PhysicsTransformSystems::PositionToTransform`],
140    ///
141    /// Default: `true`
142    pub position_to_transform: bool,
143    /// Updates [`Collider::scale()`] based on transform changes.
144    ///
145    /// This allows using transforms for scaling colliders.
146    ///
147    /// Default: `true`
148    pub transform_to_collider_scale: bool,
149}
150
151impl Default for PhysicsTransformConfig {
152    fn default() -> Self {
153        PhysicsTransformConfig {
154            propagate_before_physics: true,
155            position_to_transform: true,
156            transform_to_position: true,
157            transform_to_collider_scale: true,
158        }
159    }
160}
161
162/// System sets for managing physics transforms.
163#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
164pub enum PhysicsTransformSystems {
165    /// Propagates [`Transform`] before physics simulation.
166    Propagate,
167    /// Updates [`Position`] and [`Rotation`] based on [`Transform`] changes before physics simulation.
168    TransformToPosition,
169    /// Updates [`Transform`] based on [`Position`] and [`Rotation`] changes after physics simulation.
170    PositionToTransform,
171}
172
173/// A deprecated alias for [`PhysicsTransformSystems`].
174#[deprecated(since = "0.4.0", note = "Renamed to `PhysicsTransformSystems`")]
175pub type PhysicsTransformSet = PhysicsTransformSystems;
176
177/// Copies [`GlobalTransform`] changes to [`Position`] and [`Rotation`].
178/// This allows users to use transforms for moving and positioning bodies and colliders.
179///
180/// To account for hierarchies, transform propagation should be run before this system.
181#[allow(clippy::type_complexity)]
182pub fn transform_to_position(
183    mut query: Query<(&GlobalTransform, &mut Position, &mut Rotation)>,
184    length_unit: Res<PhysicsLengthUnit>,
185    last_physics_tick: Res<LastPhysicsTick>,
186    system_tick: SystemChangeTick,
187) {
188    // On the first tick, the last physics tick and system tick are both defaulted to 0,
189    // but to handle change detection correctly, the system tick should always be larger.
190    // So we use a minimum system tick of 1 here.
191    let this_run = if last_physics_tick.0.get() == 0 {
192        Tick::new(1)
193    } else {
194        system_tick.this_run()
195    };
196
197    // If the `GlobalTransform` translation and `Position` differ by less than 0.01 mm, we ignore the change.
198    let distance_tolerance = length_unit.0 * 1e-5;
199    // If the `GlobalTransform` rotation and `Rotation` differ by less than 0.1 degrees, we ignore the change.
200    let rotation_tolerance = (0.1 as Scalar).to_radians();
201
202    for (global_transform, mut position, mut rotation) in &mut query {
203        let global_transform = global_transform.compute_transform();
204        #[cfg(feature = "2d")]
205        let transform_translation = global_transform.translation.truncate().adjust_precision();
206        #[cfg(feature = "3d")]
207        let transform_translation = global_transform.translation.adjust_precision();
208        let transform_rotation = Rotation::from(global_transform.rotation.adjust_precision());
209
210        let position_changed = !position.is_added()
211            && is_changed_after_tick(
212                Ref::from(position.reborrow()),
213                last_physics_tick.0,
214                this_run,
215            );
216        if !position_changed && position.abs_diff_ne(&transform_translation, distance_tolerance) {
217            position.0 = transform_translation;
218        }
219
220        let rotation_changed = !rotation.is_added()
221            && is_changed_after_tick(
222                Ref::from(rotation.reborrow()),
223                last_physics_tick.0,
224                this_run,
225            );
226        if !rotation_changed
227            && rotation.angle_between(transform_rotation).abs() > rotation_tolerance
228        {
229            *rotation = transform_rotation;
230        }
231    }
232}
233
234/// Marker component indicating that the `position_to_transform` system should be applied
235/// to this entity.
236///
237/// By default, the `position_to_transform` system only runs for entities that have a
238/// [`RigidBody`] component
239#[derive(Component, Default)]
240pub struct ApplyPosToTransform;
241
242type PosToTransformComponents = (
243    &'static mut Transform,
244    &'static Position,
245    &'static Rotation,
246    Option<&'static ChildOf>,
247);
248
249type PosToTransformFilter = (
250    Or<(With<RigidBody>, With<ApplyPosToTransform>)>,
251    Or<(Changed<Position>, Changed<Rotation>)>,
252);
253
254type ParentComponents = (
255    &'static GlobalTransform,
256    Option<&'static Position>,
257    Option<&'static Rotation>,
258);
259
260/// Copies [`Position`] and [`Rotation`] changes to [`Transform`].
261/// This allows users and the engine to use these components for moving and positioning bodies.
262///
263/// Nested rigid bodies move independently of each other, so the [`Transform`]s of child entities are updated
264/// based on their own and their parent's [`Position`] and [`Rotation`].
265#[cfg(feature = "2d")]
266pub fn position_to_transform(
267    mut query: Query<PosToTransformComponents, PosToTransformFilter>,
268    parents: Query<ParentComponents, With<Children>>,
269) {
270    for (mut transform, pos, rot, parent) in &mut query {
271        if let Some(&ChildOf(parent)) = parent {
272            if let Ok((parent_transform, parent_pos, parent_rot)) = parents.get(parent) {
273                // Compute the global transform of the parent using its Position and Rotation
274                let parent_transform = parent_transform.compute_transform();
275                let parent_pos = parent_pos.map_or(parent_transform.translation, |pos| {
276                    pos.f32().extend(parent_transform.translation.z)
277                });
278                let parent_rot = parent_rot.map_or(parent_transform.rotation, |rot| {
279                    Quaternion::from(*rot).f32()
280                });
281                let parent_scale = parent_transform.scale;
282                let parent_transform = Transform::from_translation(parent_pos)
283                    .with_rotation(parent_rot)
284                    .with_scale(parent_scale);
285
286                // The new local transform of the child body,
287                // computed from the its global transform and its parents global transform
288                let new_transform = GlobalTransform::from(
289                    Transform::from_translation(
290                        pos.f32()
291                            .extend(parent_pos.z + transform.translation.z * parent_scale.z),
292                    )
293                    .with_rotation(Quaternion::from(*rot).f32()),
294                )
295                .reparented_to(&GlobalTransform::from(parent_transform));
296
297                transform.translation = new_transform.translation;
298                transform.rotation = new_transform.rotation;
299            }
300        } else {
301            transform.translation = pos.f32().extend(transform.translation.z);
302            transform.rotation = Quaternion::from(*rot).f32();
303        }
304    }
305}
306
307/// Copies [`Position`] and [`Rotation`] changes to [`Transform`].
308/// This allows users and the engine to use these components for moving and positioning bodies.
309///
310/// Nested rigid bodies move independently of each other, so the [`Transform`]s of child entities are updated
311/// based on their own and their parent's [`Position`] and [`Rotation`].
312#[cfg(feature = "3d")]
313pub fn position_to_transform(
314    mut query: Query<PosToTransformComponents, PosToTransformFilter>,
315    parents: Query<ParentComponents, With<Children>>,
316) {
317    for (mut transform, pos, rot, parent) in &mut query {
318        if let Some(&ChildOf(parent)) = parent {
319            if let Ok((parent_transform, parent_pos, parent_rot)) = parents.get(parent) {
320                // Compute the global transform of the parent using its Position and Rotation
321                let parent_transform = parent_transform.compute_transform();
322                let parent_pos = parent_pos.map_or(parent_transform.translation, |pos| pos.f32());
323                let parent_rot = parent_rot.map_or(parent_transform.rotation, |rot| rot.f32());
324                let parent_scale = parent_transform.scale;
325                let parent_transform = Transform::from_translation(parent_pos)
326                    .with_rotation(parent_rot)
327                    .with_scale(parent_scale);
328
329                // The new local transform of the child body,
330                // computed from the its global transform and its parents global transform
331                let new_transform = GlobalTransform::from(
332                    Transform::from_translation(pos.f32()).with_rotation(rot.f32()),
333                )
334                .reparented_to(&GlobalTransform::from(parent_transform));
335
336                transform.translation = new_transform.translation;
337                transform.rotation = new_transform.rotation;
338            }
339        } else {
340            transform.translation = pos.f32();
341            transform.rotation = rot.f32();
342        }
343    }
344}