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.1"
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::needless_doctest_main)]
127#![expect(clippy::type_complexity)]
128#![warn(missing_docs)]
129
130// Core interpolation and extrapolation plugins
131pub mod extrapolation;
132pub mod interpolation;
133
134// Easing backends
135// TODO: Catmull-Rom (like Hermite interpolation, but velocity is estimated from four points)
136pub mod hermite;
137
138/// The prelude.
139///
140/// This includes the most common types in this crate, re-exported for your convenience.
141pub mod prelude {
142    #[doc(inline)]
143    pub use crate::{
144        extrapolation::*,
145        hermite::{
146            RotationHermiteEasing, TransformHermiteEasing, TransformHermiteEasingPlugin,
147            TranslationHermiteEasing,
148        },
149        interpolation::*,
150        NoRotationEasing, NoScaleEasing, NoTransformEasing, NoTranslationEasing,
151        TransformEasingPlugin,
152    };
153}
154
155use core::marker::PhantomData;
156
157// For doc links.
158#[allow(unused_imports)]
159use extrapolation::*;
160#[allow(unused_imports)]
161use interpolation::*;
162
163use bevy::{
164    ecs::{component::Tick, query::QueryData, system::SystemChangeTick},
165    prelude::*,
166};
167
168/// A plugin for applying easing to [`Transform`] changes, making movement in [`FixedUpdate`] appear smooth.
169///
170/// On its own, this plugin does *not* perform any automatic interpolation. It only performs easing
171/// between the `start` and `end` states of the [`TranslationEasingState`], [`RotationEasingState`], and [`ScaleEasingState`]
172/// components, and is responsible for resetting them at appropriate times.
173///
174/// To actually perform automatic easing, an easing backend that updates the `start` and `end` states must be used.
175/// The [`TransformInterpolationPlugin`] is provided for transform interpolation, but custom backends can also be implemented.
176#[derive(Debug, Default)]
177pub struct TransformEasingPlugin;
178
179impl Plugin for TransformEasingPlugin {
180    fn build(&self, app: &mut App) {
181        // Register easing components.
182        app.register_type::<(
183            TranslationEasingState,
184            RotationEasingState,
185            ScaleEasingState,
186            NoTranslationEasing,
187            NoRotationEasing,
188            NoScaleEasing,
189        )>();
190
191        app.init_resource::<LastEasingTick>();
192
193        // Reset easing states and update start values at the start of the fixed timestep.
194        app.configure_sets(
195            FixedFirst,
196            (
197                TransformEasingSystems::Reset,
198                TransformEasingSystems::UpdateStart,
199            )
200                .chain(),
201        );
202
203        // Update end values at the end of the fixed timestep.
204        app.configure_sets(FixedLast, TransformEasingSystems::UpdateEnd);
205
206        // Perform transform easing right after the fixed timestep, before `Update`.
207        app.configure_sets(
208            RunFixedMainLoop,
209            (
210                TransformEasingSystems::Ease,
211                TransformEasingSystems::UpdateEasingTick,
212            )
213                .chain()
214                .in_set(RunFixedMainLoopSystems::AfterFixedMainLoop),
215        );
216
217        // Reset easing states.
218        app.add_systems(
219            FixedFirst,
220            (
221                reset_translation_easing,
222                reset_rotation_easing,
223                reset_scale_easing,
224            )
225                .chain()
226                .in_set(TransformEasingSystems::Reset),
227        );
228
229        app.add_systems(
230            RunFixedMainLoop,
231            reset_easing_states_on_transform_change.before(TransformEasingSystems::Ease),
232        );
233
234        // Perform easing.
235        app.add_systems(
236            RunFixedMainLoop,
237            (ease_translation_lerp, ease_rotation_slerp, ease_scale_lerp)
238                .in_set(TransformEasingSystems::Ease),
239        );
240
241        // Update the last easing tick.
242        app.add_systems(
243            RunFixedMainLoop,
244            update_last_easing_tick.in_set(TransformEasingSystems::UpdateEasingTick),
245        );
246    }
247}
248
249/// System sets for easing transform.
250#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
251pub enum TransformEasingSystems {
252    /// Resets easing states to `None` at the start of the fixed timestep.
253    Reset,
254    /// Updates the `start` values for easing at the start of the fixed timestep.
255    UpdateStart,
256    /// Updates the `end` values for easing at the end of the fixed timestep.
257    UpdateEnd,
258    /// Eases the transform values in between the `start` and `end` states.
259    /// Runs in [`RunFixedMainLoop`], right after [`FixedMain`](bevy::app::FixedMain), before [`Update`].
260    Ease,
261    /// Updates [`LastEasingTick`], the last tick when easing was performed.
262    UpdateEasingTick,
263}
264
265/// A deprecated alias for [`TransformEasingSystems`].
266#[deprecated(since = "0.3.0", note = "Renamed to `TransformEasingSystems`")]
267pub type TransformEasingSet = TransformEasingSystems;
268
269/// A resource that stores the last tick when easing was performed.
270#[derive(Resource, Clone, Copy, Debug, Default, Deref, DerefMut)]
271pub struct LastEasingTick(Tick);
272
273/// Explicitly marks this entity as having no transform easing, disabling interpolation and/or extrapolation.
274#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
275#[reflect(Component, Debug, Default)]
276#[require(NoTranslationEasing, NoRotationEasing, NoScaleEasing)]
277pub struct NoTransformEasing;
278
279/// Explicitly marks this entity as having no translation easing, disabling interpolation and/or extrapolation.
280#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
281#[reflect(Component, Debug, Default)]
282pub struct NoTranslationEasing;
283
284/// Explicitly marks this entity as having no rotation easing, disabling interpolation and/or extrapolation.
285#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
286#[reflect(Component, Debug, Default)]
287pub struct NoRotationEasing;
288
289/// Explicitly marks this entity as having no scale easing, disabling interpolation and/or extrapolation.
290#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
291#[reflect(Component, Debug, Default)]
292pub struct NoScaleEasing;
293
294/// A marker component that indicates that the entity has non-linear translation easing,
295/// and linear easing should not be applied.
296#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
297#[reflect(Component, Debug, Default)]
298pub struct NonlinearTranslationEasing;
299
300/// A marker component that indicates that the entity has non-linear rotation easing,
301/// and linear easing should not be applied.
302#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
303#[reflect(Component, Debug, Default)]
304pub struct NonlinearRotationEasing;
305
306/// A [`QueryData`] type for specifying the components that store velocity for easing.
307/// Required for [`TransformExtrapolationPlugin`] and [`TransformHermiteEasingPlugin`].
308///
309/// [`TransformExtrapolationPlugin`]: crate::extrapolation::TransformExtrapolationPlugin
310/// [`TransformHermiteEasingPlugin`]: crate::hermite::TransformHermiteEasingPlugin
311///
312/// # Example
313///
314/// ```
315/// use bevy::{ecs::query::QueryData, prelude::*};
316/// use bevy_transform_interpolation::VelocitySource;
317///
318/// // Velocity components
319///
320/// #[derive(Component)]
321/// struct LinearVelocity(Vec3);
322///
323/// #[derive(Component)]
324/// struct PreviousLinearVelocity(Vec3);
325///
326/// #[derive(Component)]
327/// struct AngularVelocity(Vec3);
328///
329/// #[derive(Component)]
330/// struct PreviousAngularVelocity(Vec3);
331///
332/// // Velocity source for easing that uses linear velocity
333/// #[derive(QueryData)]
334/// struct LinVelSource;
335///
336/// impl VelocitySource for LinVelSource {
337///     type Previous = PreviousLinearVelocity;
338///     type Current = LinearVelocity;
339///
340///     fn previous(previous: &Self::Previous) -> Vec3 {
341///         previous.0
342///     }
343///
344///     fn current(current: &Self::Current) -> Vec3 {
345///         current.0
346///     }
347/// }
348///
349/// // Velocity source for easing that uses angular velocity
350/// #[derive(QueryData)]
351/// struct AngVelSource;
352///
353/// impl VelocitySource for AngVelSource {
354///     type Previous = PreviousAngularVelocity;
355///     type Current = AngularVelocity;
356///
357///     fn previous(previous: &Self::Previous) -> Vec3 {
358///         previous.0
359///     }
360///
361///     fn current(current: &Self::Current) -> Vec3 {
362///         current.0
363///     }
364/// }
365/// ```
366///
367/// Some forms of easing such as extrapolation may not require the previous velocity.
368/// In such cases, the `Previous` component can be set to `()`, and `previous` can simply return `Vec3::ZERO`.
369pub trait VelocitySource: QueryData + Send + Sync + 'static {
370    /// The component that stores the previous velocity.
371    ///
372    /// This is not required for all easing backends, such as extrapolation.
373    /// In such cases, this can be set to `()`.
374    type Previous: Component;
375
376    /// The component that stores the current velocity.
377    type Current: Component;
378
379    /// Returns the previous velocity.
380    ///
381    /// This is not required for all easing backends, such as extrapolation.
382    /// In such cases, this can return `Vec3::ZERO`.
383    fn previous(start: &Self::Previous) -> Vec3;
384
385    /// Returns the current velocity.
386    fn current(end: &Self::Current) -> Vec3;
387}
388
389trait VelocitySourceItem<V>
390where
391    V: VelocitySource,
392{
393    fn previous(start: &V::Previous) -> Vec3;
394    fn current(end: &V::Current) -> Vec3;
395}
396
397impl<V: VelocitySource> VelocitySourceItem<V> for V::Item<'_, '_> {
398    fn previous(start: &V::Previous) -> Vec3 {
399        V::previous(start)
400    }
401
402    fn current(end: &V::Current) -> Vec3 {
403        V::current(end)
404    }
405}
406
407// Required so that `()` can be used as a "null" velocity source despite it not being a component itself.
408// This can be useful if you only want to use Hermite interpolation for rotation, for example.
409//
410// This must be public, because `VelocitySource::Start` and `VelocitySource::End` are public interfaces,
411// but you can't actually create this component since the stored value is private and there are no constructors.
412#[derive(Component)]
413#[doc(hidden)]
414pub struct DummyComponent(PhantomData<()>);
415
416impl VelocitySource for () {
417    type Previous = DummyComponent;
418    type Current = DummyComponent;
419
420    fn previous(_: &Self::Previous) -> Vec3 {
421        Vec3::ZERO
422    }
423
424    fn current(_: &Self::Current) -> Vec3 {
425        Vec3::ZERO
426    }
427}
428
429/// Stores the start and end states used for interpolating the translation of an entity.
430/// The change in translation is smoothed from `start` to `end` in between [`FixedUpdate`] runs.
431///
432/// On its own, this component is not updated automatically. Enable an easing backend
433/// such as the [`TransformInterpolationPlugin`] to perform automatic interpolation.
434#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
435#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
436#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
437#[reflect(Component, Debug, Default)]
438pub struct TranslationEasingState {
439    /// The start translation for the interpolation.
440    pub start: Option<Vec3>,
441    /// The end translation for the interpolation.
442    pub end: Option<Vec3>,
443}
444
445/// Stores the start and end states used for interpolating the rotation of an entity.
446/// The change in rotation is smoothed from `start` to `end` in between [`FixedUpdate`] runs.
447///
448/// On its own, this component is not updated automatically. Enable an easing backend
449/// such as the [`TransformInterpolationPlugin`] to perform automatic interpolation.
450#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
451#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
452#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
453#[reflect(Component, Debug, Default)]
454pub struct RotationEasingState {
455    /// The start rotation for the interpolation.
456    pub start: Option<Quat>,
457    /// The end rotation for the interpolation.
458    pub end: Option<Quat>,
459}
460
461/// Stores the start and end states used for interpolating the scale of an entity.
462/// The change in scale is smoothed from `start` to `end` in between [`FixedUpdate`] runs.
463///
464/// On its own, this component is not updated automatically. Enable an easing backend
465/// such as the [`TransformInterpolationPlugin`] to perform automatic interpolation.
466#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
467#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
468#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
469#[reflect(Component, Debug, Default)]
470pub struct ScaleEasingState {
471    /// The start scale for the interpolation.
472    pub start: Option<Vec3>,
473    /// The end scale for the interpolation.
474    pub end: Option<Vec3>,
475}
476
477fn update_last_easing_tick(
478    mut last_easing_tick: ResMut<LastEasingTick>,
479    system_change_tick: SystemChangeTick,
480) {
481    *last_easing_tick = LastEasingTick(system_change_tick.this_run());
482}
483
484/// Resets the easing states to `None` when [`Transform`] is modified outside of the fixed timestep schedules
485/// or interpolation logic. This makes it possible to "teleport" entities in schedules like [`Update`].
486#[allow(clippy::type_complexity, private_interfaces)]
487pub fn reset_easing_states_on_transform_change(
488    mut query: Query<
489        (
490            Ref<Transform>,
491            Option<&mut TranslationEasingState>,
492            Option<&mut RotationEasingState>,
493            Option<&mut ScaleEasingState>,
494        ),
495        (
496            Changed<Transform>,
497            Or<(
498                With<TranslationEasingState>,
499                With<RotationEasingState>,
500                With<ScaleEasingState>,
501            )>,
502        ),
503    >,
504    last_easing_tick: Res<LastEasingTick>,
505    system_change_tick: SystemChangeTick,
506) {
507    let this_run = system_change_tick.this_run();
508
509    query.par_iter_mut().for_each(
510        |(transform, translation_easing, rotation_easing, scale_easing)| {
511            let last_changed = transform.last_changed();
512            let is_user_change = last_changed.is_newer_than(last_easing_tick.0, this_run);
513
514            if !is_user_change {
515                return;
516            }
517
518            if let Some(mut translation_easing) = translation_easing {
519                if let (Some(start), Some(end)) = (translation_easing.start, translation_easing.end)
520                {
521                    if transform.translation != start && transform.translation != end {
522                        translation_easing.start = None;
523                        translation_easing.end = None;
524                    }
525                }
526            }
527            if let Some(mut rotation_easing) = rotation_easing {
528                if let (Some(start), Some(end)) = (rotation_easing.start, rotation_easing.end) {
529                    if transform.rotation != start && transform.rotation != end {
530                        rotation_easing.start = None;
531                        rotation_easing.end = None;
532                    }
533                }
534            }
535            if let Some(mut scale_easing) = scale_easing {
536                if let (Some(start), Some(end)) = (scale_easing.start, scale_easing.end) {
537                    if transform.scale != start && transform.scale != end {
538                        scale_easing.start = None;
539                        scale_easing.end = None;
540                    }
541                }
542            }
543        },
544    );
545}
546
547/// Resets the `start` and `end` states for translation interpolation.
548fn reset_translation_easing(mut query: Query<&mut TranslationEasingState>) {
549    for mut easing in &mut query {
550        easing.start = None;
551        easing.end = None;
552    }
553}
554
555/// Resets the `start` and `end` states for rotation interpolation.
556fn reset_rotation_easing(mut query: Query<&mut RotationEasingState>) {
557    for mut easing in &mut query {
558        easing.start = None;
559        easing.end = None;
560    }
561}
562
563/// Resets the `start` and `end` states for scale interpolation.
564fn reset_scale_easing(mut query: Query<&mut ScaleEasingState>) {
565    for mut easing in &mut query {
566        easing.start = None;
567        easing.end = None;
568    }
569}
570
571/// Eases the translations of entities with linear interpolation.
572fn ease_translation_lerp(
573    mut query: Query<
574        (&mut Transform, &TranslationEasingState),
575        (
576            Without<NonlinearTranslationEasing>,
577            Without<NoTranslationEasing>,
578        ),
579    >,
580    time: Res<Time<Fixed>>,
581) {
582    let overstep = time.overstep_fraction();
583
584    query.iter_mut().for_each(|(mut transform, interpolation)| {
585        if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
586            transform.translation = start.lerp(end, overstep);
587        }
588    });
589}
590
591/// Eases the rotations of entities with spherical linear interpolation.
592fn ease_rotation_slerp(
593    mut query: Query<
594        (&mut Transform, &RotationEasingState),
595        (Without<NonlinearRotationEasing>, Without<NoRotationEasing>),
596    >,
597    time: Res<Time<Fixed>>,
598) {
599    let overstep = time.overstep_fraction();
600
601    query
602        .par_iter_mut()
603        .for_each(|(mut transform, interpolation)| {
604            if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
605                // Note: `slerp` will always take the shortest path, but when the two rotations are more than
606                // 180 degrees apart, this can cause visual artifacts as the rotation "flips" to the other side.
607                transform.rotation = start.slerp(end, overstep);
608            }
609        });
610}
611
612/// Eases the scales of entities with linear interpolation.
613fn ease_scale_lerp(
614    mut query: Query<(&mut Transform, &ScaleEasingState), Without<NoScaleEasing>>,
615    time: Res<Time<Fixed>>,
616) {
617    let overstep = time.overstep_fraction();
618
619    query.iter_mut().for_each(|(mut transform, interpolation)| {
620        if let (Some(start), Some(end)) = (interpolation.start, interpolation.end) {
621            transform.scale = start.lerp(end, overstep);
622        }
623    });
624}