bevy_transform_interpolation/
lib.rs

1//! # `bevy_transform_interpolation`
2//!
3//! A drop-in [`Transform`] interpolation solution for fixed timesteps for the [Bevy game engine](https://bevyengine.org).
4//!
5//! ## Features
6//!
7//! - Automatically smooth out movement in [`FixedPreUpdate`], [`FixedUpdate`], and [`FixedPostUpdate`].
8//! - Support for both [`Transform`] [interpolation](TransformInterpolationPlugin) and [extrapolation](TransformExtrapolationPlugin).
9//! - Granularly ease individual [`Transform`] properties to reduce unnecessary computation.
10//! - Apply easing to specific entities or to all entities.
11//! - Works out of the box with physics engines using fixed timesteps.
12//! - Optional [Hermite interpolation][`TransformHermiteEasingPlugin`] to produce more natural and accurate movement that considers velocity.
13//! - Extensible with custom easing backends.
14//!
15//! ## Getting Started
16//!
17//! First, add `bevy_transform_interpolation` as a dependency in your `Cargo.toml`:
18//!
19//! ```toml
20//! [dependencies]
21//! bevy_transform_interpolation = "0.4"
22//! ```
23//!
24//! To enable [`Transform`] interpolation, add the [`TransformInterpolationPlugin`] to your app:
25//!
26//! ```no_run
27//! use bevy::prelude::*;
28//! use bevy_transform_interpolation::prelude::*;
29//!
30//! fn main() {
31//!     App::new()
32//!         .add_plugins((DefaultPlugins, TransformInterpolationPlugin::default()))
33//!         // ...other plugins, resources, and systems
34//!         .run();
35//! }
36//! ```
37//!
38//! By default, interpolation is only performed for entities with the [`TransformInterpolation`] component:
39//!
40//! ```
41//! # use bevy::prelude::*;
42//! # use bevy_transform_interpolation::prelude::*;
43//! #
44//! fn setup(mut commands: Commands) {
45//!     // Interpolate the entire transform: translation, rotation, and scale.
46//!     commands.spawn((
47//!         Transform::default(),
48//!         TransformInterpolation,
49//!     ));
50//! }
51//! ```
52//!
53//! Now, any changes made to the [`Transform`] of the entity in [`FixedPreUpdate`], [`FixedUpdate`], or [`FixedPostUpdate`]
54//! will automatically be interpolated in between fixed timesteps.
55//!
56//! If you want *all* entities with a [`Transform`] to be interpolated by default, you can use
57//! [`TransformInterpolationPlugin::interpolate_all()`]:
58//!
59//! ```no_run
60//! # use bevy::prelude::*;
61//! # use bevy_transform_interpolation::prelude::*;
62//! #
63//! fn main() {
64//!    App::new()
65//!       .add_plugins(TransformInterpolationPlugin::interpolate_all())
66//! #     .add_plugins(bevy::time::TimePlugin::default())
67//!       // ...
68//!       .run();
69//! }
70//! ```
71//!
72//! See the documentation of the [`TransformInterpolationPlugin`] for a more detailed overview of what it can do.
73//!
74//! ## Advanced Usage
75//!
76//! For a lot of applications, the functionality shown in the [Getting Started](#getting-started) guide might be all you need!
77//! However, `bevy_transform_interpolation` has a lot more to offer:
78//!
79//! - Granularly ease individual properties of the transform with [`TranslationInterpolation`], [`RotationInterpolation`], and [`ScaleInterpolation`].
80//! - Opt out of transform easing for individual entities with [`NoTranslationEasing`], [`NoRotationEasing`], and [`NoScaleEasing`].
81//! - Use extrapolation instead of interpolation with the [`TransformExtrapolationPlugin`] and its related components.
82//! - Use Hermite interpolation for more natural and accurate movement with the [`TransformHermiteEasingPlugin`].
83//! - Implement custom easing backends for your specific needs.
84//!
85//! ## How Does It Work?
86//!
87//! Internally, `bevy_transform_interpolation` simply maintains components that store the `start` and `end` of the interpolation.
88//! For example, translation uses the following component for easing the movement:
89//!
90//! ```
91//! # use bevy::prelude::*;
92//! #
93//! pub struct TranslationEasingState {
94//!     pub start: Option<Vec3>,
95//!     pub end: Option<Vec3>,
96//! }
97//! ```
98//!
99//! The states are updated by the [`TransformInterpolationPlugin`] or [`TransformExtrapolationPlugin`]
100//! depending on whether the entity has [`TransformInterpolation`] or [`TransformExtrapolation`] components.
101//!
102//! If interpolation is used:
103//!
104//! - In [`FixedFirst`], `start` is set to the current [`Transform`].
105//! - In [`FixedLast`], `end` is set to the current [`Transform`].
106//!
107//! If extrapolation is used:
108//!
109//! - In [`FixedLast`], `start` is set to the current [`Transform`], and `end` is set to the [`Transform`] predicted based on velocity.
110//!
111//! At the start of the [`FixedFirst`] schedule, the states are reset to `None`. If the [`Transform`] is detected to have changed
112//! since the last easing run but *outside* of the fixed timestep schedules, the easing is also reset to `None` to prevent overwriting the change.
113//!
114//! The actual easing is performed in [`RunFixedMainLoop`], right after [`FixedMain`](bevy::app::FixedMain), before [`Update`].
115//! By default, linear interpolation (`lerp`) is used for translation and scale, and spherical linear interpolation (`slerp`)
116//! is used for rotation.
117//!
118//! However, thanks to the modular and flexible architecture, other easing methods can also be used.
119//! The [`TransformHermiteEasingPlugin`] provides an easing backend using Hermite interpolation,
120//! overwriting the linear interpolation for specific entities with the [`NonlinearTranslationEasing`]
121//! and [`NonlinearRotationEasing`] marker components. Custom easing solutions can be implemented using the same pattern.
122//!
123//! [`TransformHermiteEasingPlugin`]: crate::hermite::TransformHermiteEasingPlugin
124
125#![no_std]
126#![expect(clippy::type_complexity)]
127#![warn(missing_docs)]
128
129// Core interpolation and extrapolation plugins
130pub mod extrapolation;
131pub mod interpolation;
132
133// Easing backends
134// TODO: Catmull-Rom (like Hermite interpolation, but velocity is estimated from four points)
135pub mod hermite;
136
137/// The prelude.
138///
139/// This includes the most common types in this crate, re-exported for your convenience.
140pub mod prelude {
141    #[doc(inline)]
142    pub use crate::{
143        NoRotationEasing, NoScaleEasing, NoTransformEasing, NoTranslationEasing,
144        TransformEasingPlugin,
145        extrapolation::*,
146        hermite::{
147            RotationHermiteEasing, TransformHermiteEasing, TransformHermiteEasingPlugin,
148            TranslationHermiteEasing,
149        },
150        interpolation::*,
151    };
152}
153
154use core::marker::PhantomData;
155
156// For doc links.
157#[allow(unused_imports)]
158use extrapolation::*;
159#[allow(unused_imports)]
160use interpolation::*;
161
162use bevy::{
163    ecs::{change_detection::Tick, query::QueryData, system::SystemChangeTick},
164    prelude::*,
165};
166
167/// A plugin for applying easing to [`Transform`] changes, making movement in [`FixedUpdate`] appear smooth.
168///
169/// On its own, this plugin does *not* perform any automatic interpolation. It only performs easing
170/// between the `start` and `end` states of the [`TranslationEasingState`], [`RotationEasingState`], and [`ScaleEasingState`]
171/// components, and is responsible for resetting them at appropriate times.
172///
173/// To actually perform automatic easing, an easing backend that updates the `start` and `end` states must be used.
174/// The [`TransformInterpolationPlugin`] is provided for transform interpolation, but custom backends can also be implemented.
175#[derive(Debug, Default)]
176pub struct TransformEasingPlugin;
177
178impl Plugin for TransformEasingPlugin {
179    fn build(&self, app: &mut App) {
180        // Register easing components.
181        app.register_type::<(
182            TranslationEasingState,
183            RotationEasingState,
184            ScaleEasingState,
185            NoTranslationEasing,
186            NoRotationEasing,
187            NoScaleEasing,
188        )>();
189
190        app.init_resource::<LastEasingTick>();
191
192        // Reset easing states and update start values at the start of the fixed timestep.
193        app.configure_sets(
194            FixedFirst,
195            (
196                TransformEasingSystems::Reset,
197                TransformEasingSystems::UpdateStart,
198            )
199                .chain(),
200        );
201
202        // Update end values at the end of the fixed timestep.
203        app.configure_sets(FixedLast, TransformEasingSystems::UpdateEnd);
204
205        // Perform transform easing right after the fixed timestep, before `Update`.
206        app.configure_sets(
207            RunFixedMainLoop,
208            (
209                TransformEasingSystems::Ease,
210                TransformEasingSystems::UpdateEasingTick,
211            )
212                .chain()
213                .in_set(RunFixedMainLoopSystems::AfterFixedMainLoop),
214        );
215
216        // Reset easing states.
217        app.add_systems(
218            FixedFirst,
219            (
220                reset_translation_easing,
221                reset_rotation_easing,
222                reset_scale_easing,
223            )
224                .chain()
225                .in_set(TransformEasingSystems::Reset),
226        );
227
228        app.add_systems(
229            RunFixedMainLoop,
230            reset_easing_states_on_transform_change.before(TransformEasingSystems::Ease),
231        );
232
233        // Perform easing.
234        app.add_systems(
235            RunFixedMainLoop,
236            (ease_translation_lerp, ease_rotation_slerp, ease_scale_lerp)
237                .in_set(TransformEasingSystems::Ease),
238        );
239
240        // Update the last easing tick.
241        app.add_systems(
242            RunFixedMainLoop,
243            update_last_easing_tick.in_set(TransformEasingSystems::UpdateEasingTick),
244        );
245    }
246}
247
248/// System sets for easing transform.
249#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
250pub enum TransformEasingSystems {
251    /// Resets easing states to `None` at the start of the fixed timestep.
252    Reset,
253    /// Updates the `start` values for easing at the start of the fixed timestep.
254    UpdateStart,
255    /// Updates the `end` values for easing at the end of the fixed timestep.
256    UpdateEnd,
257    /// Eases the transform values in between the `start` and `end` states.
258    /// Runs in [`RunFixedMainLoop`], right after [`FixedMain`](bevy::app::FixedMain), before [`Update`].
259    Ease,
260    /// Updates [`LastEasingTick`], the last tick when easing was performed.
261    UpdateEasingTick,
262}
263
264/// A deprecated alias for [`TransformEasingSystems`].
265#[deprecated(since = "0.3.0", note = "Renamed to `TransformEasingSystems`")]
266pub type TransformEasingSet = TransformEasingSystems;
267
268/// A resource that stores the last tick when easing was performed.
269#[derive(Resource, Clone, Copy, Debug, Default, Deref, DerefMut)]
270pub struct LastEasingTick(Tick);
271
272/// Explicitly marks this entity as having no transform easing, disabling interpolation and/or extrapolation.
273#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
274#[reflect(Component, Debug, Default)]
275#[require(NoTranslationEasing, NoRotationEasing, NoScaleEasing)]
276pub struct NoTransformEasing;
277
278/// Explicitly marks this entity as having no translation easing, disabling interpolation and/or extrapolation.
279#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
280#[reflect(Component, Debug, Default)]
281pub struct NoTranslationEasing;
282
283/// Explicitly marks this entity as having no rotation easing, disabling interpolation and/or extrapolation.
284#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
285#[reflect(Component, Debug, Default)]
286pub struct NoRotationEasing;
287
288/// Explicitly marks this entity as having no scale easing, disabling interpolation and/or extrapolation.
289#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
290#[reflect(Component, Debug, Default)]
291pub struct NoScaleEasing;
292
293/// A marker component that indicates that the entity has non-linear translation easing,
294/// and linear easing should not be applied.
295#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
296#[reflect(Component, Debug, Default)]
297pub struct NonlinearTranslationEasing;
298
299/// A marker component that indicates that the entity has non-linear rotation easing,
300/// and linear easing should not be applied.
301#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
302#[reflect(Component, Debug, Default)]
303pub struct NonlinearRotationEasing;
304
305/// A [`QueryData`] type for specifying the components that store velocity for easing.
306/// Required for [`TransformExtrapolationPlugin`] and [`TransformHermiteEasingPlugin`].
307///
308/// [`TransformExtrapolationPlugin`]: crate::extrapolation::TransformExtrapolationPlugin
309/// [`TransformHermiteEasingPlugin`]: crate::hermite::TransformHermiteEasingPlugin
310///
311/// # Example
312///
313/// ```
314/// use bevy::{ecs::query::QueryData, prelude::*};
315/// use bevy_transform_interpolation::VelocitySource;
316///
317/// // Velocity components
318///
319/// #[derive(Component)]
320/// struct LinearVelocity(Vec3);
321///
322/// #[derive(Component)]
323/// struct PreviousLinearVelocity(Vec3);
324///
325/// #[derive(Component)]
326/// struct AngularVelocity(Vec3);
327///
328/// #[derive(Component)]
329/// struct PreviousAngularVelocity(Vec3);
330///
331/// // Velocity source for easing that uses linear velocity
332/// #[derive(QueryData)]
333/// struct LinVelSource;
334///
335/// impl VelocitySource for LinVelSource {
336///     type Previous = PreviousLinearVelocity;
337///     type Current = LinearVelocity;
338///
339///     fn previous(previous: &Self::Previous) -> Vec3 {
340///         previous.0
341///     }
342///
343///     fn current(current: &Self::Current) -> Vec3 {
344///         current.0
345///     }
346/// }
347///
348/// // Velocity source for easing that uses angular velocity
349/// #[derive(QueryData)]
350/// struct AngVelSource;
351///
352/// impl VelocitySource for AngVelSource {
353///     type Previous = PreviousAngularVelocity;
354///     type Current = AngularVelocity;
355///
356///     fn previous(previous: &Self::Previous) -> Vec3 {
357///         previous.0
358///     }
359///
360///     fn current(current: &Self::Current) -> Vec3 {
361///         current.0
362///     }
363/// }
364/// ```
365///
366/// Some forms of easing such as extrapolation may not require the previous velocity.
367/// In such cases, the `Previous` component can be set to `()`, and `previous` can simply return `Vec3::ZERO`.
368pub trait VelocitySource: QueryData + Send + Sync + 'static {
369    /// The component that stores the previous velocity.
370    ///
371    /// This is not required for all easing backends, such as extrapolation.
372    /// In such cases, this can be set to `()`.
373    type Previous: Component;
374
375    /// The component that stores the current velocity.
376    type Current: Component;
377
378    /// Returns the previous velocity.
379    ///
380    /// This is not required for all easing backends, such as extrapolation.
381    /// In such cases, this can return `Vec3::ZERO`.
382    fn previous(start: &Self::Previous) -> Vec3;
383
384    /// Returns the current velocity.
385    fn current(end: &Self::Current) -> Vec3;
386}
387
388trait VelocitySourceItem<V>
389where
390    V: VelocitySource,
391{
392    fn previous(start: &V::Previous) -> Vec3;
393    fn current(end: &V::Current) -> Vec3;
394}
395
396impl<V: VelocitySource> VelocitySourceItem<V> for V::Item<'_, '_> {
397    fn previous(start: &V::Previous) -> Vec3 {
398        V::previous(start)
399    }
400
401    fn current(end: &V::Current) -> Vec3 {
402        V::current(end)
403    }
404}
405
406// Required so that `()` can be used as a "null" velocity source despite it not being a component itself.
407// This can be useful if you only want to use Hermite interpolation for rotation, for example.
408//
409// This must be public, because `VelocitySource::Start` and `VelocitySource::End` are public interfaces,
410// but you can't actually create this component since the stored value is private and there are no constructors.
411#[derive(Component)]
412#[doc(hidden)]
413pub struct DummyComponent(PhantomData<()>);
414
415impl VelocitySource for () {
416    type Previous = DummyComponent;
417    type Current = DummyComponent;
418
419    fn previous(_: &Self::Previous) -> Vec3 {
420        Vec3::ZERO
421    }
422
423    fn current(_: &Self::Current) -> Vec3 {
424        Vec3::ZERO
425    }
426}
427
428/// Stores the start and end states used for interpolating the translation of an entity.
429/// The change in translation is smoothed from `start` to `end` in between [`FixedUpdate`] runs.
430///
431/// On its own, this component is not updated automatically. Enable an easing backend
432/// such as the [`TransformInterpolationPlugin`] to perform automatic interpolation.
433#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
434#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
435#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
436#[reflect(Component, Debug, Default)]
437pub struct TranslationEasingState {
438    /// The start translation for the interpolation.
439    pub start: Option<Vec3>,
440    /// The end translation for the interpolation.
441    pub end: Option<Vec3>,
442}
443
444/// Stores the start and end states used for interpolating the rotation of an entity.
445/// The change in rotation is smoothed from `start` to `end` in between [`FixedUpdate`] runs.
446///
447/// On its own, this component is not updated automatically. Enable an easing backend
448/// such as the [`TransformInterpolationPlugin`] to perform automatic interpolation.
449#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
450#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
451#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
452#[reflect(Component, Debug, Default)]
453pub struct RotationEasingState {
454    /// The start rotation for the interpolation.
455    pub start: Option<Quat>,
456    /// The end rotation for the interpolation.
457    pub end: Option<Quat>,
458}
459
460/// Stores the start and end states used for interpolating the scale of an entity.
461/// The change in scale is smoothed from `start` to `end` in between [`FixedUpdate`] runs.
462///
463/// On its own, this component is not updated automatically. Enable an easing backend
464/// such as the [`TransformInterpolationPlugin`] to perform automatic interpolation.
465#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
466#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
467#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
468#[reflect(Component, Debug, Default)]
469pub struct ScaleEasingState {
470    /// The start scale for the interpolation.
471    pub start: Option<Vec3>,
472    /// The end scale for the interpolation.
473    pub end: Option<Vec3>,
474}
475
476fn update_last_easing_tick(
477    mut last_easing_tick: ResMut<LastEasingTick>,
478    system_change_tick: SystemChangeTick,
479) {
480    *last_easing_tick = LastEasingTick(system_change_tick.this_run());
481}
482
483/// Resets the easing states to `None` when [`Transform`] is modified outside of the fixed timestep schedules
484/// or interpolation logic. This makes it possible to "teleport" entities in schedules like [`Update`].
485#[allow(clippy::type_complexity, private_interfaces)]
486pub fn reset_easing_states_on_transform_change(
487    mut query: Query<
488        (
489            Ref<Transform>,
490            Option<&mut TranslationEasingState>,
491            Option<&mut RotationEasingState>,
492            Option<&mut ScaleEasingState>,
493        ),
494        (
495            Changed<Transform>,
496            Or<(
497                With<TranslationEasingState>,
498                With<RotationEasingState>,
499                With<ScaleEasingState>,
500            )>,
501        ),
502    >,
503    last_easing_tick: Res<LastEasingTick>,
504    system_change_tick: SystemChangeTick,
505) {
506    let this_run = system_change_tick.this_run();
507
508    query.par_iter_mut().for_each(
509        |(transform, translation_easing, rotation_easing, scale_easing)| {
510            let last_changed = transform.last_changed();
511            let is_user_change = last_changed.is_newer_than(last_easing_tick.0, this_run);
512
513            if !is_user_change {
514                return;
515            }
516
517            if let Some(mut translation_easing) = translation_easing
518                && let (Some(start), Some(end)) = (translation_easing.start, translation_easing.end)
519                && transform.translation != start
520                && transform.translation != end
521            {
522                translation_easing.start = None;
523                translation_easing.end = None;
524            }
525            if let Some(mut rotation_easing) = rotation_easing
526                && let (Some(start), Some(end)) = (rotation_easing.start, rotation_easing.end)
527                && transform.rotation != start
528                && transform.rotation != end
529            {
530                rotation_easing.start = None;
531                rotation_easing.end = None;
532            }
533            if let Some(mut scale_easing) = scale_easing
534                && let (Some(start), Some(end)) = (scale_easing.start, scale_easing.end)
535                && transform.scale != start
536                && transform.scale != end
537            {
538                scale_easing.start = None;
539                scale_easing.end = None;
540            }
541        },
542    );
543}
544
545/// Resets the `start` and `end` states for translation interpolation.
546fn reset_translation_easing(mut query: Query<&mut TranslationEasingState>) {
547    for mut easing in &mut query {
548        easing.start = None;
549        easing.end = None;
550    }
551}
552
553/// Resets the `start` and `end` states for rotation interpolation.
554fn reset_rotation_easing(mut query: Query<&mut RotationEasingState>) {
555    for mut easing in &mut query {
556        easing.start = None;
557        easing.end = None;
558    }
559}
560
561/// Resets the `start` and `end` states for scale interpolation.
562fn reset_scale_easing(mut query: Query<&mut ScaleEasingState>) {
563    for mut easing in &mut query {
564        easing.start = None;
565        easing.end = None;
566    }
567}
568
569/// Eases the translations of entities with linear interpolation.
570fn ease_translation_lerp(
571    mut query: Query<
572        (&mut Transform, &TranslationEasingState),
573        (
574            Without<NonlinearTranslationEasing>,
575            Without<NoTranslationEasing>,
576        ),
577    >,
578    time: Res<Time<Fixed>>,
579) {
580    let overstep = time.overstep_fraction();
581
582    query.iter_mut().for_each(|(mut transform, interpolation)| {
583        if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
584            transform.translation = start.lerp(end, overstep);
585        }
586    });
587}
588
589/// Eases the rotations of entities with spherical linear interpolation.
590fn ease_rotation_slerp(
591    mut query: Query<
592        (&mut Transform, &RotationEasingState),
593        (Without<NonlinearRotationEasing>, Without<NoRotationEasing>),
594    >,
595    time: Res<Time<Fixed>>,
596) {
597    let overstep = time.overstep_fraction();
598
599    query
600        .par_iter_mut()
601        .for_each(|(mut transform, interpolation)| {
602            if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
603                // Note: `slerp` will always take the shortest path, but when the two rotations are more than
604                // 180 degrees apart, this can cause visual artifacts as the rotation "flips" to the other side.
605                transform.rotation = start.slerp(end, overstep);
606            }
607        });
608}
609
610/// Eases the scales of entities with linear interpolation.
611fn ease_scale_lerp(
612    mut query: Query<(&mut Transform, &ScaleEasingState), Without<NoScaleEasing>>,
613    time: Res<Time<Fixed>>,
614) {
615    let overstep = time.overstep_fraction();
616
617    query.iter_mut().for_each(|(mut transform, interpolation)| {
618        if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
619            transform.scale = start.lerp(end, overstep);
620        }
621    });
622}