From a38561535532568e4a924121eeee335e83424504 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 12 Apr 2025 22:12:45 +0300 Subject: [PATCH 1/7] Make transform changes outside fixed time step not interrupt easing --- src/lib.rs | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1827ab8..972373f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -224,7 +224,8 @@ impl Plugin for TransformEasingPlugin { app.add_systems( RunFixedMainLoop, - reset_easing_states_on_transform_change.before(TransformEasingSet::Ease), + update_easing_states_on_transform_change + .in_set(RunFixedMainLoopSystem::BeforeFixedMainLoop), ); // Perform easing. @@ -473,10 +474,14 @@ fn update_last_easing_tick( *last_easing_tick = LastEasingTick(system_change_tick.this_run()); } -/// Resets the easing states to `None` when [`Transform`] is modified outside of the fixed timestep schedules -/// or interpolation logic. This makes it possible to "teleport" entities in schedules like [`Update`]. +/// Updates easing states when [`Transform`] is modified outside of the fixed timestep schedules +/// or interpolation logic. +/// +/// The `start` and `end` states are updated such that the current interpolated transform +/// matches the new transform. This makes it possible to "teleport" entities in schedules +/// such as [`Update`] without interrupting the easing. #[allow(clippy::type_complexity, private_interfaces)] -pub fn reset_easing_states_on_transform_change( +pub fn update_easing_states_on_transform_change( mut query: Query< ( Ref, @@ -495,8 +500,10 @@ pub fn reset_easing_states_on_transform_change( >, last_easing_tick: Res, system_change_tick: SystemChangeTick, + time: Res>, ) { let this_run = system_change_tick.this_run(); + let overstep = time.overstep_fraction(); query.par_iter_mut().for_each( |(transform, translation_easing, rotation_easing, scale_easing)| { @@ -507,28 +514,39 @@ pub fn reset_easing_states_on_transform_change( return; } + // Transform the `start` and `end` states of each transform property + // such that the current eased transform matches `transform`. if let Some(mut translation_easing) = translation_easing { if let (Some(start), Some(end)) = (translation_easing.start, translation_easing.end) { if transform.translation != start && transform.translation != end { - translation_easing.start = None; - translation_easing.end = None; + let old = start.lerp(end, overstep); + let difference = transform.translation - old; + translation_easing.start = Some(start + difference); + translation_easing.end = Some(end + difference); } } } if let Some(mut rotation_easing) = rotation_easing { if let (Some(start), Some(end)) = (rotation_easing.start, rotation_easing.end) { if transform.rotation != start && transform.rotation != end { - rotation_easing.start = None; - rotation_easing.end = None; + // TODO: Do we need to consider alternative easing modes? + let old = start.slerp(end, overstep); + let difference = old.inverse() * transform.rotation; + rotation_easing.start = Some(difference * start); + // We need to normalize here to avoid error accumulating over time. + // It seems like it's enough to only normalize the end state however. + rotation_easing.end = Some((difference * end).normalize()); } } } if let Some(mut scale_easing) = scale_easing { if let (Some(start), Some(end)) = (scale_easing.start, scale_easing.end) { if transform.scale != start && transform.scale != end { - scale_easing.start = None; - scale_easing.end = None; + let old = start.lerp(end, overstep); + let difference = transform.scale - old; + scale_easing.start = Some(start + difference); + scale_easing.end = Some(end + difference); } } } From a7b754a4c61eb05377d78994ea312f7ccd883301 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 12 Apr 2025 22:13:11 +0300 Subject: [PATCH 2/7] Add `ResetInterpolation` command --- src/commands.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 +++ 2 files changed, 45 insertions(+) create mode 100644 src/commands.rs diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..0a3de1f --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,42 @@ +//! Helper commands for operations on interpolated entities. + +use bevy::{ + ecs::{entity::Entity, system::Command, world::World}, + reflect::prelude::*, +}; + +use crate::{RotationEasingState, ScaleEasingState, TranslationEasingState}; + +/// A [`Command`] that resets the interpolation state of an entity. +/// +/// This disables interpolation for the remainder of the current fixed time step, +/// allowing you to freely set the [`Transform`](bevy::transform::components::Transform) +/// of the entity without any interpolation being applied. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] +#[reflect(Debug, PartialEq)] +pub struct ResetInterpolation(pub Entity); + +impl Command for ResetInterpolation { + fn apply(self, world: &mut World) { + let Ok(mut entity_mut) = world.get_entity_mut(self.0) else { + return; + }; + + if let Some(mut translation_easing) = entity_mut.get_mut::() { + translation_easing.start = None; + translation_easing.end = None; + } + + if let Some(mut rotation_easing) = entity_mut.get_mut::() { + rotation_easing.start = None; + rotation_easing.end = None; + } + + if let Some(mut scale_easing) = entity_mut.get_mut::() { + scale_easing.start = None; + scale_easing.end = None; + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 972373f..d4acce9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,6 +135,9 @@ pub mod interpolation; // TODO: Catmull-Rom (like Hermite interpolation, but velocity is estimated from four points) pub mod hermite; +// Helper commands +pub mod commands; + /// The prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. From c34269b5d9869bc92ed1c8de82004f30400f1408 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 12 Apr 2025 22:46:18 +0300 Subject: [PATCH 3/7] Update docs --- README.md | 3 +-- src/extrapolation.rs | 26 +++++++++++++++++++++++--- src/interpolation.rs | 23 +++++++++++++++++++++++ src/lib.rs | 3 +-- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9d11d45..6615323 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,7 @@ If extrapolation is used: - In `FixedLast`, `start` is set to the current `Transform`, and `end` is set to the `Transform` predicted based on velocity. -At the start of the `FixedFirst` schedule, the states are reset to `None`. If the `Transform` is detected to have changed -since the last easing run but *outside* of the fixed timestep schedules, the easing is also reset to `None` to prevent overwriting the change. +At the start of the `FixedFirst` schedule, the states are reset to `None`. The actual easing is performed in `RunFixedMainLoop`, right after `FixedMain`, before `Update`. By default, linear interpolation (`lerp`) is used for translation and scale, and spherical linear interpolation (`slerp`) diff --git a/src/extrapolation.rs b/src/extrapolation.rs index 2b74173..7ed520f 100644 --- a/src/extrapolation.rs +++ b/src/extrapolation.rs @@ -200,9 +200,6 @@ use bevy::prelude::*; /// When extrapolation is enabled for all entities by default, you can still opt out of it for individual entities /// by adding the [`NoTransformEasing`] component, or the individual [`NoTranslationEasing`] and [`NoRotationEasing`] components. /// -/// Note that changing [`Transform`] manually in any schedule that *doesn't* use a fixed timestep is also supported, -/// but it is equivalent to teleporting, and disables extrapolation for the entity for the remainder of that fixed timestep. -/// /// [`QueryData`]: bevy::ecs::query::QueryData /// [`TransformExtrapolationPlugin::extrapolate_all()`]: TransformExtrapolationPlugin::extrapolate_all /// [`extrapolate_translation_all`]: TransformExtrapolationPlugin::extrapolate_translation_all @@ -211,6 +208,29 @@ use bevy::prelude::*; /// [`NoTranslationEasing`]: crate::NoTranslationEasing /// [`NoRotationEasing`]: crate::NoRotationEasing /// +/// ## Changing [`Transform`] Outside of Fixed Timesteps +/// +/// Changing the [`Transform`] of an extrapolated entity in any schedule that *doesn't* use +/// a fixed timestep is also supported, but comes with some special behavior. +/// +/// [`Transform`] changes made outside of the fixed time step are applied immediately, +/// effectively teleporting the entity to the new position. However, the easing is not interrupted, +/// meaning that the remaining extrapolation will still be applied, but relative to the new transform. +/// +/// To better visualize this, consider a classic trick in games where an infinite world is simulated +/// by teleporting the player to the other side of the game area when they reach the edge of the world. +/// This teleportation is done in the [`Update`] schedule as soon as the [`Transform`] reaches the edge. +/// +/// To make the effect smooth, we want to set the visual [`Transform`] to the new position immediately, +/// but to still complete the remainder of the extrapolation to prevent any stuttering. +/// In `bevy_transform_interpolation`, this works *by default*. Just set the [`Transform`], +/// and the entity will be teleported without interrupting the extrapolation. +/// +/// In other instances, it may be desirable to instead interrupt the extrapolation and teleport the entity +/// without any easing. This can be done using the [`ResetInterpolation`] command and then setting the [`Transform`]. +/// +/// [`ResetInterpolation`]: crate::commands::ResetInterpolation +/// /// # Alternatives /// /// For many applications, the stutter caused by mispredictions in extrapolation may be undesirable. diff --git a/src/interpolation.rs b/src/interpolation.rs index 27bc0d5..fe674d3 100644 --- a/src/interpolation.rs +++ b/src/interpolation.rs @@ -97,6 +97,29 @@ use bevy::prelude::*; /// [`interpolate_rotation_all`]: TransformInterpolationPlugin::interpolate_rotation_all /// [`interpolate_scale_all`]: TransformInterpolationPlugin::interpolate_scale_all /// +/// ## Changing [`Transform`] Outside of Fixed Timesteps +/// +/// Changing the [`Transform`] of an interpolated entity in any schedule that *doesn't* use +/// a fixed timestep is also supported, but comes with some special behavior. +/// +/// [`Transform`] changes made outside of the fixed time step are applied immediately, +/// effectively teleporting the entity to the new position. However, the easing is not interrupted, +/// meaning that the remaining interpolation will still be applied, but relative to the new transform. +/// +/// To better visualize this, consider a classic trick in games where an infinite world is simulated +/// by teleporting the player to the other side of the game area when they reach the edge of the world. +/// This teleportation is done in the [`Update`] schedule as soon as the [`Transform`] reaches the edge. +/// +/// To make the effect smooth, we want to set the visual [`Transform`] to the new position immediately, +/// but to still complete the remainder of the interpolation to prevent any stuttering. +/// In `bevy_transform_interpolation`, this works *by default*. Just set the [`Transform`], +/// and the entity will be teleported without interrupting the interpolation. +/// +/// In other instances, it may be desirable to instead interrupt the interpolation and teleport the entity +/// without any easing. This can be done using the [`ResetInterpolation`] command and then setting the [`Transform`]. +/// +/// [`ResetInterpolation`]: crate::commands::ResetInterpolation +/// /// # Alternatives /// /// For games where low latency is crucial for gameplay, such as in some first-person shooters diff --git a/src/lib.rs b/src/lib.rs index d4acce9..1ee6372 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,8 +108,7 @@ //! //! - In [`FixedLast`], `start` is set to the current [`Transform`], and `end` is set to the [`Transform`] predicted based on velocity. //! -//! At the start of the [`FixedFirst`] schedule, the states are reset to `None`. If the [`Transform`] is detected to have changed -//! since the last easing run but *outside* of the fixed timestep schedules, the easing is also reset to `None` to prevent overwriting the change. +//! At the start of the [`FixedFirst`] schedule, the states are reset to `None`. //! //! The actual easing is performed in [`RunFixedMainLoop`], right after [`FixedMain`](bevy::app::FixedMain), before [`Update`]. //! By default, linear interpolation (`lerp`) is used for translation and scale, and spherical linear interpolation (`slerp`) From e853de32ef7812c37fa92497a44ff80a40b25db4 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 12 Apr 2025 22:47:49 +0300 Subject: [PATCH 4/7] Rename `ResetInterpolation` to `ResetEasing` --- src/commands.rs | 10 +++++----- src/extrapolation.rs | 4 ++-- src/interpolation.rs | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 0a3de1f..4ad05ec 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -7,18 +7,18 @@ use bevy::{ use crate::{RotationEasingState, ScaleEasingState, TranslationEasingState}; -/// A [`Command`] that resets the interpolation state of an entity. +/// A [`Command`] that resets the easing states of an entity. /// -/// This disables interpolation for the remainder of the current fixed time step, +/// This disables easing for the remainder of the current fixed time step, /// allowing you to freely set the [`Transform`](bevy::transform::components::Transform) -/// of the entity without any interpolation being applied. +/// of the entity without any easing being applied. #[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] #[reflect(Debug, PartialEq)] -pub struct ResetInterpolation(pub Entity); +pub struct ResetEasing(pub Entity); -impl Command for ResetInterpolation { +impl Command for ResetEasing { fn apply(self, world: &mut World) { let Ok(mut entity_mut) = world.get_entity_mut(self.0) else { return; diff --git a/src/extrapolation.rs b/src/extrapolation.rs index 7ed520f..1a698ad 100644 --- a/src/extrapolation.rs +++ b/src/extrapolation.rs @@ -227,9 +227,9 @@ use bevy::prelude::*; /// and the entity will be teleported without interrupting the extrapolation. /// /// In other instances, it may be desirable to instead interrupt the extrapolation and teleport the entity -/// without any easing. This can be done using the [`ResetInterpolation`] command and then setting the [`Transform`]. +/// without any easing. This can be done using the [`ResetEasing`] command and then setting the [`Transform`]. /// -/// [`ResetInterpolation`]: crate::commands::ResetInterpolation +/// [`ResetEasing`]: crate::commands::ResetEasing /// /// # Alternatives /// diff --git a/src/interpolation.rs b/src/interpolation.rs index fe674d3..2065ee5 100644 --- a/src/interpolation.rs +++ b/src/interpolation.rs @@ -116,9 +116,9 @@ use bevy::prelude::*; /// and the entity will be teleported without interrupting the interpolation. /// /// In other instances, it may be desirable to instead interrupt the interpolation and teleport the entity -/// without any easing. This can be done using the [`ResetInterpolation`] command and then setting the [`Transform`]. +/// without any easing. This can be done using the [`ResetEasing`] command and then setting the [`Transform`]. /// -/// [`ResetInterpolation`]: crate::commands::ResetInterpolation +/// [`ResetEasing`]: crate::commands::ResetEasing /// /// # Alternatives /// From 5bdd9721682eab29ab4b1b4efb3363354438c3cc Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sun, 13 Apr 2025 00:15:45 +0300 Subject: [PATCH 5/7] Add `ResetEasing` to prelude and fix doc comment --- src/commands.rs | 2 +- src/lib.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands.rs b/src/commands.rs index 4ad05ec..fb3d014 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,4 +1,4 @@ -//! Helper commands for operations on interpolated entities. +//! Helper commands for operations on interpolated or extrapolated entities. use bevy::{ ecs::{entity::Entity, system::Command, world::World}, diff --git a/src/lib.rs b/src/lib.rs index 1ee6372..9bfdccf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,6 +143,7 @@ pub mod commands; pub mod prelude { #[doc(inline)] pub use crate::{ + commands::ResetEasing, extrapolation::*, hermite::{ RotationHermiteEasing, TransformHermiteEasing, TransformHermiteEasingPlugin, From 3f0231943b758b7eb97ffebb3e02117b90f0703c Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sun, 13 Apr 2025 17:47:41 +0300 Subject: [PATCH 6/7] Fix normalization --- src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9bfdccf..249a6f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -536,9 +536,7 @@ pub fn update_easing_states_on_transform_change( // TODO: Do we need to consider alternative easing modes? let old = start.slerp(end, overstep); let difference = old.inverse() * transform.rotation; - rotation_easing.start = Some(difference * start); - // We need to normalize here to avoid error accumulating over time. - // It seems like it's enough to only normalize the end state however. + rotation_easing.start = Some((difference * start).normalize()); rotation_easing.end = Some((difference * end).normalize()); } } From e9982e3c66ff56d15edc9baa39ac7aec3a55a6bb Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sun, 13 Apr 2025 17:48:03 +0300 Subject: [PATCH 7/7] Remove outdated doc comment --- src/interpolation.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/interpolation.rs b/src/interpolation.rs index 2065ee5..5fd43bf 100644 --- a/src/interpolation.rs +++ b/src/interpolation.rs @@ -90,9 +90,6 @@ use bevy::prelude::*; /// by adding the [`NoTransformEasing`] component, or the individual [`NoTranslationEasing`], [`NoRotationEasing`], /// and [`NoScaleEasing`] components. /// -/// Note that changing [`Transform`] manually in any schedule that *doesn't* use a fixed timestep is also supported, -/// but it is equivalent to teleporting, and disables interpolation for the entity for the remainder of that fixed timestep. -/// /// [`interpolate_translation_all`]: TransformInterpolationPlugin::interpolate_translation_all /// [`interpolate_rotation_all`]: TransformInterpolationPlugin::interpolate_rotation_all /// [`interpolate_scale_all`]: TransformInterpolationPlugin::interpolate_scale_all