Skip to content

Commit

Permalink
Merge pull request #53819 from TokageItLab/re-implement-ping-pong
Browse files Browse the repository at this point in the history
Reimplement ping-pong animation and reverse playback
  • Loading branch information
akien-mga authored Nov 9, 2021
2 parents efea312 + 953a7bc commit 06a33e5
Show file tree
Hide file tree
Showing 29 changed files with 1,041 additions and 440 deletions.
13 changes: 13 additions & 0 deletions core/math/math_funcs.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,19 @@ class Math {
return is_zero_approx(range) ? min : value - (range * Math::floor((value - min) / range));
}

static _ALWAYS_INLINE_ float fract(float value) {
return value - floor(value);
}
static _ALWAYS_INLINE_ double fract(double value) {
return value - floor(value);
}
static _ALWAYS_INLINE_ float pingpong(float value, float length) {
return (length != 0.0f) ? abs(fract((value - length) / (length * 2.0f)) * length * 2.0f - length) : 0.0f;
}
static _ALWAYS_INLINE_ double pingpong(double value, double length) {
return (length != 0.0) ? abs(fract((value - length) / (length * 2.0)) * length * 2.0 - length) : 0.0;
}

// double only, as these functions are mainly used by the editor and not performance-critical,
static double ease(double p_x, double p_c);
static int step_decimals(double p_step);
Expand Down
5 changes: 5 additions & 0 deletions core/variant/variant_utility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,10 @@ struct VariantUtilityFunctions {
return Math::wrapf(value, min, max);
}

static inline double pingpong(double value, double length) {
return Math::pingpong(value, length);
}

