diff --git a/doc/classes/Animation.xml b/doc/classes/Animation.xml
index 2a88e7081890..e04320c30d54 100644
--- a/doc/classes/Animation.xml
+++ b/doc/classes/Animation.xml
@@ -651,7 +651,7 @@
Update at the keyframes.
- Same as linear interpolation, but also interpolates from the current value (i.e. dynamically at runtime) if the first key isn't at 0 seconds.
+ Same as [constant UPDATE_CONTINUOUS] but works as a flag to capture the value of the current object and perform interpolation in some methods. See also [method AnimationMixer.capture] and [method AnimationPlayer.play_with_capture].
At both ends of the animation, the animation will stop playing.
diff --git a/doc/classes/AnimationMixer.xml b/doc/classes/AnimationMixer.xml
index 31308c4e20e4..d17e9377dac1 100644
--- a/doc/classes/AnimationMixer.xml
+++ b/doc/classes/AnimationMixer.xml
@@ -36,6 +36,18 @@
Manually advance the animations by the specified time (in seconds).
+
+
+
+
+
+
+
+ If the animation track specified by [param name] has an option [constant Animation.UPDATE_CAPTURE], stores current values of the objects indicated by the track path as a cache. If there is already a captured cache, the old cache is discarded.
+ After this it will interpolate with current animation blending result during the playback process for the time specified by [param duration], working like a crossfade.
+ You can specify [param trans_type] as the curve for the interpolation. For better results, it may be appropriate to specify [constant Tween.TRANS_LINEAR] for cases where the first key of the track begins with a non-zero value or where the key value does not change, and [constant Tween.TRANS_QUAD] for cases where the key value changes linearly.
+
+
diff --git a/doc/classes/AnimationPlayer.xml b/doc/classes/AnimationPlayer.xml
index dda0187e8b2e..233d31a101dd 100644
--- a/doc/classes/AnimationPlayer.xml
+++ b/doc/classes/AnimationPlayer.xml
@@ -110,6 +110,26 @@
This method is a shorthand for [method play] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], so see its description for more information.
+
+
+
+
+
+
+
+
+
+
+ See [method AnimationMixer.capture]. It is almost the same as the following:
+ [codeblock]
+ capture(name, duration, trans_type, ease_type)
+ play(name, custom_blend, custom_speed, from_end)
+ [/codeblock]
+ If name is blank, it specifies [member assigned_animation].
+ If [param duration] is a negative value, the duration is set to the interval between the current position and the first key, when [param from_end] is [code]true[/code], uses the interval between the current position and the last key instead.
+ [b]Note:[/b] The [param duration] takes [member speed_scale] into account, but [param custom_speed] does not, because the capture cache is interpolated with the blend result and the result may contain multiple animations.
+
+
@@ -125,7 +145,7 @@
Seeks the animation to the [param seconds] point in time (in seconds). If [param update] is [code]true[/code], the animation updates too, otherwise it updates at process time. Events between the current frame and [param seconds] are skipped.
- If [param update_only] is true, the method / audio / animation playback tracks will not be processed.
+ If [param update_only] is [code]true[/code], the method / audio / animation playback tracks will not be processed.
[b]Note:[/b] Seeking to the end of the animation doesn't emit [signal AnimationMixer.animation_finished]. If you want to skip animation and emit the signal, use [method AnimationMixer.advance].
diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp
index 2adb3695a716..78abdbdd3e74 100644
--- a/scene/animation/animation_mixer.cpp
+++ b/scene/animation/animation_mixer.cpp
@@ -552,6 +552,7 @@ void AnimationMixer::_clear_caches() {
}
track_cache.clear();
cache_valid = false;
+ capture_cache.clear();
emit_signal(SNAME("caches_cleared"));
}
@@ -915,6 +916,7 @@ bool AnimationMixer::_update_caches() {
void AnimationMixer::_process_animation(double p_delta, bool p_update_only) {
_blend_init();
if (_blend_pre_process(p_delta, track_count, track_map)) {
+ _blend_capture(p_delta);
_blend_calc_total_weight();
_blend_process(p_delta, p_update_only);
_blend_apply();
@@ -1013,6 +1015,43 @@ void AnimationMixer::_blend_post_process() {
//
}
+void AnimationMixer::_blend_capture(double p_delta) {
+ blend_capture(p_delta);
+}
+
+void AnimationMixer::blend_capture(double p_delta) {
+ if (capture_cache.animation.is_null()) {
+ return;
+ }
+
+ capture_cache.remain -= p_delta * capture_cache.step;
+ if (capture_cache.remain <= 0.0) {
+ capture_cache.clear();
+ return;
+ }
+
+ real_t weight = Tween::run_equation(capture_cache.trans_type, capture_cache.ease_type, capture_cache.remain, 0.0, 1.0, 1.0);
+
+ // Blend with other animations.
+ real_t inv = 1.0 - weight;
+ for (AnimationInstance &ai : animation_instances) {
+ ai.playback_info.weight *= inv;
+ }
+
+ // Build capture animation instance.
+ AnimationData ad;
+ ad.animation = capture_cache.animation;
+
+ PlaybackInfo pi;
+ pi.weight = weight;
+
+ AnimationInstance ai;
+ ai.animation_data = ad;
+ ai.playback_info = pi;
+
+ animation_instances.push_back(ai);
+}
+
void AnimationMixer::_blend_calc_total_weight() {
for (const AnimationInstance &ai : animation_instances) {
Ref a = ai.animation_data.animation;
@@ -1848,6 +1887,10 @@ Vector3 AnimationMixer::get_root_motion_scale_accumulator() const {
return root_motion_scale_accumulator;
}
+/* -------------------------------------------- */
+/* -- Reset on save --------------------------- */
+/* -------------------------------------------- */
+
void AnimationMixer::set_reset_on_save_enabled(bool p_enabled) {
reset_on_save = p_enabled;
}
@@ -2011,6 +2054,50 @@ Ref AnimationMixer::apply_reset(bool p_user_initiated) {
}
#endif // TOOLS_ENABLED
+/* -------------------------------------------- */
+/* -- Capture feature ------------------------- */
+/* -------------------------------------------- */
+
+void AnimationMixer::capture(const StringName &p_name, double p_duration, Tween::TransitionType p_trans_type, Tween::EaseType p_ease_type) {
+ ERR_FAIL_COND(!active);
+ ERR_FAIL_COND(!has_animation(p_name));
+ ERR_FAIL_COND(Math::is_zero_approx(p_duration));
+ Ref reference_animation = get_animation(p_name);
+
+ if (!cache_valid) {
+ _update_caches(); // Need to retrieve object id.
+ }
+
+ capture_cache.remain = 1.0;
+ capture_cache.step = 1.0 / p_duration;
+ capture_cache.trans_type = p_trans_type;
+ capture_cache.ease_type = p_ease_type;
+ capture_cache.animation.instantiate();
+
+ bool is_valid = false;
+ for (int i = 0; i < reference_animation->get_track_count(); i++) {
+ if (!reference_animation->track_is_enabled(i)) {
+ continue;
+ }
+ if (reference_animation->track_get_type(i) == Animation::TYPE_VALUE && reference_animation->value_track_get_update_mode(i) == Animation::UPDATE_CAPTURE) {
+ TrackCacheValue *t = static_cast(track_cache[reference_animation->track_get_type_hash(i)]);
+ Object *t_obj = ObjectDB::get_instance(t->object_id);
+ if (t_obj) {
+ Variant value = t_obj->get_indexed(t->subpath);
+ int inserted_idx = capture_cache.animation->add_track(Animation::TYPE_VALUE);
+ capture_cache.animation->track_set_path(inserted_idx, reference_animation->track_get_path(i));
+ capture_cache.animation->track_insert_key(inserted_idx, 0, value);
+ capture_cache.animation->value_track_set_update_mode(inserted_idx, Animation::UPDATE_CONTINUOUS);
+ capture_cache.animation->track_set_interpolation_type(inserted_idx, Animation::INTERPOLATION_LINEAR);
+ is_valid = true;
+ }
+ }
+ }
+ if (!is_valid) {
+ capture_cache.clear();
+ }
+}
+
/* -------------------------------------------- */
/* -- General functions ----------------------- */
/* -------------------------------------------- */
@@ -2118,9 +2205,14 @@ void AnimationMixer::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "active"), "set_active", "is_active");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "deterministic"), "set_deterministic", "is_deterministic");
+ /* ---- Reset on save ---- */
ClassDB::bind_method(D_METHOD("set_reset_on_save_enabled", "enabled"), &AnimationMixer::set_reset_on_save_enabled);
ClassDB::bind_method(D_METHOD("is_reset_on_save_enabled"), &AnimationMixer::is_reset_on_save_enabled);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "reset_on_save", PROPERTY_HINT_NONE, ""), "set_reset_on_save_enabled", "is_reset_on_save_enabled");
+
+ /* ---- Capture feature ---- */
+ ClassDB::bind_method(D_METHOD("capture", "name", "duration", "trans_type", "ease_type"), &AnimationMixer::capture, DEFVAL(Tween::TRANS_LINEAR), DEFVAL(Tween::EASE_IN));
+
ADD_SIGNAL(MethodInfo("mixer_updated")); // For updating dummy player.
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "root_node"), "set_root_node", "get_root_node");
diff --git a/scene/animation/animation_mixer.h b/scene/animation/animation_mixer.h
index 098c2c3b86b4..d618d3833247 100644
--- a/scene/animation/animation_mixer.h
+++ b/scene/animation/animation_mixer.h
@@ -31,6 +31,7 @@
#ifndef ANIMATION_MIXER_H
#define ANIMATION_MIXER_H
+#include "scene/animation/tween.h"
#include "scene/main/node.h"
#include "scene/resources/animation.h"
#include "scene/resources/animation_library.h"
@@ -334,12 +335,34 @@ class AnimationMixer : public Node {
void _blend_init();
virtual bool _blend_pre_process(double p_delta, int p_track_count, const HashMap &p_track_map);
+ virtual void _blend_capture(double p_delta);
void _blend_calc_total_weight(); // For undeterministic blending.
void _blend_process(double p_delta, bool p_update_only = false);
void _blend_apply();
virtual void _blend_post_process();
void _call_object(ObjectID p_object_id, const StringName &p_method, const Vector &p_params, bool p_deferred);
+ /* ---- Capture feature ---- */
+ struct CaptureCache {
+ Ref animation;
+ double remain = 0.0;
+ double step = 0.0;
+ Tween::TransitionType trans_type = Tween::TRANS_LINEAR;
+ Tween::EaseType ease_type = Tween::EASE_IN;
+
+ void clear() {
+ animation.unref();
+ remain = 0.0;
+ step = 0.0;
+ }
+
+ CaptureCache() {}
+ ~CaptureCache() {
+ clear();
+ }
+ } capture_cache;
+ void blend_capture(double p_delta); // To blend capture track with all other animations.
+
#ifndef DISABLE_DEPRECATED
virtual Variant _post_process_key_value_bind_compat_86687(const Ref &p_anim, int p_track, Variant p_value, Object *p_object, int p_object_idx = -1);
@@ -400,9 +423,12 @@ class AnimationMixer : public Node {
virtual void advance(double p_time);
virtual void clear_caches(); ///< must be called by hand if an animation was modified after added
+ /* ---- Capture feature ---- */
+ void capture(const StringName &p_name, double p_duration, Tween::TransitionType p_trans_type = Tween::TRANS_LINEAR, Tween::EaseType p_ease_type = Tween::EASE_IN);
+
+ /* ---- Reset on save ---- */
void set_reset_on_save_enabled(bool p_enabled);
bool is_reset_on_save_enabled() const;
-
bool can_apply_reset() const;
void _build_backup_track_cache();
Ref make_backup();
diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp
index 36f1cd01f497..a57ce39336f2 100644
--- a/scene/animation/animation_player.cpp
+++ b/scene/animation/animation_player.cpp
@@ -310,6 +310,10 @@ bool AnimationPlayer::_blend_pre_process(double p_delta, int p_track_count, cons
return true;
}
+void AnimationPlayer::_blend_capture(double p_delta) {
+ blend_capture(p_delta * Math::abs(speed_scale));
+}
+
void AnimationPlayer::_blend_post_process() {
if (end_reached) {
// If the method track changes current animation, the animation is not finished.
@@ -366,13 +370,73 @@ void AnimationPlayer::play_backwards(const StringName &p_name, double p_custom_b
play(p_name, p_custom_blend, -1, true);
}
+void AnimationPlayer::play_with_capture(const StringName &p_name, double p_duration, double p_custom_blend, float p_custom_scale, bool p_from_end, Tween::TransitionType p_trans_type, Tween::EaseType p_ease_type) {
+ StringName name = p_name;
+ if (name == StringName()) {
+ name = playback.assigned;
+ }
+
+ if (signbit(p_duration)) {
+ double max_dur = 0;
+ Ref anim = get_animation(name);
+ if (anim.is_valid()) {
+ double current_pos = playback.current.pos;
+ if (playback.assigned != name) {
+ current_pos = p_from_end ? anim->get_length() : 0;
+ }
+ for (int i = 0; i < anim->get_track_count(); i++) {
+ if (anim->track_get_type(i) != Animation::TYPE_VALUE) {
+ continue;
+ }
+ if (anim->value_track_get_update_mode(i) != Animation::UPDATE_CAPTURE) {
+ continue;
+ }
+ if (anim->track_get_key_count(i) == 0) {
+ continue;
+ }
+ max_dur = MAX(max_dur, p_from_end ? current_pos - anim->track_get_key_time(i, anim->track_get_key_count(i) - 1) : anim->track_get_key_time(i, 0) - current_pos);
+ }
+ }
+ p_duration = max_dur;
+ }
+
+ capture(name, p_duration, p_trans_type, p_ease_type);
+ play(name, p_custom_blend, p_custom_scale, p_from_end);
+}
+
void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) {
StringName name = p_name;
- if (String(name) == "") {
+ if (name == StringName()) {
name = playback.assigned;
}
+#ifdef TOOLS_ENABLED
+ if (!Engine::get_singleton()->is_editor_hint()) {
+ bool warn_enabled = false;
+ if (capture_cache.animation.is_null()) {
+ Ref anim = get_animation(name);
+ if (anim.is_valid()) {
+ for (int i = 0; i < anim->get_track_count(); i++) {
+ if (anim->track_get_type(i) != Animation::TYPE_VALUE) {
+ continue;
+ }
+ if (anim->value_track_get_update_mode(i) != Animation::UPDATE_CAPTURE) {
+ continue;
+ }
+ if (anim->track_get_key_count(i) == 0) {
+ continue;
+ }
+ warn_enabled = true;
+ }
+ }
+ }
+ if (warn_enabled) {
+ WARN_PRINT_ONCE_ED("Capture track found. If you want to interpolate animation with captured frame, you can use play_with_capture() instead of play().");
+ }
+ }
+#endif
+
ERR_FAIL_COND_MSG(!animation_set.has(name), vformat("Animation not found: %s.", name));
Playback &c = playback;
@@ -417,7 +481,7 @@ void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, floa
}
if (get_current_animation() != p_name) {
- _clear_caches();
+ _clear_playing_caches();
}
c.current.from = &animation_set[name];
@@ -751,6 +815,7 @@ void AnimationPlayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("play", "name", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play, DEFVAL(""), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false));
ClassDB::bind_method(D_METHOD("play_backwards", "name", "custom_blend"), &AnimationPlayer::play_backwards, DEFVAL(""), DEFVAL(-1));
+ ClassDB::bind_method(D_METHOD("play_with_capture", "name", "duration", "custom_blend", "custom_speed", "from_end", "trans_type", "ease_type"), &AnimationPlayer::play_with_capture, DEFVAL(-1.0), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false), DEFVAL(Tween::TRANS_LINEAR), DEFVAL(Tween::EASE_IN));
ClassDB::bind_method(D_METHOD("pause"), &AnimationPlayer::pause);
ClassDB::bind_method(D_METHOD("stop", "keep_state"), &AnimationPlayer::stop, DEFVAL(false));
ClassDB::bind_method(D_METHOD("is_playing"), &AnimationPlayer::is_playing);
diff --git a/scene/animation/animation_player.h b/scene/animation/animation_player.h
index 16bca45d4b7b..24a5351fb735 100644
--- a/scene/animation/animation_player.h
+++ b/scene/animation/animation_player.h
@@ -128,6 +128,7 @@ class AnimationPlayer : public AnimationMixer {
// Make animation instances.
virtual bool _blend_pre_process(double p_delta, int p_track_count, const HashMap &p_track_map) override;
+ virtual void _blend_capture(double p_delta) override;
virtual void _blend_post_process() override;
virtual void _animation_removed(const StringName &p_name, const StringName &p_library) override;
@@ -157,6 +158,7 @@ class AnimationPlayer : public AnimationMixer {
void play(const StringName &p_name = StringName(), double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false);
void play_backwards(const StringName &p_name = StringName(), double p_custom_blend = -1);
+ void play_with_capture(const StringName &p_name, double p_duration = -1.0, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false, Tween::TransitionType p_trans_type = Tween::TRANS_LINEAR, Tween::EaseType p_ease_type = Tween::EASE_IN);
void queue(const StringName &p_name);
Vector get_queue();
void clear_queue();