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