static inline Variant max(const Variant **p_args, int p_argcount, Callable::CallError &r_error) {
if (p_argcount < 2) {
r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS;
Expand Down Expand Up @@ -1226,6 +1230,7 @@ void Variant::_register_variant_utility_functions() {
FUNCBINDR(clampf, sarray("value", "min", "max"), Variant::UTILITY_FUNC_TYPE_MATH);

FUNCBINDR(nearest_po2, sarray("value"), Variant::UTILITY_FUNC_TYPE_MATH);
FUNCBINDR(pingpong, sarray("value", "length"), Variant::UTILITY_FUNC_TYPE_MATH);

// Random

Expand Down
20 changes: 20 additions & 0 deletions doc/classes/@GlobalScope.xml
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,26 @@
[b]Warning:[/b] Due to the way it is implemented, this function returns [code]0[/code] rather than [code]1[/code] for non-positive values of [code]value[/code] (in reality, 1 is the smallest integer power of 2).
</description>
</method>
<method name="pingpong">
<return type="float" />
<argument index="0" name="value" type="float" />
<argument index="1" name="length" type="float" />
<description>
Returns the [code]value[/code] wrapped between [code]0[/code] and the [code]length[/code]. If the limit is reached, the next value the function returned is decreased to the [code]0[/code] side or increased to the [code]length[/code] side (like a triangle wave). If [code]length[/code] is less than zero, it becomes positive.
[codeblock]
pingpong(-3.0, 3.0) # Returns 3
pingpong(-2.0, 3.0) # Returns 2
pingpong(-1.0, 3.0) # Returns 1
pingpong(0.0, 3.0) # Returns 0
pingpong(1.0, 3.0) # Returns 1
pingpong(2.0, 3.0) # Returns 2
pingpong(3.0, 3.0) # Returns 3
pingpong(4.0, 3.0) # Returns 2
pingpong(5.0, 3.0) # Returns 1
pingpong(6.0, 3.0) # Returns 0
[/codeblock]
</description>
</method>
<method name="posmod">
<return type="int" />
<argument index="0" name="x" type="int" />
Expand Down
13 changes: 11 additions & 2 deletions doc/classes/Animation.xml
Original file line number Diff line number Diff line change
Expand Up @@ -551,8 +551,8 @@
The total length of the animation (in seconds).
[b]Note:[/b] Length is not delimited by the last key, as this one may be before or after the end to ensure correct interpolation and looping.
</member>
<member name="loop" type="bool" setter="set_loop" getter="has_loop" default="false">
A flag indicating that the animation must loop. This is used for correct interpolation of animation cycles, and for hinting the player that it must restart the animation.
<member name="loop_mode" type="int" setter="set_loop_mode" getter="get_loop_mode" enum="Animation.LoopMode" default="0">
Determines the behavior of both ends of the animation timeline during animation playback. This is used for correct interpolation of animation cycles, and for hinting the player that it must restart the animation.
</member>
<member name="step" type="float" setter="set_step" getter="get_step" default="0.1">
The animation step value.
Expand Down Expand Up @@ -610,5 +610,14 @@
<constant name="UPDATE_CAPTURE" value="3" enum="UpdateMode">
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.
</constant>
<constant name="LOOP_NONE" value="0" enum="LoopMode">
At both ends of the animation, the animation will stop playing.
</constant>
<constant name="LOOP_LINEAR" value="1" enum="LoopMode">
At both ends of the animation, the animation will be repeated without changing the playback direction.
</constant>
<constant name="LOOP_PINGPONG" value="2" enum="LoopMode">
Repeats playback and reverse playback at both ends of the animation.
</constant>
</constants>
</class>
1 change: 1 addition & 0 deletions doc/classes/AnimationNode.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<argument index="2" name="delta" type="float" />
<argument index="3" name="seeked" type="bool" />
<argument index="4" name="blend" type="float" />
<argument index="5" name="pingponged" type="int" default="0" />
<description>
Blend an animation by [code]blend[/code] amount (name must be valid in the linked [AnimationPlayer]). A [code]time[/code] and [code]delta[/code] may be passed, as well as whether [code]seek[/code] happened.
</description>
Expand Down
9 changes: 9 additions & 0 deletions doc/classes/AnimationNodeAnimation.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,14 @@
<member name="animation" type="StringName" setter="set_animation" getter="get_animation" default="&amp;&quot;&quot;">
Animation to use as an output. It is one of the animations provided by [member AnimationTree.anim_player].
</member>
<member name="play_mode" type="int" setter="set_play_mode" getter="get_play_mode" enum="AnimationNodeAnimation.PlayMode" default="0">
Determines the playback direction of the animation.
</member>
</members>
<constants>
<constant name="PLAY_MODE_FORWARD" value="0" enum="PlayMode">
</constant>
<constant name="PLAY_MODE_BACKWARD" value="1" enum="PlayMode">
</constant>
</constants>
</class>
2 changes: 1 addition & 1 deletion doc/classes/AudioStreamSample.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
<constant name="LOOP_FORWARD" value="1" enum="LoopMode">
Audio loops the data between [member loop_begin] and [member loop_end], playing forward only.
</constant>
<constant name="LOOP_PING_PONG" value="2" enum="LoopMode">
<constant name="LOOP_PINGPONG" value="2" enum="LoopMode">
Audio loops the data between [member loop_begin] and [member loop_end], playing back and forth.
</constant>
<constant name="LOOP_BACKWARD" value="3" enum="LoopMode">
Expand Down
59 changes: 44 additions & 15 deletions editor/animation_track_editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1377,8 +1377,20 @@ void AnimationTimelineEdit::_anim_length_changed(double p_new_len) {

void AnimationTimelineEdit::_anim_loop_pressed() {
undo_redo->create_action(TTR("Change Animation Loop"));
undo_redo->add_do_method(animation.ptr(), "set_loop", loop->is_pressed());
undo_redo->add_undo_method(animation.ptr(), "set_loop", animation->has_loop());
switch (animation->get_loop_mode()) {
case Animation::LoopMode::LOOP_NONE: {
undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LoopMode::LOOP_LINEAR);
} break;
case Animation::LoopMode::LOOP_LINEAR: {
undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LoopMode::LOOP_PINGPONG);
} break;
case Animation::LoopMode::LOOP_PINGPONG: {
undo_redo->add_do_method(animation.ptr(), "set_loop_mode", Animation::LoopMode::LOOP_NONE);
} break;
default:
break;
}
undo_redo->add_undo_method(animation.ptr(), "set_loop_mode", animation->get_loop_mode());
undo_redo->commit_action();
}

Expand Down Expand Up @@ -1664,7 +1676,24 @@ void AnimationTimelineEdit::update_values() {
length->set_tooltip(TTR("Animation length (seconds)"));
time_icon->set_tooltip(TTR("Animation length (seconds)"));
}
loop->set_pressed(animation->has_loop());

switch (animation->get_loop_mode()) {
case Animation::LoopMode::LOOP_NONE: {
loop->set_icon(get_theme_icon("Loop", "EditorIcons"));
loop->set_pressed(false);
} break;
case Animation::LoopMode::LOOP_LINEAR: {
loop->set_icon(get_theme_icon("Loop", "EditorIcons"));
loop->set_pressed(true);
} break;
case Animation::LoopMode::LOOP_PINGPONG: {
loop->set_icon(get_theme_icon("PingPongLoop", "EditorIcons"));
loop->set_pressed(true);
} break;
default:
break;
}

editing = false;
}

Expand Down Expand Up @@ -2110,25 +2139,25 @@ void AnimationTrackEdit::_notification(int p_what) {

Ref<Texture2D> icon = wrap_icon[loop_wrap ? 1 : 0];

loop_mode_rect.position.x = ofs;
loop_mode_rect.position.y = int(get_size().height - icon->get_height()) / 2;
loop_mode_rect.size = icon->get_size();
loop_wrap_rect.position.x = ofs;
loop_wrap_rect.position.y = int(get_size().height - icon->get_height()) / 2;
loop_wrap_rect.size = icon->get_size();

if (!animation->track_is_compressed(track) && (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D)) {
draw_texture(icon, loop_mode_rect.position);
draw_texture(icon, loop_wrap_rect.position);
}

loop_mode_rect.position.y = 0;
loop_mode_rect.size.y = get_size().height;
loop_wrap_rect.position.y = 0;
loop_wrap_rect.size.y = get_size().height;

ofs += icon->get_width() + hsep;
loop_mode_rect.size.x += hsep;
loop_wrap_rect.size.x += hsep;

if (!animation->track_is_compressed(track) && (animation->track_get_type(track) == Animation::TYPE_VALUE || animation->track_get_type(track) == Animation::TYPE_BLEND_SHAPE || animation->track_get_type(track) == Animation::TYPE_POSITION_3D || animation->track_get_type(track) == Animation::TYPE_SCALE_3D || animation->track_get_type(track) == Animation::TYPE_ROTATION_3D)) {
draw_texture(down_icon, Vector2(ofs, int(get_size().height - down_icon->get_height()) / 2));
loop_mode_rect.size.x += down_icon->get_width();
loop_wrap_rect.size.x += down_icon->get_width();
} else {
loop_mode_rect = Rect2();
loop_wrap_rect = Rect2();
}

ofs += down_icon->get_width();
Expand Down Expand Up @@ -2478,7 +2507,7 @@ String AnimationTrackEdit::get_tooltip(const Point2 &p_pos) const {
return TTR("Interpolation Mode");
}

if (loop_mode_rect.has_point(p_pos)) {
if (loop_wrap_rect.has_point(p_pos)) {
return TTR("Loop Wrap Mode (Interpolate end with beginning on loop)");
}

Expand Down Expand Up @@ -2681,7 +2710,7 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
accept_event();
}

if (loop_mode_rect.has_point(pos)) {
if (loop_wrap_rect.has_point(pos)) {
if (!menu) {
menu = memnew(PopupMenu);
add_child(menu);
Expand All @@ -2692,7 +2721,7 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &p_event) {
menu->add_icon_item(get_theme_icon(SNAME("InterpWrapLoop"), SNAME("EditorIcons")), TTR("Wrap Loop Interp"), MENU_LOOP_WRAP);
menu->set_as_minsize();

Vector2 popup_pos = get_screen_position() + loop_mode_rect.position + Vector2(0, loop_mode_rect.size.height);
Vector2 popup_pos = get_screen_position() + loop_wrap_rect.position + Vector2(0, loop_wrap_rect.size.height);
menu->set_position(popup_pos);
menu->popup();
accept_event();
Expand Down
3 changes: 2 additions & 1 deletion editor/animation_track_editor.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ class AnimationTrackEdit : public Control {

Rect2 update_mode_rect;
Rect2 interp_mode_rect;
Rect2 loop_mode_rect;
Rect2 loop_wrap_rect;
Rect2 remove_rect;
Rect2 bezier_edit_rect;

Expand Down Expand Up @@ -466,6 +466,7 @@ class AnimationTrackEditor : public VBoxContainer {
Animation::TrackType track_type = Animation::TrackType::TYPE_ANIMATION;
Animation::InterpolationType interp_type = Animation::InterpolationType::INTERPOLATION_CUBIC;
Animation::UpdateMode update_mode = Animation::UpdateMode::UPDATE_CAPTURE;
Animation::LoopMode loop_mode = Animation::LoopMode::LOOP_LINEAR;
bool loop_wrap = false;
bool enabled = false;

Expand Down
1 change: 1 addition & 0 deletions editor/icons/PingPongLoop.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 10 additions & 10 deletions editor/import/resource_importer_scene.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -424,10 +424,10 @@ Node *ResourceImporterScene::_pre_fix_node(Node *p_node, Node *p_root, Map<Ref<I

String animname = E;
const int loop_string_count = 3;
static const char *loop_strings[loop_string_count] = { "loops", "loop", "cycle" };
static const char *loop_strings[loop_string_count] = { "loop_mode", "loop", "cycle" };
for (int i = 0; i < loop_string_count; i++) {
if (_teststr(animname, loop_strings[i])) {
anim->set_loop(true);
anim->set_loop_mode(Animation::LoopMode::LOOP_LINEAR);
animname = _fixstr(animname, loop_strings[i]);
ap->rename_animation(E, animname);
}
Expand Down Expand Up @@ -868,15 +868,15 @@ Node *ResourceImporterScene::_post_fix_node(Node *p_node, Node *p_root, Map<Ref<
String name = node_settings["clip_" + itos(i + 1) + "/name"];
int from_frame = node_settings["clip_" + itos(i + 1) + "/start_frame"];
int end_frame = node_settings["clip_" + itos(i + 1) + "/end_frame"];
bool loop = node_settings["clip_" + itos(i + 1) + "/loops"];
Animation::LoopMode loop_mode = static_cast<Animation::LoopMode>((int)node_settings["clip_" + itos(i + 1) + "/loop_mode"]);
bool save_to_file = node_settings["clip_" + itos(i + 1) + "/save_to_file/enabled"];
bool save_to_path = node_settings["clip_" + itos(i + 1) + "/save_to_file/path"];
bool save_to_file_keep_custom = node_settings["clip_" + itos(i + 1) + "/save_to_file/keep_custom_tracks"];

animation_clips.push_back(name);
animation_clips.push_back(from_frame / p_animation_fps);
animation_clips.push_back(end_frame / p_animation_fps);
animation_clips.push_back(loop);
animation_clips.push_back(loop_mode);
animation_clips.push_back(save_to_file);
animation_clips.push_back(save_to_path);
animation_clips.push_back(save_to_file_keep_custom);
Expand All @@ -903,7 +903,7 @@ Node *ResourceImporterScene::_post_fix_node(Node *p_node, Node *p_root, Map<Ref<
}
}

anim->set_loop(anim_settings["settings/loops"]);
anim->set_loop_mode(static_cast<Animation::LoopMode>((int)anim_settings["settings/loop_mode"]));
bool save = anim_settings["save_to_file/enabled"];
String path = anim_settings["save_to_file/path"];
bool keep_custom = anim_settings["save_to_file/keep_custom_tracks"];
Expand Down Expand Up @@ -977,7 +977,7 @@ Ref<Animation> ResourceImporterScene::_save_animation_to_file(Ref<Animation> ani
old_anim->copy_track(i, anim);
}
}
anim->set_loop(old_anim->has_loop());
anim->set_loop_mode(old_anim->get_loop_mode());
}
}

Expand Down Expand Up @@ -1005,7 +1005,7 @@ void ResourceImporterScene::_create_clips(AnimationPlayer *anim, const Array &p_
String name = p_clips[i];
float from = p_clips[i + 1];
float to = p_clips[i + 2];
bool loop = p_clips[i + 3];
Animation::LoopMode loop_mode = static_cast<Animation::LoopMode>((int)p_clips[i + 3]);
bool save_to_file = p_clips[i + 4];
String save_to_path = p_clips[i + 5];
bool keep_current = p_clips[i + 6];
Expand Down Expand Up @@ -1135,7 +1135,7 @@ void ResourceImporterScene::_create_clips(AnimationPlayer *anim, const Array &p_
}
}

new_anim->set_loop(loop);
new_anim->set_loop_mode(loop_mode);
new_anim->set_length(to - from);
anim->add_animation(name, new_anim);

Expand Down Expand Up @@ -1218,7 +1218,7 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p
r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "use_external/path", PROPERTY_HINT_FILE, "*.material,*.res,*.tres"), ""));
} break;
case INTERNAL_IMPORT_CATEGORY_ANIMATION: {
r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "settings/loops"), false));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "settings/loop_mode"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "save_to_file/enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "save_to_file/path", PROPERTY_HINT_SAVE_FILE, "*.res,*.tres"), ""));
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "save_to_file/keep_custom_tracks"), ""));
Expand All @@ -1240,7 +1240,7 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p
r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "slice_" + itos(i + 1) + "/name"), ""));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "slice_" + itos(i + 1) + "/start_frame"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "slice_" + itos(i + 1) + "/end_frame"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "slice_" + itos(i + 1) + "/loops"), false));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "slice_" + itos(i + 1) + "/loop_mode"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "slice_" + itos(i + 1) + "/save_to_file/enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "slice_" + itos(i + 1) + "/save_to_file/path", PROPERTY_HINT_SAVE_FILE, ".res,*.tres"), ""));
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "slice_" + itos(i + 1) + "/save_to_file/keep_custom_tracks"), false));
Expand Down
2 changes: 1 addition & 1 deletion editor/import/resource_importer_wav.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s
if (loop_type == 0x00) {
loop = AudioStreamSample::LOOP_FORWARD;
} else if (loop_type == 0x01) {
loop = AudioStreamSample::LOOP_PING_PONG;
loop = AudioStreamSample::LOOP_PINGPONG;
} else if (loop_type == 0x02) {
loop = AudioStreamSample::LOOP_BACKWARD;
}
Expand Down
9 changes: 5 additions & 4 deletions editor/plugins/animation_player_editor_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include "editor/plugins/canvas_item_editor_plugin.h" // For onion skinning.
#include "editor/plugins/node_3d_editor_plugin.h" // For onion skinning.
#include "scene/main/window.h"
#include "scene/resources/animation.h"
#include "servers/rendering_server.h"

