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();