Merge pull request #117275 from uno1982/feature/blend-space-sync-continuous

Add `SyncMode::CYCLIC` to BlendSpace1D and BlendSpace2D
This commit is contained in:
Thaddeus Crews
2026-04-13 11:45:45 -05:00
10 changed files with 586 additions and 195 deletions
+22 -3
View File
@@ -101,6 +101,9 @@
<member name="blend_mode" type="int" setter="set_blend_mode" getter="get_blend_mode" enum="AnimationNodeBlendSpace1D.BlendMode" default="0">
Controls the interpolation between animations.
</member>
<member name="cyclic_length" type="float" setter="set_cyclic_length" getter="get_cyclic_length" default="0.0">
The cycle length in seconds used by [constant SYNC_MODE_CYCLIC_CONSTANT]. All animations are time-scaled so they complete one full cycle in this duration. Must be greater than [code]0[/code] for cyclic sync to take effect.
</member>
<member name="max_space" type="float" setter="set_max_space" getter="get_max_space" default="1.0">
The blend space's axis's upper limit for the points' position. See [method add_blend_point].
</member>
@@ -110,9 +113,11 @@
<member name="snap" type="float" setter="set_snap" getter="get_snap" default="0.1">
Position increment to snap to when moving a point on the axis.
</member>
<member name="sync" type="bool" setter="set_use_sync" getter="is_using_sync" default="false">
If [code]false[/code], the blended animations' frame are stopped when the blend value is [code]0[/code].
If [code]true[/code], forcing the blended animations to advance frame.
<member name="sync" type="bool" setter="set_use_sync" getter="is_using_sync" deprecated="Use [member sync_mode] instead.">
If [code]true[/code], sync mode is enabled (equivalent to [constant SYNC_MODE_INDEPENDENT]). This property is kept for backward compatibility.
</member>
<member name="sync_mode" type="int" setter="set_sync_mode" getter="get_sync_mode" enum="AnimationNodeBlendSpace1D.SyncMode" default="0">
Controls how animations are synced when blended. See [enum SyncMode] for available options.
</member>
<member name="value_label" type="String" setter="set_value_label" getter="get_value_label" default="&quot;value&quot;">
Label of the virtual axis of the blend space.
@@ -128,5 +133,19 @@
<constant name="BLEND_MODE_DISCRETE_CARRY" value="2" enum="BlendMode">
Similar to [constant BLEND_MODE_DISCRETE], but starts the new animation at the last animation's playback position.
</constant>
<constant name="SYNC_MODE_NONE" value="0" enum="SyncMode">
Inactive animations are frozen and do not advance.
</constant>
<constant name="SYNC_MODE_INDEPENDENT" value="1" enum="SyncMode">
Inactive animations advance with a weight of [code]0[/code]. This is equivalent to the previous [code]sync = true[/code] behavior.
</constant>
<constant name="SYNC_MODE_CYCLIC_MUTABLE" value="2" enum="SyncMode">
All animations are time-scaled so they stay in sync, with the cycle length dynamically computed from active blend weights. This is self-normalizing: a solo animation plays at normal speed.
[b]Note:[/b] If you apply [AnimationNodeTimeSeek] to the result when handling animations of different lengths, synchronization will be broken. In such cases, it is recommended to use [member AnimationNodeAnimation.use_custom_timeline] to align the animation lengths.
</constant>
<constant name="SYNC_MODE_CYCLIC_CONSTANT" value="3" enum="SyncMode">
All animations are time-scaled so they complete one cycle in [member cyclic_length] seconds, keeping them in sync regardless of their individual lengths.
[b]Note:[/b] If you apply [AnimationNodeTimeSeek] to the result when handling animations of different lengths, synchronization will be broken. In such cases, it is recommended to use [member AnimationNodeAnimation.use_custom_timeline] to align the animation lengths.
</constant>
</constants>
</class>
+22 -3
View File
@@ -136,6 +136,9 @@
<member name="blend_mode" type="int" setter="set_blend_mode" getter="get_blend_mode" enum="AnimationNodeBlendSpace2D.BlendMode" default="0">
Controls the interpolation between animations.
</member>
<member name="cyclic_length" type="float" setter="set_cyclic_length" getter="get_cyclic_length" default="0.0">
The cycle length in seconds used by [constant SYNC_MODE_CYCLIC_CONSTANT]. All animations are time-scaled so they complete one full cycle in this duration. Must be greater than [code]0[/code] for cyclic sync to take effect.
</member>
<member name="max_space" type="Vector2" setter="set_max_space" getter="get_max_space" default="Vector2(1, 1)">
The blend space's X and Y axes' upper limit for the points' position. See [method add_blend_point].
</member>
@@ -145,9 +148,11 @@
<member name="snap" type="Vector2" setter="set_snap" getter="get_snap" default="Vector2(0.1, 0.1)">
Position increment to snap to when moving a point.
</member>
<member name="sync" type="bool" setter="set_use_sync" getter="is_using_sync" default="false">
If [code]false[/code], the blended animations' frame are stopped when the blend value is [code]0[/code].
If [code]true[/code], forcing the blended animations to advance frame.
<member name="sync" type="bool" setter="set_use_sync" getter="is_using_sync" deprecated="Use [member sync_mode] instead.">
If [code]true[/code], sync mode is enabled (equivalent to [constant SYNC_MODE_INDEPENDENT]). This property is kept for backward compatibility.
</member>
<member name="sync_mode" type="int" setter="set_sync_mode" getter="get_sync_mode" enum="AnimationNodeBlendSpace2D.SyncMode" default="0">
Controls how animations are synced when blended. See [enum SyncMode] for available options.
</member>
<member name="x_label" type="String" setter="set_x_label" getter="get_x_label" default="&quot;x&quot;">
Name of the blend space's X axis.
@@ -173,5 +178,19 @@
<constant name="BLEND_MODE_DISCRETE_CARRY" value="2" enum="BlendMode">
Similar to [constant BLEND_MODE_DISCRETE], but starts the new animation at the last animation's playback position.
</constant>
<constant name="SYNC_MODE_NONE" value="0" enum="SyncMode">
Inactive animations are frozen and do not advance.
</constant>
<constant name="SYNC_MODE_INDEPENDENT" value="1" enum="SyncMode">
Inactive animations advance with a weight of [code]0[/code]. This is equivalent to the previous [code]sync = true[/code] behavior.
</constant>
<constant name="SYNC_MODE_CYCLIC_MUTABLE" value="2" enum="SyncMode">
All animations are time-scaled so they stay in sync, with the cycle length dynamically computed from active blend weights. This is self-normalizing: a solo animation plays at normal speed.
[b]Note:[/b] If you apply [AnimationNodeTimeSeek] to the result when handling animations of different lengths, synchronization will be broken. In such cases, it is recommended to use [member AnimationNodeAnimation.use_custom_timeline] to align the animation lengths.
</constant>
<constant name="SYNC_MODE_CYCLIC_CONSTANT" value="3" enum="SyncMode">
All animations are time-scaled so they complete one cycle in [member cyclic_length] seconds, keeping them in sync regardless of their individual lengths.
[b]Note:[/b] If you apply [AnimationNodeTimeSeek] to the result when handling animations of different lengths, synchronization will be broken. In such cases, it is recommended to use [member AnimationNodeAnimation.use_custom_timeline] to align the animation lengths.
</constant>
</constants>
</class>
@@ -42,7 +42,6 @@
#include "editor/themes/editor_scale.h"
#include "scene/animation/animation_blend_tree.h"
#include "scene/gui/button.h"
#include "scene/gui/check_box.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/option_button.h"
#include "scene/gui/panel_container.h"
@@ -294,6 +293,7 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_draw() {
int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));
Ref<Texture2D> icon = get_editor_theme_icon(SNAME("KeyValue"));
Ref<Texture2D> icon_selected = get_editor_theme_icon(SNAME("KeySelected"));
Ref<Texture2D> icon_invalid = get_editor_theme_icon(SNAME("KeyInvalid"));
const float pm = POINT_MARGIN * EDSCALE;
@@ -331,8 +331,8 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_draw() {
}
}
bool does_include_invalid_key = false;
points.clear();
for (int i = 0; i < blend_space->get_blend_point_count(); i++) {
float point = blend_space->get_blend_point_position(i);
if (!read_only && dragging_selected && selected_point == i) {
@@ -348,8 +348,19 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_draw() {
points.push_back(point + ofs.x);
// Draw × marker on non-AnimationNodeAnimation points when in cyclic mode.
bool is_key_valid = true;
AnimationNodeBlendSpace1D::SyncMode sync_mode = blend_space->get_sync_mode();
if (sync_mode == AnimationNodeBlendSpace1D::SYNC_MODE_CYCLIC_MUTABLE || sync_mode == AnimationNodeBlendSpace1D::SYNC_MODE_CYCLIC_CONSTANT) {
Ref<AnimationNode> node = blend_space->get_blend_point_node(i);
Ref<AnimationNodeAnimation> anim_node = node;
if (anim_node.is_null()) {
is_key_valid = false;
does_include_invalid_key = true;
}
}
Vector2 gui_point = (ofs + Vector2(point, s.height / 2.0) - icon->get_size() / 2.0).floor();
blend_space_draw->draw_texture(i == selected_point ? icon_selected : icon, gui_point);
blend_space_draw->draw_texture(is_key_valid ? (i == selected_point ? icon_selected : icon) : icon_invalid, gui_point);
if (point >= 0.0 && point <= s.width && editing_point != i) {
String name_text = show_indices ? itos(i) : String(blend_space->get_blend_point_name(i));
@@ -366,6 +377,11 @@ void AnimationNodeBlendSpace1DEditor::_blend_space_draw() {
text_rects.write[i] = Rect2(Vector2(text_pos.x, text_pos.y - font->get_ascent(font_size)), text_size);
}
}
if (does_include_invalid_key) {
invalid_point_warning_hb->show();
} else {
invalid_point_warning_hb->hide();
}
// blend position
{
@@ -403,7 +419,10 @@ void AnimationNodeBlendSpace1DEditor::_update_space() {
max_value->set_value(blend_space->get_max_space());
min_value->set_value(blend_space->get_min_space());
sync->set_pressed(blend_space->is_using_sync());
sync->select(blend_space->get_sync_mode());
cyclic_length_value->set_value(blend_space->get_cyclic_length());
cyclic_length_value->set_visible(blend_space->get_sync_mode() == AnimationNodeBlendSpace1D::SYNC_MODE_CYCLIC_CONSTANT);
interpolation->select(blend_space->get_blend_mode());
label_value->set_text(blend_space->get_value_label());
@@ -433,8 +452,10 @@ void AnimationNodeBlendSpace1DEditor::_config_changed(double) {
undo_redo->add_undo_method(blend_space.ptr(), "set_min_space", blend_space->get_min_space());
undo_redo->add_do_method(blend_space.ptr(), "set_snap", snap_value->get_value());
undo_redo->add_undo_method(blend_space.ptr(), "set_snap", blend_space->get_snap());
undo_redo->add_do_method(blend_space.ptr(), "set_use_sync", sync->is_pressed());
undo_redo->add_undo_method(blend_space.ptr(), "set_use_sync", blend_space->is_using_sync());
undo_redo->add_do_method(blend_space.ptr(), "set_sync_mode", sync->get_selected());
undo_redo->add_undo_method(blend_space.ptr(), "set_sync_mode", blend_space->get_sync_mode());
undo_redo->add_do_method(blend_space.ptr(), "set_cyclic_length", cyclic_length_value->get_value());
undo_redo->add_undo_method(blend_space.ptr(), "set_cyclic_length", blend_space->get_cyclic_length());
undo_redo->add_do_method(blend_space.ptr(), "set_blend_mode", interpolation->get_selected());
undo_redo->add_undo_method(blend_space.ptr(), "set_blend_mode", blend_space->get_blend_mode());
undo_redo->add_do_method(this, "_update_space");
@@ -442,6 +463,9 @@ void AnimationNodeBlendSpace1DEditor::_config_changed(double) {
undo_redo->commit_action();
updating = false;
// Update cyclic_length visibility immediately (undo/redo calls _update_space while updating=true).
cyclic_length_value->set_visible(sync->get_selected() == AnimationNodeBlendSpace1D::SYNC_MODE_CYCLIC_CONSTANT);
blend_space_draw->queue_redraw();
}
@@ -755,6 +779,7 @@ void AnimationNodeBlendSpace1DEditor::_notification(int p_what) {
interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackContinuous")), TTR("Continuous"), 0);
interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackDiscrete")), TTR("Discrete"), 1);
interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackCapture")), TTR("Capture"), 2);
invalid_point_warning->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning")));
} break;
case NOTIFICATION_PROCESS: {
@@ -772,6 +797,11 @@ void AnimationNodeBlendSpace1DEditor::_notification(int p_what) {
}
}
void AnimationNodeBlendSpace1DEditor::_show_invalid_point_warning() {
EditorNode::get_singleton()->show_warning(
TTR("BlendSpace contains points that are not AnimationNodeAnimation.\nCyclic sync modes require that all blend points use AnimationNodeAnimation with a finite, immutable length."));
}
void AnimationNodeBlendSpace1DEditor::_bind_methods() {
ClassDB::bind_method("_update_space", &AnimationNodeBlendSpace1DEditor::_update_space);
ClassDB::bind_method("_update_tool_erase", &AnimationNodeBlendSpace1DEditor::_update_tool_erase);
@@ -804,6 +834,7 @@ void AnimationNodeBlendSpace1DEditor::edit(const Ref<AnimationNode> &p_node) {
min_value->set_editable(!read_only);
max_value->set_editable(!read_only);
sync->set_disabled(read_only);
cyclic_length_value->set_editable(!read_only);
interpolation->set_disabled(read_only);
}
@@ -999,9 +1030,24 @@ AnimationNodeBlendSpace1DEditor::AnimationNodeBlendSpace1DEditor() {
top_hb->add_child(memnew(VSeparator));
top_hb->add_child(memnew(Label(TTR("Sync"))));
sync = memnew(CheckBox);
sync = memnew(OptionButton);
sync->add_item(TTR("None"));
sync->add_item(TTR("Independent"));
sync->add_item(TTR("Cyclic Mutable"));
sync->add_item(TTR("Cyclic Constant"));
top_hb->add_child(sync);
sync->connect(SceneStringName(toggled), callable_mp(this, &AnimationNodeBlendSpace1DEditor::_config_changed));
sync->connect(SceneStringName(item_selected), callable_mp(this, &AnimationNodeBlendSpace1DEditor::_config_changed));
cyclic_length_value = memnew(SpinBox);
cyclic_length_value->set_min(0.0);
cyclic_length_value->set_max(99.0);
cyclic_length_value->set_step(0.001);
cyclic_length_value->set_allow_greater(true);
cyclic_length_value->set_suffix("s");
cyclic_length_value->set_accessibility_name(TTRC("Cyclic Length"));
cyclic_length_value->set_tooltip_text(TTR("Cycle length in seconds for cyclic sync. All animations are time-scaled to complete one cycle in this duration."));
top_hb->add_child(cyclic_length_value);
cyclic_length_value->connect(SceneStringName(value_changed), callable_mp(this, &AnimationNodeBlendSpace1DEditor::_config_changed));
top_hb->add_child(memnew(VSeparator));
@@ -1115,6 +1161,16 @@ AnimationNodeBlendSpace1DEditor::AnimationNodeBlendSpace1DEditor() {
error_panel->add_child(error_label);
error_panel->hide();
invalid_point_warning_hb = memnew(HBoxContainer);
add_child(invalid_point_warning_hb);
invalid_point_warning_hb->hide();
invalid_point_warning = memnew(Button);
invalid_point_warning->set_text(TTRC("Contains Invalid Point"));
invalid_point_warning->set_tooltip_text(TTRC("Warning: BlendSpace contains invalid points for cyclic sync"));
invalid_point_warning->connect(SceneStringName(pressed), callable_mp(this, &AnimationNodeBlendSpace1DEditor::_show_invalid_point_warning));
invalid_point_warning_hb->add_child(invalid_point_warning);
menu = memnew(PopupMenu);
add_child(menu);
menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationNodeBlendSpace1DEditor::_add_menu_type));
@@ -67,7 +67,8 @@ class AnimationNodeBlendSpace1DEditor : public AnimationTreeNodeEditorPlugin {
SpinBox *max_value = nullptr;
SpinBox *min_value = nullptr;
CheckBox *sync = nullptr;
OptionButton *sync = nullptr;
SpinBox *cyclic_length_value = nullptr;
OptionButton *interpolation = nullptr;
HBoxContainer *edit_hb = nullptr;
@@ -83,6 +84,10 @@ class AnimationNodeBlendSpace1DEditor : public AnimationTreeNodeEditorPlugin {
PanelContainer *error_panel = nullptr;
RichTextLabel *error_label = nullptr;
HBoxContainer *invalid_point_warning_hb = nullptr;
Button *invalid_point_warning = nullptr;
void _show_invalid_point_warning();
bool updating = false;
static AnimationNodeBlendSpace1DEditor *singleton;
@@ -43,7 +43,6 @@
#include "editor/themes/editor_scale.h"
#include "scene/animation/animation_blend_tree.h"
#include "scene/gui/button.h"
#include "scene/gui/check_box.h"
#include "scene/gui/grid_container.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/option_button.h"
@@ -90,6 +89,7 @@ void AnimationNodeBlendSpace2DEditor::edit(const Ref<AnimationNode> &p_node) {
tool_triangle->set_disabled(read_only);
auto_triangles->set_disabled(read_only);
sync->set_disabled(read_only);
cyclic_length_value->set_editable(!read_only);
interpolation->set_disabled(read_only);
}
@@ -535,6 +535,7 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_draw() {
int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label"));
Ref<Texture2D> icon = get_editor_theme_icon(SNAME("KeyValue"));
Ref<Texture2D> icon_selected = get_editor_theme_icon(SNAME("KeySelected"));
Ref<Texture2D> icon_invalid = get_editor_theme_icon(SNAME("KeyInvalid"));
const float pm = POINT_MARGIN * EDSCALE;
@@ -629,6 +630,7 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_draw() {
blend_space_draw->draw_primitive(bl_points, colors, Vector<Vector2>());
}
bool does_include_invalid_key = false;
points.clear();
for (int i = 0; i < blend_space->get_blend_point_count(); i++) {
Vector2 point = blend_space->get_blend_point_position(i);
@@ -646,8 +648,19 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_draw() {
points.push_back(ofs + point);
// Draw × marker on non-AnimationNodeAnimation points when in cyclic mode.
bool is_key_valid = true;
AnimationNodeBlendSpace2D::SyncMode sync_mode = blend_space->get_sync_mode();
if (sync_mode == AnimationNodeBlendSpace2D::SYNC_MODE_CYCLIC_MUTABLE || sync_mode == AnimationNodeBlendSpace2D::SYNC_MODE_CYCLIC_CONSTANT) {
Ref<AnimationNode> node = blend_space->get_blend_point_node(i);
Ref<AnimationNodeAnimation> anim_node = node;
if (anim_node.is_null()) {
is_key_valid = false;
does_include_invalid_key = true;
}
}
Vector2 gui_point = (ofs + point - icon->get_size() / 2).floor();
blend_space_draw->draw_texture(i == selected_point ? icon_selected : icon, gui_point);
blend_space_draw->draw_texture(is_key_valid ? (i == selected_point ? icon_selected : icon) : icon_invalid, gui_point);
if (point.x >= 0.0 && point.x <= s.width && point.y >= 0.0 && point.y <= s.height && editing_point != i) {
String name_text = show_indices ? itos(i) : String(blend_space->get_blend_point_name(i));
@@ -669,6 +682,11 @@ void AnimationNodeBlendSpace2DEditor::_blend_space_draw() {
text_rects.write[i] = Rect2(Vector2(text_pos.x, text_pos.y - font->get_ascent(font_size)), text_size);
}
}
if (does_include_invalid_key) {
invalid_point_warning_hb->show();
} else {
invalid_point_warning_hb->hide();
}
if (making_triangle.size()) {
Vector<Vector2> bl_points;
@@ -745,7 +763,10 @@ void AnimationNodeBlendSpace2DEditor::_update_space() {
auto_triangles->set_pressed(blend_space->get_auto_triangles());
sync->set_pressed(blend_space->is_using_sync());
sync->select(blend_space->get_sync_mode());
cyclic_length_value->set_value(blend_space->get_cyclic_length());
cyclic_length_value->set_visible(blend_space->get_sync_mode() == AnimationNodeBlendSpace2D::SYNC_MODE_CYCLIC_CONSTANT);
interpolation->select(blend_space->get_blend_mode());
max_x_value->set_value(blend_space->get_max_space().x);
@@ -785,8 +806,10 @@ void AnimationNodeBlendSpace2DEditor::_config_changed(double) {
undo_redo->add_undo_method(blend_space.ptr(), "set_min_space", blend_space->get_min_space());
undo_redo->add_do_method(blend_space.ptr(), "set_snap", Vector2(snap_x->get_value(), snap_y->get_value()));
undo_redo->add_undo_method(blend_space.ptr(), "set_snap", blend_space->get_snap());
undo_redo->add_do_method(blend_space.ptr(), "set_use_sync", sync->is_pressed());
undo_redo->add_undo_method(blend_space.ptr(), "set_use_sync", blend_space->is_using_sync());
undo_redo->add_do_method(blend_space.ptr(), "set_sync_mode", sync->get_selected());
undo_redo->add_undo_method(blend_space.ptr(), "set_sync_mode", blend_space->get_sync_mode());
undo_redo->add_do_method(blend_space.ptr(), "set_cyclic_length", cyclic_length_value->get_value());
undo_redo->add_undo_method(blend_space.ptr(), "set_cyclic_length", blend_space->get_cyclic_length());
undo_redo->add_do_method(blend_space.ptr(), "set_blend_mode", interpolation->get_selected());
undo_redo->add_undo_method(blend_space.ptr(), "set_blend_mode", blend_space->get_blend_mode());
undo_redo->add_do_method(this, "_update_space");
@@ -794,6 +817,9 @@ void AnimationNodeBlendSpace2DEditor::_config_changed(double) {
undo_redo->commit_action();
updating = false;
// Update cyclic_length visibility immediately (undo/redo calls _update_space while updating=true).
cyclic_length_value->set_visible(sync->get_selected() == AnimationNodeBlendSpace2D::SYNC_MODE_CYCLIC_CONSTANT);
blend_space_draw->queue_redraw();
}
@@ -998,6 +1024,7 @@ void AnimationNodeBlendSpace2DEditor::_notification(int p_what) {
interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackContinuous")), TTR("Continuous"), 0);
interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackDiscrete")), TTR("Discrete"), 1);
interpolation->add_icon_item(get_editor_theme_icon(SNAME("TrackCapture")), TTR("Capture"), 2);
invalid_point_warning->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning")));
} break;
case NOTIFICATION_PROCESS: {
@@ -1015,6 +1042,11 @@ void AnimationNodeBlendSpace2DEditor::_notification(int p_what) {
}
}
void AnimationNodeBlendSpace2DEditor::_show_invalid_point_warning() {
EditorNode::get_singleton()->show_warning(
TTR("BlendSpace contains points that are not AnimationNodeAnimation.\nCyclic sync modes require that all blend points use AnimationNodeAnimation with a finite, immutable length."));
}
void AnimationNodeBlendSpace2DEditor::_open_editor() {
if (selected_point >= 0 && selected_point < blend_space->get_blend_point_count()) {
Ref<AnimationNode> an = blend_space->get_blend_point_node(selected_point);
@@ -1265,9 +1297,24 @@ AnimationNodeBlendSpace2DEditor::AnimationNodeBlendSpace2DEditor() {
top_hb->add_child(memnew(VSeparator));
top_hb->add_child(memnew(Label(TTR("Sync"))));
sync = memnew(CheckBox);
sync = memnew(OptionButton);
sync->add_item(TTR("None"));
sync->add_item(TTR("Independent"));
sync->add_item(TTR("Cyclic Mutable"));
sync->add_item(TTR("Cyclic Constant"));
top_hb->add_child(sync);
sync->connect(SceneStringName(toggled), callable_mp(this, &AnimationNodeBlendSpace2DEditor::_config_changed));
sync->connect(SceneStringName(item_selected), callable_mp(this, &AnimationNodeBlendSpace2DEditor::_config_changed));
cyclic_length_value = memnew(SpinBox);
cyclic_length_value->set_min(0.0);
cyclic_length_value->set_max(99.0);
cyclic_length_value->set_step(0.001);
cyclic_length_value->set_allow_greater(true);
cyclic_length_value->set_suffix("s");
cyclic_length_value->set_accessibility_name(TTRC("Cyclic Length"));
cyclic_length_value->set_tooltip_text(TTR("Cycle length in seconds for cyclic sync. All animations are time-scaled to complete one cycle in this duration."));
top_hb->add_child(cyclic_length_value);
cyclic_length_value->connect(SceneStringName(value_changed), callable_mp(this, &AnimationNodeBlendSpace2DEditor::_config_changed));
top_hb->add_child(memnew(VSeparator));
@@ -1423,6 +1470,16 @@ AnimationNodeBlendSpace2DEditor::AnimationNodeBlendSpace2DEditor() {
error_panel->add_child(error_label);
error_panel->hide();
invalid_point_warning_hb = memnew(HBoxContainer);
add_child(invalid_point_warning_hb);
invalid_point_warning_hb->hide();
invalid_point_warning = memnew(Button);
invalid_point_warning->set_text(TTRC("Contains Invalid Point"));
invalid_point_warning->set_tooltip_text(TTRC("Warning: BlendSpace contains invalid points for cyclic sync"));
invalid_point_warning->connect(SceneStringName(pressed), callable_mp(this, &AnimationNodeBlendSpace2DEditor::_show_invalid_point_warning));
invalid_point_warning_hb->add_child(invalid_point_warning);
set_custom_minimum_size(Size2(0, 300 * EDSCALE));
menu = memnew(PopupMenu);
@@ -64,7 +64,8 @@ class AnimationNodeBlendSpace2DEditor : public AnimationTreeNodeEditorPlugin {
Button *snap = nullptr;
SpinBox *snap_x = nullptr;
SpinBox *snap_y = nullptr;
CheckBox *sync = nullptr;
OptionButton *sync = nullptr;
SpinBox *cyclic_length_value = nullptr;
OptionButton *interpolation = nullptr;
Button *auto_triangles = nullptr;
@@ -91,6 +92,10 @@ class AnimationNodeBlendSpace2DEditor : public AnimationTreeNodeEditorPlugin {
PanelContainer *error_panel = nullptr;
RichTextLabel *error_label = nullptr;
HBoxContainer *invalid_point_warning_hb = nullptr;
Button *invalid_point_warning = nullptr;
void _show_invalid_point_warning();
bool updating;
static AnimationNodeBlendSpace2DEditor *singleton;
+165 -76
View File
@@ -63,6 +63,7 @@ Ref<AnimationNode> AnimationNodeBlendSpace1D::get_child_by_name(const StringName
void AnimationNodeBlendSpace1D::_tree_changed() {
AnimationRootNode::_tree_changed();
_check_can_sync();
}
void AnimationNodeBlendSpace1D::_animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name) {
@@ -109,19 +110,38 @@ void AnimationNodeBlendSpace1D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_blend_mode", "mode"), &AnimationNodeBlendSpace1D::set_blend_mode);
ClassDB::bind_method(D_METHOD("get_blend_mode"), &AnimationNodeBlendSpace1D::get_blend_mode);
#ifndef DISABLE_DEPRECATED
ClassDB::bind_method(D_METHOD("set_use_sync", "enable"), &AnimationNodeBlendSpace1D::set_use_sync);
ClassDB::bind_method(D_METHOD("is_using_sync"), &AnimationNodeBlendSpace1D::is_using_sync);
#endif // DISABLE_DEPRECATED
ClassDB::bind_method(D_METHOD("set_sync_mode", "sync_mode"), &AnimationNodeBlendSpace1D::set_sync_mode);
ClassDB::bind_method(D_METHOD("get_sync_mode"), &AnimationNodeBlendSpace1D::get_sync_mode);
ClassDB::bind_method(D_METHOD("set_cyclic_length", "length"), &AnimationNodeBlendSpace1D::set_cyclic_length);
ClassDB::bind_method(D_METHOD("get_cyclic_length"), &AnimationNodeBlendSpace1D::get_cyclic_length);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "min_space", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_min_space", "get_min_space");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_space", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_max_space", "get_max_space");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "snap", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_snap", "get_snap");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "value_label", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_value_label", "get_value_label");
ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Interpolated,Discrete,Carry", PROPERTY_USAGE_NO_EDITOR), "set_blend_mode", "get_blend_mode");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_use_sync", "is_using_sync");
#ifndef DISABLE_DEPRECATED
// Keep "sync" as bool with old getter/setter for GDExtension API backward compatibility (4.0-4.6).
// Old scenes with "sync = true" still load via set_use_sync -> SYNC_MODE_INDEPENDENT.
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_use_sync", "is_using_sync");
#endif // DISABLE_DEPRECATED
ADD_PROPERTY(PropertyInfo(Variant::INT, "sync_mode", PROPERTY_HINT_ENUM, "None,Independent,Cyclic Mutable,Cyclic Constant", PROPERTY_USAGE_NO_EDITOR), "set_sync_mode", "get_sync_mode");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "cyclic_length", PROPERTY_HINT_RANGE, "0.0,99.0,0.001,or_greater", PROPERTY_USAGE_NO_EDITOR), "set_cyclic_length", "get_cyclic_length");
BIND_ENUM_CONSTANT(BLEND_MODE_INTERPOLATED);
BIND_ENUM_CONSTANT(BLEND_MODE_DISCRETE);
BIND_ENUM_CONSTANT(BLEND_MODE_DISCRETE_CARRY);
BIND_ENUM_CONSTANT(SYNC_MODE_NONE);
BIND_ENUM_CONSTANT(SYNC_MODE_INDEPENDENT);
BIND_ENUM_CONSTANT(SYNC_MODE_CYCLIC_MUTABLE);
BIND_ENUM_CONSTANT(SYNC_MODE_CYCLIC_CONSTANT);
}
void AnimationNodeBlendSpace1D::get_child_nodes(LocalVector<ChildNode> *r_child_nodes) {
@@ -161,6 +181,7 @@ void AnimationNodeBlendSpace1D::add_blend_point(const Ref<AnimationRootNode> &p_
_add_node(blend_points[p_at_index].node);
blend_points_used++;
lengths_dirty = true;
_tree_changed();
}
@@ -181,6 +202,7 @@ void AnimationNodeBlendSpace1D::set_blend_point_node(int p_point, const Ref<Anim
blend_points[p_point].node = p_node;
_add_node(blend_points[p_point].node);
lengths_dirty = true;
_tree_changed();
}
@@ -234,6 +256,7 @@ void AnimationNodeBlendSpace1D::remove_blend_point(int p_point) {
blend_points_used--;
blend_points[blend_points_used].name = StringName();
lengths_dirty = true;
emit_signal(SNAME("animation_node_removed"), get_instance_id(), itos(p_point));
_tree_changed();
@@ -307,12 +330,32 @@ AnimationNodeBlendSpace1D::BlendMode AnimationNodeBlendSpace1D::get_blend_mode()
return blend_mode;
}
#ifndef DISABLE_DEPRECATED
void AnimationNodeBlendSpace1D::set_use_sync(bool p_sync) {
sync = p_sync;
sync_mode = p_sync ? SYNC_MODE_INDEPENDENT : SYNC_MODE_NONE;
}
bool AnimationNodeBlendSpace1D::is_using_sync() const {
return sync;
return sync_mode != SYNC_MODE_NONE;
}
#endif // DISABLE_DEPRECATED
void AnimationNodeBlendSpace1D::set_sync_mode(SyncMode p_sync_mode) {
sync_mode = p_sync_mode;
_check_can_sync();
}
AnimationNodeBlendSpace1D::SyncMode AnimationNodeBlendSpace1D::get_sync_mode() const {
return sync_mode;
}
void AnimationNodeBlendSpace1D::set_cyclic_length(double p_length) {
cyclic_length = p_length;
inverted_cycle_length = (p_length > CMP_EPSILON) ? (1.0 / p_length) : 0.0;
}
double AnimationNodeBlendSpace1D::get_cyclic_length() const {
return cyclic_length;
}
bool AnimationNodeBlendSpace1D::_set(const StringName &p_name, const Variant &p_value) {
@@ -365,8 +408,22 @@ void AnimationNodeBlendSpace1D::_get_property_list(List<PropertyInfo> *p_list) c
}
}
void AnimationNodeBlendSpace1D::_check_can_sync() {
is_contain_invalid_point = false;
if (sync_mode < SYNC_MODE_CYCLIC_MUTABLE) {
return;
}
for (int i = 0; i < blend_points_used; i++) {
Ref<AnimationNodeAnimation> na = static_cast<Ref<AnimationNodeAnimation>>(blend_points[i].node);
if (na.is_null()) {
is_contain_invalid_point = true;
break;
}
}
}
AnimationNode::NodeTimeInfo AnimationNodeBlendSpace1D::_process(ProcessState &p_process_state, AnimationNodeInstance &p_instance, const AnimationMixer::PlaybackInfo &p_playback_info, bool p_test_only) {
if (!blend_points_used) {
if (!blend_points_used || is_contain_invalid_point) {
return NodeTimeInfo();
}
@@ -383,6 +440,18 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace1D::_process(ProcessState &p_
int cur_closest = p_instance.get_parameter_closest();
NodeTimeInfo mind;
// Build weights array for all blend points.
LocalVector<float> weights;
LocalVector<double> deltas;
weights.resize_initialized(blend_points_used);
deltas.resize_initialized(blend_points_used);
if (sync_mode == SYNC_MODE_NONE) {
for (int i = 0; i < blend_points_used; i++) {
deltas[i] = -1; // Not process.
}
}
int new_closest = -1;
if (blend_mode == BLEND_MODE_INTERPOLATED) {
int point_lower = -1;
float pos_lower = 0.0;
@@ -405,57 +474,28 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace1D::_process(ProcessState &p_
}
// fill in weights
float weights[MAX_BLEND_POINTS] = {};
if (point_lower == -1 && point_higher != -1) {
// we are on the left side, no other point to the left
// we just play the next point.
weights[point_higher] = 1.0;
} else if (point_higher == -1) {
// we are on the right side, no other point to the right
// we just play the previous point
weights[point_lower] = 1.0;
} else {
// we are between two points.
// figure out weights, then blend the animations
float distance_between_points = pos_higher - pos_lower;
float current_pos_inbetween = blend_pos - pos_lower;
float blend_percentage = current_pos_inbetween / distance_between_points;
float blend_lower = 1.0 - blend_percentage;
float blend_higher = blend_percentage;
weights[point_lower] = blend_lower;
weights[point_higher] = blend_higher;
weights[point_lower] = 1.0 - blend_percentage;
weights[point_higher] = blend_percentage;
}
// actually blend the animations now
bool first = true;
double max_weight = 0.0;
// Determine closest for return info.
float max_weight = 0.0;
for (int i = 0; i < blend_points_used; i++) {
if (i == point_lower || i == point_higher) {
pi.weight = weights[i];
AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i));
NodeTimeInfo t = blend_node(p_process_state, p_instance, &other_instance, pi, FILTER_IGNORE, true, p_test_only);
if (first || pi.weight > max_weight) {
max_weight = pi.weight;
mind = t;
first = false;
}
} else if (sync) {
pi.weight = 0;
AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i));
blend_node(p_process_state, p_instance, &other_instance, pi, FILTER_IGNORE, true, p_test_only);
if (weights[i] >= max_weight) {
max_weight = weights[i];
new_closest = i;
}
}
} else {
int new_closest = -1;
double new_closest_dist = 1e20;
for (int i = 0; i < blend_points_used; i++) {
double d = std::abs(blend_points[i].position - blend_pos);
if (d < new_closest_dist) {
@@ -463,49 +503,98 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace1D::_process(ProcessState &p_
new_closest_dist = d;
}
}
weights[new_closest] = 1.0;
}
AnimationNodeInstance *instance_current_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(cur_closest));
if (new_closest != cur_closest && new_closest != -1) {
AnimationNodeInstance *instance_new_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(new_closest));
if (blend_mode == BLEND_MODE_DISCRETE_CARRY && cur_closest != -1) {
NodeTimeInfo from;
// For ping-pong loop.
Ref<AnimationNodeAnimation> na_c = static_cast<Ref<AnimationNodeAnimation>>(blend_points[cur_closest].node);
Ref<AnimationNodeAnimation> na_n = static_cast<Ref<AnimationNodeAnimation>>(blend_points[new_closest].node);
if (na_c.is_valid() && na_n.is_valid() && instance_current_closest && instance_new_closest) {
na_n->set_backward(*instance_new_closest, p_process_state, na_c->is_backward(*instance_current_closest, p_process_state));
na_n = nullptr;
na_c = nullptr;
}
// See how much animation remains.
pi.seeked = false;
pi.weight = 0;
from = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, true);
pi.time = from.position;
}
pi.seeked = true;
pi.weight = 1.0;
mind = blend_node(p_process_state, p_instance, instance_new_closest, pi, FILTER_IGNORE, true, p_test_only);
cur_closest = new_closest;
} else {
pi.weight = 1.0;
mind = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, p_test_only);
}
if (sync) {
pi = p_playback_info;
pi.weight = 0;
// Compute all deltas.
if (sync_mode >= SYNC_MODE_CYCLIC_MUTABLE) {
// Refresh cached lengths when blend points have changed.
if (lengths_dirty) {
cached_lengths.resize(blend_points_used);
for (int i = 0; i < blend_points_used; i++) {
if (i != cur_closest) {
AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i));
blend_node(p_process_state, p_instance, &other_instance, pi, FILTER_IGNORE, true, p_test_only);
AnimationMixer::PlaybackInfo test_pi = p_playback_info;
test_pi.weight = 0;
AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i));
NodeTimeInfo info = blend_node(p_process_state, p_instance, &other_instance, test_pi, FILTER_IGNORE, true, true);
cached_lengths[i] = (info.length > CMP_EPSILON) ? info.length : 0.0;
}
lengths_dirty = false;
}
double inv_target_length = 0.0;
if (sync_mode == SYNC_MODE_CYCLIC_MUTABLE) {
// Compute blended_len from active weights.
double target_length = 0.0;
double total_weight = 0.0;
for (int i = 0; i < blend_points_used; i++) {
if (weights[i] > 0.0f && cached_lengths[i] > CMP_EPSILON) {
target_length += weights[i] * cached_lengths[i];
total_weight += weights[i];
}
}
if (total_weight > CMP_EPSILON) {
target_length /= total_weight;
}
inv_target_length = (target_length > CMP_EPSILON) ? (1.0 / target_length) : 0.0;
} else {
// Use cached inverse of user-specified cyclic_length.
inv_target_length = inverted_cycle_length;
}
for (int i = 0; i < blend_points_used; i++) {
deltas[i] = pi.delta * cached_lengths[i] * inv_target_length;
}
} else if (sync_mode == SYNC_MODE_INDEPENDENT) {
for (int i = 0; i < blend_points_used; i++) {
deltas[i] = pi.delta;
}
} else {
// SYNC_MODE_NONE: only active points get delta.
for (int i = 0; i < blend_points_used; i++) {
if (weights[i] >= CMP_EPSILON) {
deltas[i] = pi.delta;
}
}
}
p_instance.set_parameter_closest(cur_closest, p_process_state.is_testing);
// Special case for discrete carry.
AnimationNodeInstance *instance_current_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(cur_closest));
if (new_closest != cur_closest && new_closest != -1 && cur_closest != -1 && blend_mode == BLEND_MODE_DISCRETE_CARRY && sync_mode < SYNC_MODE_CYCLIC_MUTABLE) {
AnimationNodeInstance *instance_new_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(new_closest));
NodeTimeInfo from;
// For ping-pong loop.
Ref<AnimationNodeAnimation> na_c = static_cast<Ref<AnimationNodeAnimation>>(blend_points[cur_closest].node);
Ref<AnimationNodeAnimation> na_n = static_cast<Ref<AnimationNodeAnimation>>(blend_points[new_closest].node);
if (na_c.is_valid() && na_n.is_valid() && instance_current_closest && instance_new_closest) {
na_n->set_backward(*instance_new_closest, p_process_state, na_c->is_backward(*instance_current_closest, p_process_state));
na_n = nullptr;
na_c = nullptr;
}
// See how much animation remains.
pi.seeked = false;
pi.weight = 0;
from = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, true);
pi.time = from.position;
pi.seeked = true;
pi.weight = 1.0;
mind = blend_node(p_process_state, p_instance, instance_new_closest, pi, FILTER_IGNORE, true, p_test_only);
deltas[new_closest] = -1; // No more blend new_closest point.
}
// Normal case, blend all points.
for (int i = 0; i < blend_points_used; i++) {
if (std::signbit(deltas[i])) {
continue;
}
pi = p_playback_info;
pi.weight = weights[i];
pi.delta = deltas[i];
AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i));
NodeTimeInfo t = blend_node(p_process_state, p_instance, &other_instance, pi, FILTER_IGNORE, true, p_test_only);
if (i == new_closest) {
mind = t;
}
}
p_instance.set_parameter_closest(new_closest, p_process_state.is_testing);
return mind;
}
+26 -3
View File
@@ -42,6 +42,13 @@ public:
BLEND_MODE_DISCRETE_CARRY,
};
enum SyncMode {
SYNC_MODE_NONE, // Inactive animations are frozen (not advanced).
SYNC_MODE_INDEPENDENT, // Inactive animations advance with weight=0 (previous "sync" behavior).
SYNC_MODE_CYCLIC_MUTABLE, // Time-scaled with blend-weight-dependent cycle length (self-normalizing).
SYNC_MODE_CYCLIC_CONSTANT, // Time-scaled to complete one cycle in cyclic_length seconds.
};
protected:
enum {
MAX_BLEND_POINTS = 64
@@ -72,7 +79,11 @@ protected:
BlendMode blend_mode = BLEND_MODE_INTERPOLATED;
bool sync = false;
SyncMode sync_mode = SYNC_MODE_NONE;
double cyclic_length = 0.0;
double inverted_cycle_length = 0.0; // Cached 1/cyclic_length.
LocalVector<double> cached_lengths;
bool lengths_dirty = true;
static void _bind_methods();
@@ -80,6 +91,9 @@ protected:
virtual void _animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name) override;
virtual void _animation_node_removed(const ObjectID &p_oid, const StringName &p_node) override;
bool is_contain_invalid_point = false;
void _check_can_sync();
#ifndef DISABLE_DEPRECATED
void _add_blend_point_bind_compat_110369(const Ref<AnimationRootNode> &p_node, float p_position, int p_at_index = -1);
static void _bind_compatibility_methods();
@@ -122,8 +136,16 @@ public:
void set_blend_mode(BlendMode p_blend_mode);
BlendMode get_blend_mode() const;
void set_use_sync(bool p_sync);
bool is_using_sync() const;
void set_cyclic_length(double p_length);
double get_cyclic_length() const;
#ifndef DISABLE_DEPRECATED
void set_use_sync(bool p_sync); // Compat: maps to SYNC_MODE_INDEPENDENT or SYNC_MODE_NONE.
bool is_using_sync() const; // Compat: returns sync_mode != SYNC_MODE_NONE.
#endif // DISABLE_DEPRECATED
void set_sync_mode(SyncMode p_sync_mode);
SyncMode get_sync_mode() const;
virtual NodeTimeInfo _process(ProcessState &p_process_state, AnimationNodeInstance &p_instance, const AnimationMixer::PlaybackInfo &p_playback_info, bool p_test_only = false) override;
String get_caption() const override;
@@ -132,3 +154,4 @@ public:
};
VARIANT_ENUM_CAST(AnimationNodeBlendSpace1D::BlendMode)
VARIANT_ENUM_CAST(AnimationNodeBlendSpace1D::SyncMode)
+184 -89
View File
@@ -99,6 +99,7 @@ void AnimationNodeBlendSpace2D::add_blend_point(const Ref<AnimationRootNode> &p_
_add_node(blend_points[p_at_index].node);
blend_points_used++;
lengths_dirty = true;
_queue_auto_triangles();
_tree_changed();
@@ -120,6 +121,7 @@ void AnimationNodeBlendSpace2D::set_blend_point_node(int p_point, const Ref<Anim
blend_points[p_point].node = p_node;
_add_node(blend_points[p_point].node);
lengths_dirty = true;
_tree_changed();
}
@@ -189,6 +191,7 @@ void AnimationNodeBlendSpace2D::remove_blend_point(int p_point) {
blend_points_used--;
blend_points[blend_points_used].name = StringName();
lengths_dirty = true;
emit_signal(SNAME("animation_node_removed"), get_instance_id(), itos(p_point));
_tree_changed();
@@ -506,7 +509,7 @@ Vector2 AnimationNodeBlendSpace2D::get_closest_point(const Vector2 &p_point) {
return best_point;
}
void AnimationNodeBlendSpace2D::_blend_triangle(const Vector2 &p_pos, const Vector2 *p_points, float *r_weights) {
void AnimationNodeBlendSpace2D::_blend_triangle(const Vector2 &p_pos, const LocalVector<Vector2> &p_points, LocalVector<float> &r_weights) {
if (p_pos.is_equal_approx(p_points[0])) {
r_weights[0] = 1;
r_weights[1] = 0;
@@ -551,32 +554,61 @@ void AnimationNodeBlendSpace2D::_blend_triangle(const Vector2 &p_pos, const Vect
r_weights[2] = w;
}
void AnimationNodeBlendSpace2D::_check_can_sync() {
is_contain_invalid_point = false;
if (sync_mode < SYNC_MODE_CYCLIC_MUTABLE) {
return;
}
for (int i = 0; i < blend_points_used; i++) {
Ref<AnimationNodeAnimation> na = static_cast<Ref<AnimationNodeAnimation>>(blend_points[i].node);
if (na.is_null()) {
is_contain_invalid_point = true;
break;
}
}
}
AnimationNode::NodeTimeInfo AnimationNodeBlendSpace2D::_process(ProcessState &p_process_state, AnimationNodeInstance &p_instance, const AnimationMixer::PlaybackInfo &p_playback_info, bool p_test_only) {
_update_triangles();
if (!blend_points_used) {
if (!blend_points_used || is_contain_invalid_point) {
return NodeTimeInfo();
}
Vector2 blend_pos = p_instance.get_parameter(blend_position);
int cur_closest = p_instance.get_parameter_closest();
NodeTimeInfo mind; //time of min distance point
constexpr int TRIANGLE_VERTS = 3;
AnimationMixer::PlaybackInfo pi = p_playback_info;
Vector2 blend_pos = p_instance.get_parameter(blend_position);
int cur_closest = p_instance.get_parameter_closest();
NodeTimeInfo mind; // Time of closest point.
// Build weights array for all blend points.
LocalVector<float> weights;
LocalVector<double> deltas;
weights.resize_initialized(blend_points_used);
deltas.resize_initialized(blend_points_used);
if (sync_mode == SYNC_MODE_NONE) {
for (int i = 0; i < blend_points_used; i++) {
deltas[i] = -1; // Not process.
}
}
int new_closest = -1;
LocalVector<int> triangle_points;
LocalVector<float> blend_weights;
triangle_points.resize_initialized(TRIANGLE_VERTS);
blend_weights.resize_initialized(TRIANGLE_VERTS);
if (blend_mode == BLEND_MODE_INTERPOLATED) {
if (triangles.is_empty()) {
return NodeTimeInfo();
}
Vector2 best_point;
bool first = true;
int blend_triangle = -1;
float blend_weights[3] = { 0, 0, 0 };
bool first = true;
LocalVector<Vector2> points;
for (int i = 0; i < triangles.size(); i++) {
Vector2 points[3];
for (int j = 0; j < 3; j++) {
points.resize_initialized(TRIANGLE_VERTS);
for (int j = 0; j < TRIANGLE_VERTS; j++) {
points[j] = get_blend_point_position(get_triangle_point(i, j));
}
@@ -586,9 +618,9 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace2D::_process(ProcessState &p_
break;
}
for (int j = 0; j < 3; j++) {
for (int j = 0; j < TRIANGLE_VERTS; j++) {
const Vector2 segment_a = points[j];
const Vector2 segment_b = points[(j + 1) % 3];
const Vector2 segment_b = points[(j + 1) % TRIANGLE_VERTS];
Vector2 closest2 = Geometry2D::get_closest_point_to_segment(blend_pos, segment_a, segment_b);
if (first || closest2.distance_to(blend_pos) < best_point.distance_to(blend_pos)) {
best_point = closest2;
@@ -597,57 +629,33 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace2D::_process(ProcessState &p_
const real_t d = segment_a.distance_to(segment_b);
if (d == 0.0) {
blend_weights[j] = 1.0;
blend_weights[(j + 1) % 3] = 0.0;
blend_weights[(j + 2) % 3] = 0.0;
blend_weights[(j + 1) % TRIANGLE_VERTS] = 0.0;
blend_weights[(j + 2) % TRIANGLE_VERTS] = 0.0;
} else {
const real_t c = segment_a.distance_to(closest2) / d;
blend_weights[j] = 1.0 - c;
blend_weights[(j + 1) % 3] = c;
blend_weights[(j + 2) % 3] = 0.0;
blend_weights[(j + 1) % TRIANGLE_VERTS] = c;
blend_weights[(j + 2) % TRIANGLE_VERTS] = 0.0;
}
}
}
}
ERR_FAIL_COND_V(blend_triangle == -1, NodeTimeInfo()); // Should never reach here.
ERR_FAIL_COND_V(blend_triangle == -1, NodeTimeInfo()); //should never reach here
int triangle_points[3];
for (int j = 0; j < 3; j++) {
triangle_points[j] = get_triangle_point(blend_triangle, j);
for (int i = 0; i < TRIANGLE_VERTS; i++) {
triangle_points[i] = get_triangle_point(blend_triangle, i);
}
first = true;
double max_weight = 0.0;
for (int i = 0; i < blend_points_used; i++) {
bool found = false;
for (int j = 0; j < 3; j++) {
if (i == triangle_points[j]) {
//blend with the given weight
pi.weight = blend_weights[j];
AnimationNodeInstance *other_instance = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(i));
NodeTimeInfo t = blend_node(p_process_state, p_instance, other_instance, pi, FILTER_IGNORE, true, p_test_only);
if (first || pi.weight > max_weight) {
mind = t;
max_weight = pi.weight;
first = false;
}
found = true;
break;
}
}
if (sync && !found) {
pi.weight = 0;
AnimationNodeInstance *other_instance = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(i));
blend_node(p_process_state, p_instance, other_instance, pi, FILTER_IGNORE, true, p_test_only);
float max_weight = 0.0;
for (int i = 0; i < TRIANGLE_VERTS; i++) {
weights[triangle_points[i]] = blend_weights[i];
if (blend_weights[i] >= max_weight) {
max_weight = blend_weights[i];
new_closest = triangle_points[i];
}
}
} else {
int new_closest = -1;
float new_closest_dist = 1e20;
for (int i = 0; i < blend_points_used; i++) {
float d = blend_points[i].position.distance_squared_to(blend_pos);
if (d < new_closest_dist) {
@@ -655,49 +663,98 @@ AnimationNode::NodeTimeInfo AnimationNodeBlendSpace2D::_process(ProcessState &p_
new_closest_dist = d;
}
}
weights[new_closest] = 1.0;
}
AnimationNodeInstance *instance_current_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(cur_closest));
if (new_closest != cur_closest && new_closest != -1) {
AnimationNodeInstance *instance_new_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(new_closest));
if (blend_mode == BLEND_MODE_DISCRETE_CARRY && cur_closest != -1) {
NodeTimeInfo from;
// For ping-pong loop.
Ref<AnimationNodeAnimation> na_c = static_cast<Ref<AnimationNodeAnimation>>(blend_points[cur_closest].node);
Ref<AnimationNodeAnimation> na_n = static_cast<Ref<AnimationNodeAnimation>>(blend_points[new_closest].node);
if (na_c.is_valid() && na_n.is_valid() && instance_current_closest && instance_new_closest) {
na_n->set_backward(*instance_new_closest, p_process_state, na_c->is_backward(*instance_current_closest, p_process_state));
na_n = nullptr;
na_c = nullptr;
}
// See how much animation remains.
pi.seeked = false;
pi.weight = 0;
from = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, true);
pi.time = from.position;
}
pi.seeked = true;
pi.weight = 1.0;
mind = blend_node(p_process_state, p_instance, instance_new_closest, pi, FILTER_IGNORE, true, p_test_only);
cur_closest = new_closest;
} else {
pi.weight = 1.0;
mind = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, p_test_only);
}
if (sync) {
pi = p_playback_info;
pi.weight = 0;
// Compute all deltas.
if (sync_mode >= SYNC_MODE_CYCLIC_MUTABLE) {
// Refresh cached lengths when blend points have changed.
if (lengths_dirty) {
cached_lengths.resize(blend_points_used);
for (int i = 0; i < blend_points_used; i++) {
if (i != cur_closest) {
AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i));
blend_node(p_process_state, p_instance, &other_instance, pi, FILTER_IGNORE, true, p_test_only);
AnimationMixer::PlaybackInfo test_pi = p_playback_info;
test_pi.weight = 0;
AnimationNodeInstance &other_instance = p_instance.get_child_instance_by_path(get_blend_point_name(i));
NodeTimeInfo info = blend_node(p_process_state, p_instance, &other_instance, test_pi, FILTER_IGNORE, true, true);
cached_lengths[i] = (info.length > CMP_EPSILON) ? info.length : 0.0;
}
lengths_dirty = false;
}
double inv_target_length = 0.0;
if (sync_mode == SYNC_MODE_CYCLIC_MUTABLE) {
// Compute blended_len from active weights.
double target_length = 0.0;
double total_weight = 0.0;
for (int i = 0; i < blend_points_used; i++) {
if (weights[i] > 0.0f && cached_lengths[i] > CMP_EPSILON) {
target_length += weights[i] * cached_lengths[i];
total_weight += weights[i];
}
}
if (total_weight > CMP_EPSILON) {
target_length /= total_weight;
}
inv_target_length = (target_length > CMP_EPSILON) ? (1.0 / target_length) : 0.0;
} else {
// Use cached inverse of user-specified cyclic_length.
inv_target_length = inverted_cycle_length;
}
for (int i = 0; i < blend_points_used; i++) {
deltas[i] = pi.delta * cached_lengths[i] * inv_target_length;
}
} else if (sync_mode == SYNC_MODE_INDEPENDENT) {
for (int i = 0; i < blend_points_used; i++) {
deltas[i] = pi.delta;
}
} else {
// SYNC_MODE_NONE: only active points get delta.
for (int i = 0; i < blend_points_used; i++) {
if (weights[i] >= CMP_EPSILON) {
deltas[i] = pi.delta;
}
}
}
p_instance.set_parameter_closest(cur_closest, p_process_state.is_testing);
// Special case for discrete carry.
if (new_closest != cur_closest && new_closest != -1 && cur_closest != -1 && blend_mode == BLEND_MODE_DISCRETE_CARRY && sync_mode < SYNC_MODE_CYCLIC_MUTABLE) {
AnimationNodeInstance *instance_current_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(cur_closest));
AnimationNodeInstance *instance_new_closest = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(new_closest));
NodeTimeInfo from;
// For ping-pong loop.
Ref<AnimationNodeAnimation> na_c = static_cast<Ref<AnimationNodeAnimation>>(blend_points[cur_closest].node);
Ref<AnimationNodeAnimation> na_n = static_cast<Ref<AnimationNodeAnimation>>(blend_points[new_closest].node);
if (na_c.is_valid() && na_n.is_valid() && instance_current_closest && instance_new_closest) {
na_n->set_backward(*instance_new_closest, p_process_state, na_c->is_backward(*instance_current_closest, p_process_state));
na_n = nullptr;
na_c = nullptr;
}
// See how much animation remains.
pi.seeked = false;
pi.weight = 0;
from = blend_node(p_process_state, p_instance, instance_current_closest, pi, FILTER_IGNORE, true, true);
pi.time = from.position;
pi.seeked = true;
pi.weight = 1.0;
mind = blend_node(p_process_state, p_instance, instance_new_closest, pi, FILTER_IGNORE, true, p_test_only);
deltas[new_closest] = -1; // No more blend new_closest point.
}
// Normal case, blend all points.
for (int i = 0; i < blend_points_used; i++) {
if (std::signbit(deltas[i])) {
continue;
}
pi = p_playback_info;
pi.weight = weights[i];
pi.delta = deltas[i];
AnimationNodeInstance *other_instance = p_instance.get_child_instance_by_path_or_null(get_blend_point_name(i));
NodeTimeInfo t = blend_node(p_process_state, p_instance, other_instance, pi, FILTER_IGNORE, true, p_test_only);
if (i == new_closest) {
mind = t;
}
}
p_instance.set_parameter_closest(new_closest, p_process_state.is_testing);
return mind;
}
@@ -740,16 +797,37 @@ AnimationNodeBlendSpace2D::BlendMode AnimationNodeBlendSpace2D::get_blend_mode()
return blend_mode;
}
#ifndef DISABLE_DEPRECATED
void AnimationNodeBlendSpace2D::set_use_sync(bool p_sync) {
sync = p_sync;
sync_mode = p_sync ? SYNC_MODE_INDEPENDENT : SYNC_MODE_NONE;
}
bool AnimationNodeBlendSpace2D::is_using_sync() const {
return sync;
return sync_mode != SYNC_MODE_NONE;
}
#endif // DISABLE_DEPRECATED
void AnimationNodeBlendSpace2D::set_sync_mode(SyncMode p_sync_mode) {
sync_mode = p_sync_mode;
_check_can_sync();
}
AnimationNodeBlendSpace2D::SyncMode AnimationNodeBlendSpace2D::get_sync_mode() const {
return sync_mode;
}
void AnimationNodeBlendSpace2D::set_cyclic_length(double p_length) {
cyclic_length = p_length;
inverted_cycle_length = (p_length > CMP_EPSILON) ? (1.0 / p_length) : 0.0;
}
double AnimationNodeBlendSpace2D::get_cyclic_length() const {
return cyclic_length;
}
void AnimationNodeBlendSpace2D::_tree_changed() {
AnimationRootNode::_tree_changed();
_check_can_sync();
}
void AnimationNodeBlendSpace2D::_animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name) {
@@ -811,8 +889,16 @@ void AnimationNodeBlendSpace2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_blend_mode", "mode"), &AnimationNodeBlendSpace2D::set_blend_mode);
ClassDB::bind_method(D_METHOD("get_blend_mode"), &AnimationNodeBlendSpace2D::get_blend_mode);
#ifndef DISABLE_DEPRECATED
ClassDB::bind_method(D_METHOD("set_use_sync", "enable"), &AnimationNodeBlendSpace2D::set_use_sync);
ClassDB::bind_method(D_METHOD("is_using_sync"), &AnimationNodeBlendSpace2D::is_using_sync);
#endif // DISABLE_DEPRECATED
ClassDB::bind_method(D_METHOD("set_sync_mode", "sync_mode"), &AnimationNodeBlendSpace2D::set_sync_mode);
ClassDB::bind_method(D_METHOD("get_sync_mode"), &AnimationNodeBlendSpace2D::get_sync_mode);
ClassDB::bind_method(D_METHOD("set_cyclic_length", "length"), &AnimationNodeBlendSpace2D::set_cyclic_length);
ClassDB::bind_method(D_METHOD("get_cyclic_length"), &AnimationNodeBlendSpace2D::get_cyclic_length);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_triangles", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_auto_triangles", "get_auto_triangles");
ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "triangles", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_triangles", "_get_triangles");
@@ -823,10 +909,19 @@ void AnimationNodeBlendSpace2D::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::STRING, "x_label", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_x_label", "get_x_label");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "y_label", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_y_label", "get_y_label");
ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_mode", PROPERTY_HINT_ENUM, "Interpolated,Discrete,Carry", PROPERTY_USAGE_NO_EDITOR), "set_blend_mode", "get_blend_mode");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_use_sync", "is_using_sync");
#ifndef DISABLE_DEPRECATED
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "sync", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_use_sync", "is_using_sync");
#endif // DISABLE_DEPRECATED
ADD_PROPERTY(PropertyInfo(Variant::INT, "sync_mode", PROPERTY_HINT_ENUM, "None,Independent,Cyclic Mutable,Cyclic Constant", PROPERTY_USAGE_NO_EDITOR), "set_sync_mode", "get_sync_mode");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "cyclic_length", PROPERTY_HINT_RANGE, "0.0,99.0,0.001,or_greater", PROPERTY_USAGE_NO_EDITOR), "set_cyclic_length", "get_cyclic_length");
ADD_SIGNAL(MethodInfo("triangles_updated"));
BIND_ENUM_CONSTANT(BLEND_MODE_INTERPOLATED);
BIND_ENUM_CONSTANT(BLEND_MODE_DISCRETE);
BIND_ENUM_CONSTANT(BLEND_MODE_DISCRETE_CARRY);
BIND_ENUM_CONSTANT(SYNC_MODE_NONE);
BIND_ENUM_CONSTANT(SYNC_MODE_INDEPENDENT);
BIND_ENUM_CONSTANT(SYNC_MODE_CYCLIC_MUTABLE);
BIND_ENUM_CONSTANT(SYNC_MODE_CYCLIC_CONSTANT);
}
+27 -4
View File
@@ -42,6 +42,13 @@ public:
BLEND_MODE_DISCRETE_CARRY,
};
enum SyncMode {
SYNC_MODE_NONE, // Inactive animations are frozen (not advanced).
SYNC_MODE_INDEPENDENT, // Inactive animations advance with weight=0 (previous "sync" behavior).
SYNC_MODE_CYCLIC_MUTABLE, // Time-scaled with blend-weight-dependent cycle length (self-normalizing).
SYNC_MODE_CYCLIC_CONSTANT, // Time-scaled to complete one cycle in cyclic_length seconds.
};
protected:
enum {
MAX_BLEND_POINTS = 64
@@ -78,7 +85,7 @@ protected:
void _set_triangles(const Vector<int> &p_triangles);
Vector<int> _get_triangles() const;
void _blend_triangle(const Vector2 &p_pos, const Vector2 *p_points, float *r_weights);
void _blend_triangle(const Vector2 &p_pos, const LocalVector<Vector2> &p_points, LocalVector<float> &r_weights);
bool auto_triangles = true;
bool triangles_dirty = false;
@@ -86,7 +93,11 @@ protected:
void _update_triangles();
void _queue_auto_triangles();
bool sync = false;
SyncMode sync_mode = SYNC_MODE_NONE;
double cyclic_length = 0.0;
double inverted_cycle_length = 0.0; // Cached 1/cyclic_length.
LocalVector<double> cached_lengths;
bool lengths_dirty = true;
void _validate_property(PropertyInfo &p_property) const;
static void _bind_methods();
@@ -95,6 +106,9 @@ protected:
virtual void _animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name) override;
virtual void _animation_node_removed(const ObjectID &p_oid, const StringName &p_node) override;
bool is_contain_invalid_point = false;
void _check_can_sync();
#ifndef DISABLE_DEPRECATED
void _add_blend_point_bind_compat_110369(const Ref<AnimationRootNode> &p_node, const Vector2 &p_position, int p_at_index = -1);
static void _bind_compatibility_methods();
@@ -153,10 +167,19 @@ public:
void set_blend_mode(BlendMode p_blend_mode);
BlendMode get_blend_mode() const;
void set_use_sync(bool p_sync);
bool is_using_sync() const;
void set_cyclic_length(double p_length);
double get_cyclic_length() const;
#ifndef DISABLE_DEPRECATED
void set_use_sync(bool p_sync); // Compat: maps to SYNC_MODE_INDEPENDENT or SYNC_MODE_NONE.
bool is_using_sync() const; // Compat: returns sync_mode != SYNC_MODE_NONE.
#endif // DISABLE_DEPRECATED
void set_sync_mode(SyncMode p_sync_mode);
SyncMode get_sync_mode() const;
virtual Ref<AnimationNode> get_child_by_name(const StringName &p_name) const override;
};
VARIANT_ENUM_CAST(AnimationNodeBlendSpace2D::BlendMode)
VARIANT_ENUM_CAST(AnimationNodeBlendSpace2D::SyncMode)