void AnimationPlayerEditor::_node_removed(Node *p_node) {
Expand Down Expand Up @@ -72,7 +73,7 @@ void AnimationPlayerEditor::_notification(int p_what) {
if (player->has_animation(animname)) {
Ref<Animation> anim = player->get_animation(animname);
if (!anim.is_null()) {
frame->set_max(anim->get_length());
frame->set_max((double)anim->get_length());
}
}
}
Expand Down Expand Up @@ -289,7 +290,7 @@ void AnimationPlayerEditor::_animation_selected(int p_which) {
track_editor->set_root(root);
}
}
frame->set_max(anim->get_length());
frame->set_max((double)anim->get_length());

} else {
track_editor->set_animation(Ref<Animation>());
Expand Down Expand Up @@ -1014,7 +1015,7 @@ void AnimationPlayerEditor::_seek_value_changed(float p_value, bool p_set, bool
Ref<Animation> anim;
anim = player->get_animation(current);

float pos = CLAMP(anim->get_length() * (p_value / frame->get_max()), 0, anim->get_length());
float pos = CLAMP((double)anim->get_length() * (p_value / frame->get_max()), 0, (double)anim->get_length());
if (track_editor->is_snap_enabled()) {
pos = Math::snapped(pos, _get_editor_step());
}
Expand Down Expand Up @@ -1424,7 +1425,7 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() {

float pos = cpos + step_off * anim->get_step();

bool valid = anim->has_loop() || (pos >= 0 && pos <= anim->get_length());
bool valid = anim->get_loop_mode() != Animation::LoopMode::LOOP_NONE || (pos >= 0 && pos <= anim->get_length());
onion.captures_valid.write[cidx] = valid;
if (valid) {
player->seek(pos, true);
Expand Down
Loading

2 comments on commit 06a33e5

@TheOrangeDay
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks to have introduced a cast issue between double and float in the generated C# Mathf.PingPong method.

@TheOrangeDay
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this is actually caused by Fract which uses Math.Floor. real_t is float but System.Math.Floor only accepts double or decimal

Please sign in to comment.