Skip to main content

avian3d/physics_transform/
mod.rs

1//! Manages physics transforms and synchronizes them with [`Transform`].
2//!
3//! See [`PhysicsTransformPlugin`].
4
5mod transform;
6#[allow(unused_imports)]
7pub(crate) use transform::init_physics_transform;
8pub use transform::{Position, PreSolveDeltaPosition, PreSolveDeltaRotation, Rotation};
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    utils::{MIN_PAR_ITER_ENTITIES, ParallelQueryForEach},
20};
21use approx::AbsDiffEq;
22use bevy::{
23    ecs::{
24        change_detection::Tick, intern::Interned, schedule::ScheduleLabel, system::SystemChangeTick,
25    },
26    math::Affine3A,
27    prelude::*,
28    transform::systems::{mark_dirty_trees, propagate_parent_transforms, sync_simple_transforms},
29};
30
31/// Manages physics transforms and synchronizes them with [`Transform`].
32///
33/// # Syncing Between [`Position`]/[`Rotation`] and [`Transform`]
34///
35/// By default, each body's `Transform` will be updated when [`Position`] or [`Rotation`]
36/// change, and vice versa. This means that you can use any of these components to move
37/// or position bodies, and the changes be reflected in the other components.
38///
39/// You can configure what data is synchronized and how it is synchronized
40/// using the [`PhysicsTransformConfig`] resource.
41///
42/// # `Transform` Hierarchies
43///
44/// When synchronizing changes in [`Position`] or [`Rotation`] to `Transform`,
45/// the engine treats nested [rigid bodies](RigidBody) as a flat structure. This means that
46/// the bodies move independently of the parents, and moving the parent will not affect the child.
47///
48/// If you would like a child entity to be rigidly attached to its parent, you could use a [`FixedJoint`]
49/// or write your own system to handle hierarchies differently.
50pub struct PhysicsTransformPlugin {
51    schedule: Interned<dyn ScheduleLabel>,
52}
53
54impl PhysicsTransformPlugin {
55    /// Creates a [`PhysicsTransformPlugin`] with the schedule that is used for running the [`PhysicsSchedule`].
56    ///
57    /// The default schedule is `FixedPostUpdate`.
58    pub fn new(schedule: impl ScheduleLabel) -> Self {
59        Self {
60            schedule: schedule.intern(),
61        }
62    }
63}
64
65impl Default for PhysicsTransformPlugin {
66    fn default() -> Self {
67        Self::new(FixedPostUpdate)
68    }
69}
70
71impl Plugin for PhysicsTransformPlugin {
72    fn build(&self, app: &mut App) {
73        app.init_resource::<PhysicsTransformConfig>();
74
75        // In case `TransformPlugin` is not added
76        app.init_resource::<StaticTransformOptimizations>();
77
78        if app
79            .world()
80            .resource::<PhysicsTransformConfig>()
81            .position_to_transform
82        {
83            app.register_required_components::<Position, Transform>();
84            app.register_required_components::<Rotation, Transform>();
85        }
86
87        // Run transform propagation and transform-to-position synchronization before physics.
88        app.configure_sets(
89            self.schedule,
90            (
91                PhysicsTransformSystems::Propagate,
92                PhysicsTransformSystems::TransformToPosition,
93            )
94                .chain()
95                .in_set(PhysicsSystems::Prepare),
96        );
97        app.add_systems(
98            self.schedule,
99            (
100                mark_dirty_trees,
101                propagate_parent_transforms,
102                sync_simple_transforms,
103            )
104                .chain()
105                .in_set(PhysicsTransformSystems::Propagate)
106                .run_if(|config: Res<PhysicsTransformConfig>| config.propagate_before_physics),
107        );
108        app.add_systems(
109            self.schedule,
110            transform_to_position
111                .in_set(PhysicsTransformSystems::TransformToPosition)
112                .run_if(|config: Res<PhysicsTransformConfig>| config.transform_to_position),
113        );
114
115        // Run position-to-transform synchronization after physics.
116        app.configure_sets(
117            self.schedule,
118            PhysicsTransformSystems::PositionToTransform.in_set(PhysicsSystems::Writeback),
119        );
120        app.add_systems(
121            self.schedule,
122            position_to_transform
123                .in_set(PhysicsTransformSystems::PositionToTransform)
124                .run_if(|config: Res<PhysicsTransformConfig>| config.position_to_transform),
125        );
126    }
127}
128
129/// Configures how physics transforms are managed and synchronized with [`Transform`].
130#[derive(Resource, Reflect, Clone, Debug, PartialEq, Eq)]
131#[reflect(Resource)]
132pub struct PhysicsTransformConfig {
133    /// If true, [`Transform`] is propagated before stepping physics to ensure that
134    /// [`GlobalTransform`] is up-to-date.
135    ///
136    /// Default: `true`
137    pub propagate_before_physics: bool,
138    /// Updates [`Position`] and [`Rotation`] based on [`Transform`] changes
139    /// in [`PhysicsTransformSystems::TransformToPosition`],
140    ///
141    /// This allows using transforms for moving and positioning bodies,
142    ///
143    /// Default: `true`
144    pub transform_to_position: bool,
145    /// Updates [`Transform`] based on [`Position`] and [`Rotation`] changes
146    /// in [`PhysicsTransformSystems::PositionToTransform`],
147    ///
148    /// Default: `true`
149    pub position_to_transform: bool,
150    /// Updates [`Collider::scale()`] based on transform changes.
151    ///
152    /// This allows using transforms for scaling colliders.
153    ///
154    /// Default: `true`
155    pub transform_to_collider_scale: bool,
156}
157
158impl Default for PhysicsTransformConfig {
159    fn default() -> Self {
160        PhysicsTransformConfig {
161            propagate_before_physics: true,
162            position_to_transform: true,
163            transform_to_position: true,
164            transform_to_collider_scale: true,
165        }
166    }
167}
168
169/// System sets for managing physics transforms.
170#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
171pub enum PhysicsTransformSystems {
172    /// Propagates [`Transform`] before physics simulation.
173    Propagate,
174    /// Updates [`Position`] and [`Rotation`] based on [`Transform`] changes before physics simulation.
175    TransformToPosition,
176    /// Updates [`Transform`] based on [`Position`] and [`Rotation`] changes after physics simulation.
177    PositionToTransform,
178}
179
180/// A deprecated alias for [`PhysicsTransformSystems`].
181#[deprecated(since = "0.4.0", note = "Renamed to `PhysicsTransformSystems`")]
182pub type PhysicsTransformSet = PhysicsTransformSystems;
183
184/// Copies [`GlobalTransform`] changes to [`Position`] and [`Rotation`].
185/// This allows users to use transforms for moving and positioning bodies and colliders.
186///
187/// To account for hierarchies, transform propagation should be run before this system.
188#[allow(clippy::type_complexity)]
189pub fn transform_to_position(
190    mut query: Query<(Ref<GlobalTransform>, &mut Position, &mut Rotation)>,
191    length_unit: Res<PhysicsLengthUnit>,
192    last_physics_tick: Res<LastPhysicsTick>,
193    system_tick: SystemChangeTick,
194) {
195    // On the first tick, the last physics tick and system tick are both defaulted to 0,
196    // but to handle change detection correctly, the system tick should always be larger.
197    // So we use a minimum system tick of 1 here.
198    let this_run = if last_physics_tick.0.get() == 0 {
199        Tick::new(1)
200    } else {
201        system_tick.this_run()
202    };
203
204    // If the `GlobalTransform` translation and `Position` differ by less than 0.01 mm, we ignore the change.
205    let distance_tolerance = length_unit.real() * 1e-5;
206
207    let last_physics_tick = last_physics_tick.0;
208
209    query.par_for_each_mut(
210        MIN_PAR_ITER_ENTITIES,
211        |(global_transform, mut position, mut rotation)| {
212            let transform_changed = global_transform.is_added()
213                || is_changed_after_tick(global_transform, last_physics_tick, this_run);
214            if !transform_changed {
215                return;
216            }
217
218            let affine = global_transform.affine();
219
220            let position_changed = !position.is_added()
221                && is_changed_after_tick(
222                    Ref::from(position.reborrow()),
223                    last_physics_tick,
224                    this_run,
225                );
226            if !position_changed {
227                #[cfg(feature = "2d")]
228                let transform_translation = affine.translation.truncate().real();
229                #[cfg(feature = "3d")]
230                let transform_translation = Vec3::from(affine.translation).real();
231
232                if position.abs_diff_ne(&transform_translation, distance_tolerance) {
233                    position.0 = transform_translation;
234                }
235            }
236
237            let rotation_changed = !rotation.is_added()
238                && is_changed_after_tick(
239                    Ref::from(rotation.reborrow()),
240                    last_physics_tick,
241                    this_run,
242                );
243            if !rotation_changed {
244                let transform_rotation = rotation_from_affine(&affine);
245                // The rotations differ by more than the tolerance if the cosine of the angle
246                // between them is smaller than the cosine of the tolerance angle.
247                if cos_angle_between(*rotation, transform_rotation) < ROTATION_COS_TOLERANCE {
248                    *rotation = transform_rotation;
249                }
250            }
251        },
252    );
253}
254
255/// The cosine of the angle below which a difference between the `GlobalTransform` rotation
256/// and [`Rotation`] is ignored. This corresponds to an angle of 0.1 degrees.
257const ROTATION_COS_TOLERANCE: f32 = 0.999_998_5;
258
259/// Returns the cosine of the angle between two rotations.
260///
261/// This is a cheaper alternative to `Rotation::angle_between`,
262/// as it avoids inverse trigonometric functions.
263#[inline]
264fn cos_angle_between(a: Rotation, b: Rotation) -> f32 {
265    #[cfg(feature = "2d")]
266    {
267        a.cos * b.cos + a.sin * b.sin
268    }
269    #[cfg(feature = "3d")]
270    {
271        // The angle between two unit quaternions is `2 * acos(|dot|)`,
272        // and `cos(2 * acos(x)) == 2 * x^2 - 1`.
273        let dot = a.dot(b.0);
274        2.0 * dot * dot - 1.0
275    }
276}
277
278/// Extracts the [`Rotation`] from the affine transform of a `GlobalTransform`.
279///
280/// This is equivalent to `Rotation::from(global_transform.compute_transform().rotation)`,
281/// but avoids the full scale-rotation-translation decomposition, which is comparatively expensive.
282#[inline]
283fn rotation_from_affine(affine: &Affine3A) -> Rotation {
284    let mat = affine.matrix3;
285
286    let det_sign = mat.determinant().signum();
287
288    #[cfg(feature = "2d")]
289    {
290        let x_axis = Vec2::new(mat.x_axis.x, mat.x_axis.y) * det_sign;
291        let x_axis = x_axis.normalize_or(Vec2::X);
292        Rotation {
293            cos: x_axis.x,
294            sin: x_axis.y,
295        }
296    }
297    #[cfg(feature = "3d")]
298    {
299        let x_axis = (mat.x_axis * det_sign).normalize_or(Vec3A::X);
300        let y_axis = mat.y_axis.normalize_or(Vec3A::Y);
301        let z_axis = mat.z_axis.normalize_or(Vec3A::Z);
302        Rotation(Quat::from_mat3a(&Mat3A::from_cols(x_axis, y_axis, z_axis)))
303    }
304}
305
306/// Marker component indicating that the `position_to_transform` system should be applied
307/// to this entity.
308///
309/// By default, the `position_to_transform` system only runs for entities that have a
310/// [`RigidBody`] component
311#[derive(Component, Default)]
312pub struct ApplyPosToTransform;
313
314type PosToTransformComponents = (
315    &'static mut Transform,
316    &'static Position,
317    &'static Rotation,
318    Option<&'static ChildOf>,
319);
320
321type PosToTransformFilter = (
322    Or<(With<RigidBody>, With<ApplyPosToTransform>)>,
323    Or<(Changed<Position>, Changed<Rotation>)>,
324);
325
326type ParentComponents = (
327    &'static GlobalTransform,
328    Option<&'static Position>,
329    Option<&'static Rotation>,
330);
331
332/// Copies [`Position`] and [`Rotation`] changes to [`Transform`].
333/// This allows users and the engine to use these components for moving and positioning bodies.
334///
335/// Nested rigid bodies move independently of each other, so the [`Transform`]s of child entities are updated
336/// based on their own and their parent's [`Position`] and [`Rotation`].
337#[cfg(feature = "2d")]
338pub fn position_to_transform(
339    mut query: Query<PosToTransformComponents, PosToTransformFilter>,
340    parents: Query<ParentComponents, With<Children>>,
341) {
342    for (mut transform, pos, rot, parent) in &mut query {
343        if let Some(&ChildOf(parent)) = parent {
344            if let Ok((parent_transform, parent_pos, parent_rot)) = parents.get(parent) {
345                // Compute the global transform of the parent using its Position and Rotation
346                let parent_transform = parent_transform.compute_transform();
347                let parent_pos = parent_pos.map_or(parent_transform.translation, |pos| {
348                    pos.f32().extend(parent_transform.translation.z)
349                });
350                let parent_rot =
351                    parent_rot.map_or(parent_transform.rotation, |rot| Quat::from(*rot));
352                let parent_scale = parent_transform.scale;
353                let parent_transform = Transform::from_translation(parent_pos)
354                    .with_rotation(parent_rot)
355                    .with_scale(parent_scale);
356
357                // The new local transform of the child body,
358                // computed from the its global transform and its parents global transform
359                let new_transform = GlobalTransform::from(
360                    Transform::from_translation(
361                        pos.f32()
362                            .extend(parent_pos.z + transform.translation.z * parent_scale.z),
363                    )
364                    .with_rotation(Quat::from(*rot)),
365                )
366                .reparented_to(&GlobalTransform::from(parent_transform));
367
368                transform.translation = new_transform.translation;
369                transform.rotation = new_transform.rotation;
370            }
371        } else {
372            transform.translation = pos.f32().extend(transform.translation.z);
373            transform.rotation = Quat::from(*rot);
374        }
375    }
376}
377
378/// Copies [`Position`] and [`Rotation`] changes to [`Transform`].
379/// This allows users and the engine to use these components for moving and positioning bodies.
380///
381/// Nested rigid bodies move independently of each other, so the [`Transform`]s of child entities are updated
382/// based on their own and their parent's [`Position`] and [`Rotation`].
383#[cfg(feature = "3d")]
384pub fn position_to_transform(
385    mut query: Query<PosToTransformComponents, PosToTransformFilter>,
386    parents: Query<ParentComponents, With<Children>>,
387) {
388    for (mut transform, pos, rot, parent) in &mut query {
389        if let Some(&ChildOf(parent)) = parent {
390            if let Ok((parent_transform, parent_pos, parent_rot)) = parents.get(parent) {
391                // Compute the global transform of the parent using its Position and Rotation
392                let parent_transform = parent_transform.compute_transform();
393                let parent_pos = parent_pos.map_or(parent_transform.translation, |pos| pos.f32());
394                let parent_rot = parent_rot.map_or(parent_transform.rotation, |rot| rot.0);
395                let parent_scale = parent_transform.scale;
396                let parent_transform = Transform::from_translation(parent_pos)
397                    .with_rotation(parent_rot)
398                    .with_scale(parent_scale);
399
400                // The new local transform of the child body,
401                // computed from the its global transform and its parents global transform
402                let new_transform = GlobalTransform::from(
403                    Transform::from_translation(pos.f32()).with_rotation(rot.0),
404                )
405                .reparented_to(&GlobalTransform::from(parent_transform));
406
407                transform.translation = new_transform.translation;
408                transform.rotation = new_transform.rotation;
409            }
410        } else {
411            transform.translation = pos.f32();
412            transform.rotation = rot.0;
413        }
414    }
415}