diff --git a/blendalot_animation_graph.cpp b/blendalot_animation_graph.cpp index 7ca1a81..2c3d54b 100644 --- a/blendalot_animation_graph.cpp +++ b/blendalot_animation_graph.cpp @@ -23,7 +23,7 @@ void BLTAnimationGraph::_bind_methods() { ClassDB::bind_method(D_METHOD("set_tree_root", "animation_node"), &BLTAnimationGraph::set_root_animation_node); ClassDB::bind_method(D_METHOD("get_tree_root"), &BLTAnimationGraph::get_root_animation_node); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "tree_root", PROPERTY_HINT_RESOURCE_TYPE, "SyncedAnimationNode"), "set_tree_root", "get_tree_root"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "tree_root", PROPERTY_HINT_RESOURCE_TYPE, "BLTAnimationNode"), "set_tree_root", "get_tree_root"); ClassDB::bind_method(D_METHOD("set_skeleton", "skeleton"), &BLTAnimationGraph::set_skeleton); ClassDB::bind_method(D_METHOD("get_skeleton"), &BLTAnimationGraph::get_skeleton); @@ -51,7 +51,7 @@ void BLTAnimationGraph::_update_properties_for_node(const String &p_base_path, R p_node->get_child_nodes(&children); for (const Ref &child_node : children) { - _update_properties_for_node(p_base_path + child_node->name + "/", child_node); + _update_properties_for_node(p_base_path + child_node->get_name() + "/", child_node); } } @@ -132,20 +132,24 @@ void BLTAnimationGraph::_get_property_list(List *p_list) const { } } -void BLTAnimationGraph::_tree_changed() { +void BLTAnimationGraph::_graph_changed(const StringName &node_name) { + print_line(vformat("Graph changed %x", (uintptr_t)this)); + if (properties_dirty) { return; } callable_mp(this, &BLTAnimationGraph::_update_properties).call_deferred(); + callable_mp(this, &BLTAnimationGraph::_setup_graph).call_deferred(); + properties_dirty = true; } void BLTAnimationGraph::_notification(int p_what) { - GodotProfileZone("SyncedAnimationGraph::_notification"); + GodotProfileZone("BLTAnimationGraph::_notification"); switch (p_what) { - case Node::NOTIFICATION_READY: { + case NOTIFICATION_ENTER_TREE: { _setup_evaluation_context(); _setup_graph(); @@ -230,14 +234,9 @@ AnimationMixer::AnimationCallbackModeDiscrete BLTAnimationGraph::get_callback_mo } void BLTAnimationGraph::set_animation_player(const NodePath &p_path) { + print_line(vformat("set_animation_player(%s) ", p_path)); + animation_player_path = p_path; - if (p_path.is_empty()) { - // set_root_node(SceneStringName(path_pp)); - // while (animation_libraries.size()) { - // remove_animation_library(animation_libraries[0].name); - // } - } - graph_context.animation_player = Object::cast_to(get_node_or_null(animation_player_path)); _setup_evaluation_context(); _setup_graph(); @@ -250,15 +249,17 @@ NodePath BLTAnimationGraph::get_animation_player() const { } void BLTAnimationGraph::set_root_animation_node(const Ref &p_animation_node) { + print_line(vformat("setting root node to node %s", p_animation_node->get_name())); + if (root_animation_node.is_valid()) { - root_animation_node->disconnect(SNAME("tree_changed"), callable_mp(this, &BLTAnimationGraph::_tree_changed)); + root_animation_node->disconnect(SNAME("node_changed"), callable_mp(this, &BLTAnimationGraph::_graph_changed)); } root_animation_node = p_animation_node; if (root_animation_node.is_valid()) { _setup_graph(); - root_animation_node->connect(SNAME("tree_changed"), callable_mp(this, &BLTAnimationGraph::_tree_changed)); + root_animation_node->connect(SNAME("node_changed"), callable_mp(this, &BLTAnimationGraph::_graph_changed)); } properties_dirty = true; @@ -271,14 +272,9 @@ Ref BLTAnimationGraph::get_root_animation_node() const { } void BLTAnimationGraph::set_skeleton(const NodePath &p_path) { + print_line(vformat("set_skeleton(%s) ", p_path)); + skeleton_path = p_path; - if (p_path.is_empty()) { - // set_root_node(SceneStringName(path_pp)); - // while (animation_libraries.size()) { - // remove_animation_library(animation_libraries[0].name); - // } - } - graph_context.skeleton_3d = Object::cast_to(get_node_or_null(skeleton_path)); _setup_evaluation_context(); _setup_graph(); @@ -291,11 +287,15 @@ NodePath BLTAnimationGraph::get_skeleton() const { } void BLTAnimationGraph::_process_graph(double p_delta, bool p_update_only) { - if (!root_animation_node.is_valid()) { + if (!root_animation_node.is_valid() || is_graph_initialization_valid == false) { return; } - GodotProfileZone("SyncedAnimationGraph::_process_graph"); + if (graph_context.skeleton_3d == nullptr || graph_context.animation_player == nullptr) { + return; + } + + GodotProfileZone("BLTAnimationGraph::_process_graph"); _update_properties(); @@ -311,7 +311,7 @@ void BLTAnimationGraph::_process_graph(double p_delta, bool p_update_only) { } void BLTAnimationGraph::_apply_animation_data(const AnimationData &output_data) const { - GodotProfileZone("SyncedAnimationGraph::_apply_animation_data"); + GodotProfileZone("BLTAnimationGraph::_apply_animation_data"); for (const KeyValue &K : output_data.value_buffer_offset) { const AnimationData::TrackValue *track_value = output_data.get_value(K.key); @@ -353,10 +353,20 @@ void BLTAnimationGraph::_set_process(bool p_process, bool p_force) { processing = p_process; } -void BLTAnimationGraph::_setup_evaluation_context() { - _cleanup_evaluation_context(); +void BLTAnimationGraph::_setup_animation_player() { + if (!is_inside_tree()) { + return; + } graph_context.animation_player = Object::cast_to(get_node_or_null(animation_player_path)); + print_line(vformat("AnimationPlayer of graph %x is now %x", (uintptr_t)(this), (uintptr_t)graph_context.animation_player)); +} + +void BLTAnimationGraph::_setup_evaluation_context() { + print_line("_setup_evaluation_context()"); + _cleanup_evaluation_context(); + + _setup_animation_player(); graph_context.skeleton_3d = Object::cast_to(get_node_or_null(skeleton_path)); } @@ -370,7 +380,9 @@ void BLTAnimationGraph::_setup_graph() { return; } - root_animation_node->initialize(graph_context); + print_line(vformat("_setup_graph() on graph %x and root node %x", (uintptr_t)(void *)(this), (uintptr_t)(root_animation_node.ptr()))); + is_graph_initialization_valid = root_animation_node->initialize(graph_context); + print_line(vformat("is_graph_initialization_valid = %s", is_graph_initialization_valid ? "true" : "false")); } BLTAnimationGraph::BLTAnimationGraph() { diff --git a/blendalot_animation_graph.h b/blendalot_animation_graph.h index a532997..3298979 100644 --- a/blendalot_animation_graph.h +++ b/blendalot_animation_graph.h @@ -19,11 +19,12 @@ private: mutable AHashMap, StringName>> parameter_to_node_parameter_map; mutable bool properties_dirty = true; + bool is_graph_initialization_valid = false; void _update_properties() const; void _update_properties_for_node(const String &p_base_path, Ref p_node) const; - void _tree_changed(); + void _graph_changed(const StringName &node_name); protected: void _notification(int p_what); @@ -75,6 +76,7 @@ public: private: void _set_process(bool p_process, bool p_force = false); + void _setup_animation_player(); void _setup_evaluation_context(); void _cleanup_evaluation_context(); diff --git a/blendalot_animation_node.cpp b/blendalot_animation_node.cpp index 1418d25..3cd933c 100644 --- a/blendalot_animation_node.cpp +++ b/blendalot_animation_node.cpp @@ -5,9 +5,18 @@ #include "blendalot_animation_node.h" void BLTAnimationNode::_bind_methods() { - ADD_SIGNAL(MethodInfo("tree_changed")); + ClassDB::bind_method(D_METHOD("set_position", "position"), &BLTAnimationNode::set_position); + ClassDB::bind_method(D_METHOD("get_position"), &BLTAnimationNode::get_position); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "position", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_position", "get_position"); + ADD_SIGNAL(MethodInfo("animation_node_renamed", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "old_name"), PropertyInfo(Variant::STRING, "new_name"))); ADD_SIGNAL(MethodInfo("animation_node_removed", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "name"))); + + ADD_SIGNAL(MethodInfo(SNAME("node_changed"), PropertyInfo(Variant::STRING_NAME, "node_name"))); + + ClassDB::bind_method(D_METHOD("get_input_names"), &BLTAnimationNode::get_input_names_as_typed_array); + ClassDB::bind_method(D_METHOD("get_input_count"), &BLTAnimationNode::get_input_count); + ClassDB::bind_method(D_METHOD("get_input_index", "node"), &BLTAnimationNode::get_input_index); } void BLTAnimationNode::get_parameter_list(List *r_list) const { @@ -28,8 +37,8 @@ Variant BLTAnimationNode::get_parameter(const StringName &p_name) const { return Variant(); } -void BLTAnimationNode::_tree_changed() { - emit_signal(SNAME("tree_changed")); +void BLTAnimationNode::_node_changed() { + emit_signal(SNAME("node_changed"), get_name()); } void BLTAnimationNode::_animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name) { @@ -40,119 +49,10 @@ void BLTAnimationNode::_animation_node_removed(const ObjectID &p_oid, const Stri emit_signal(SNAME("animation_node_removed"), p_oid, p_node); } -void BLTAnimationNodeBlendTree::_bind_methods() { - ClassDB::bind_method(D_METHOD("add_node", "animation_node"), &BLTAnimationNodeBlendTree::add_node); - ClassDB::bind_method(D_METHOD("get_output_node"), &BLTAnimationNodeBlendTree::get_output_node); - ClassDB::bind_method(D_METHOD("add_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::add_connection); - - BIND_CONSTANT(CONNECTION_OK); - BIND_CONSTANT(CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED); - BIND_CONSTANT(CONNECTION_ERROR_NO_SOURCE_NODE); - BIND_CONSTANT(CONNECTION_ERROR_NO_TARGET_NODE); - BIND_CONSTANT(CONNECTION_ERROR_PARENT_EXISTS); - BIND_CONSTANT(CONNECTION_ERROR_TARGET_PORT_NOT_FOUND); - BIND_CONSTANT(CONNECTION_ERROR_TARGET_PORT_ALREADY_CONNECTED); - BIND_CONSTANT(CONNECTION_ERROR_CONNECTION_CREATES_LOOP); -} - -void BLTAnimationNodeBlendTree::_get_property_list(List *p_list) const { - for (const Ref &node : tree_graph.nodes) { - String prop_name = node->name; - if (prop_name != "Output") { - p_list->push_back(PropertyInfo(Variant::OBJECT, "nodes/" + prop_name + "/node", PROPERTY_HINT_RESOURCE_TYPE, "AnimationNode", PROPERTY_USAGE_NO_EDITOR)); - } - p_list->push_back(PropertyInfo(Variant::VECTOR2, "nodes/" + prop_name + "/position", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); - } - - p_list->push_back(PropertyInfo(Variant::ARRAY, "node_connections", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); -} - -bool BLTAnimationNodeBlendTree::_get(const StringName &p_name, Variant &r_value) const { - String prop_name = p_name; - if (prop_name.begins_with("nodes/")) { - String node_name = prop_name.get_slicec('/', 1); - String what = prop_name.get_slicec('/', 2); - int node_index = find_node_index_by_name(node_name); - - if (what == "node") { - if (node_index != -1) { - r_value = tree_graph.nodes[node_index]; - return true; - } - } - - if (what == "position") { - if (node_index != -1) { - r_value = tree_graph.nodes[node_index]->position; - return true; - } - } - } else if (prop_name == "node_connections") { - Array conns; - conns.resize(tree_graph.connections.size() * 3); - - int idx = 0; - for (const BLTBlendTreeConnection &connection : tree_graph.connections) { - conns[idx * 3 + 0] = connection.target_node->name; - conns[idx * 3 + 1] = connection.target_node->get_input_index(connection.target_port_name); - conns[idx * 3 + 2] = connection.source_node->name; - idx++; - } - - r_value = conns; - return true; - } - - return false; -} - -bool BLTAnimationNodeBlendTree::_set(const StringName &p_name, const Variant &p_value) { - String prop_name = p_name; - if (prop_name.begins_with("nodes/")) { - String node_name = prop_name.get_slicec('/', 1); - String what = prop_name.get_slicec('/', 2); - - if (what == "node") { - Ref anode = p_value; - if (anode.is_valid()) { - anode->name = node_name; - add_node(anode); - } - return true; - } - - if (what == "position") { - int node_index = find_node_index_by_name(node_name); - if (node_index > -1) { - tree_graph.nodes[node_index]->position = p_value; - } - return true; - } - } else if (prop_name == "node_connections") { - Array conns = p_value; - ERR_FAIL_COND_V(conns.size() % 3 != 0, false); - - for (int i = 0; i < conns.size(); i += 3) { - int target_node_index = find_node_index_by_name(conns[i]); - int target_node_port_index = conns[i + 1]; - int source_node_index = find_node_index_by_name(conns[i + 2]); - - Ref target_node = tree_graph.nodes[target_node_index]; - Vector target_input_names; - target_node->get_input_names(target_input_names); - - add_connection(tree_graph.nodes[source_node_index], target_node, target_input_names[target_node_port_index]); - } - return true; - } - - return false; -} - void AnimationData::sample_from_animation(const Ref &animation, const Skeleton3D *skeleton_3d, double p_time) { GodotProfileZone("AnimationData::sample_from_animation"); - const LocalVector tracks = animation->get_tracks(); + const LocalVector &tracks = animation->get_tracks(); Animation::Track *const *tracks_ptr = tracks.ptr(); int count = tracks.size(); @@ -236,7 +136,7 @@ void AnimationData::allocate_track_value(const Animation::Track *animation_track void AnimationData::allocate_track_values(const Ref &animation, const Skeleton3D *skeleton_3d) { GodotProfileZone("AnimationData::allocate_track_values"); - const LocalVector tracks = animation->get_tracks(); + const LocalVector &tracks = animation->get_tracks(); Animation::Track *const *tracks_ptr = tracks.ptr(); int count = tracks.size(); @@ -254,35 +154,30 @@ void AnimationDataAllocator::register_track_values(const Ref &animati default_data.allocate_track_values(animation, skeleton_3d); } +// +// BLTAnimationNodeSampler +// bool BLTAnimationNodeSampler::initialize(GraphEvaluationContext &context) { - BLTAnimationNode::initialize(context); + if (!BLTAnimationNode::initialize(context)) { + return false; + } - animation = context.animation_player->get_animation(animation_name); - if (!animation.is_valid()) { - print_error(vformat("Cannot initialize node %s: animation '%s' not found in animation player.", name, animation_name)); + animation_player = context.animation_player; + + if (animation_player == nullptr) { + return false; + } + + if (animation_name.is_empty()) { + return false; + } + + if (!set_animation(animation_name)) { return false; } context.animation_data_allocator.register_track_values(animation, context.skeleton_3d); - node_time_info.loop_mode = animation->get_loop_mode(); - - // Initialize Sync Track from marker - LocalVector sync_markers; - int marker_index = 0; - StringName marker_name = itos(marker_index); - while (animation->has_marker(marker_name)) { - sync_markers.push_back(animation->get_marker_time(marker_name)); - marker_index++; - marker_name = itos(marker_index); - } - - if (sync_markers.size() > 0) { - node_time_info.sync_track = SyncTrack::create_from_markers(animation->get_length(), sync_markers); - } else { - node_time_info.sync_track = SyncTrack::create_from_markers(animation->get_length(), { 0 }); - } - return true; } @@ -317,21 +212,134 @@ void BLTAnimationNodeSampler::evaluate(GraphEvaluationContext &context, const Lo output.sample_from_animation(animation, context.skeleton_3d, node_time_info.position); } -void BLTAnimationNodeSampler::set_animation(const StringName &p_name) { +void BLTAnimationNodeSampler::set_animation_player(AnimationPlayer *p_player) { + animation_player = p_player; + _node_changed(); +} + +bool BLTAnimationNodeSampler::set_animation(const StringName &p_name) { + bool has_animation_name_changed = p_name != animation_name; animation_name = p_name; + + if (animation_player == nullptr) { + return false; + } + + if (!animation_player->has_animation(p_name)) { + if (has_animation_name_changed) { + _node_changed(); + } + return false; + } + + animation = animation_player->get_animation(p_name); + if (!animation.is_valid()) { + print_error(vformat("Cannot initialize node %s: animation '%s' not found in animation player.", get_name(), animation_name)); + + _node_changed(); + return false; + } + + node_time_info.loop_mode = animation->get_loop_mode(); + + // Initialize Sync Track from marker + LocalVector sync_markers; + int marker_index = 0; + StringName marker_name = itos(marker_index); + while (animation->has_marker(marker_name)) { + sync_markers.push_back(animation->get_marker_time(marker_name)); + marker_index++; + marker_name = itos(marker_index); + } + + if (sync_markers.size() > 0) { + node_time_info.sync_track = SyncTrack::create_from_markers(animation->get_length(), sync_markers); + } else { + node_time_info.sync_track = SyncTrack::create_from_markers(animation->get_length(), { 0 }); + } + + if (has_animation_name_changed) { + _node_changed(); + } + + return true; } StringName BLTAnimationNodeSampler::get_animation() const { return animation_name; } +AnimationPlayer *BLTAnimationNodeSampler::get_animation_player() const { + return animation_player; +} + +TypedArray BLTAnimationNodeSampler::get_animations_as_typed_array() const { + TypedArray typed_arr; + + if (animation_player == nullptr) { + print_error(vformat("BLTAnimationNodeSampler '%s' not yet initialized", get_name())); + return typed_arr; + } + + Vector vec; + + List animation_libraries; + animation_player->get_animation_library_list(&animation_libraries); + + for (const StringName &library_name : animation_libraries) { + Ref library = animation_player->get_animation_library(library_name); + List animation_list; + library->get_animation_list(&animation_list); + for (const StringName &library_animation : animation_list) { + vec.push_back(library_animation); + } + } + + typed_arr.resize(vec.size()); + for (uint32_t i = 0; i < vec.size(); i++) { + typed_arr[i] = vec[i]; + } + return typed_arr; +} + void BLTAnimationNodeSampler::_bind_methods() { ClassDB::bind_method(D_METHOD("set_animation", "name"), &BLTAnimationNodeSampler::set_animation); ClassDB::bind_method(D_METHOD("get_animation"), &BLTAnimationNodeSampler::get_animation); + ClassDB::bind_method(D_METHOD("get_animation_player"), &BLTAnimationNodeSampler::get_animation_player); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "animation"), "set_animation", "get_animation"); + + ClassDB::bind_method(D_METHOD("get_animations"), &BLTAnimationNodeSampler::get_animations_as_typed_array); } +// +// BLTAnimationNodeTimeScale +// +void BLTAnimationNodeTimeScale::_get_property_list(List *p_list) const { + p_list->push_back(PropertyInfo(Variant::FLOAT, scale_name, PROPERTY_HINT_RANGE, "-10,10,0.01,or_less,or_greater")); +} + +bool BLTAnimationNodeTimeScale::_get(const StringName &p_name, Variant &r_value) const { + if (p_name == scale_name) { + r_value = scale; + return true; + } + + return false; +} + +bool BLTAnimationNodeTimeScale::_set(const StringName &p_name, const Variant &p_value) { + if (p_name == scale_name) { + scale = p_value; + return true; + } + + return false; +} + +// +// BLTAnimationNodeBlend2 +// void BLTAnimationNodeBlend2::evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) { GodotProfileZone("AnimationBlend2Node::evaluate"); @@ -378,7 +386,6 @@ Variant BLTAnimationNodeBlend2::get_parameter_default_value(const StringName &p_ void BLTAnimationNodeBlend2::_get_property_list(List *p_list) const { p_list->push_back(PropertyInfo(Variant::FLOAT, blend_weight_pname, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater")); - p_list->push_back(PropertyInfo(Variant::BOOL, sync_pname)); } bool BLTAnimationNodeBlend2::_get(const StringName &p_name, Variant &r_value) const { @@ -387,11 +394,6 @@ bool BLTAnimationNodeBlend2::_get(const StringName &p_name, Variant &r_value) co return true; } - if (p_name == sync_pname) { - r_value = sync; - return true; - } - return false; } @@ -401,18 +403,16 @@ bool BLTAnimationNodeBlend2::_set(const StringName &p_name, const Variant &p_val return true; } - if (p_name == sync_pname) { - sync = p_value; - return true; - } - return false; } +// +// BLTAnimationNodeBlendTree +// BLTAnimationNodeBlendTree::BLTBlendTreeGraph::BLTBlendTreeGraph() { Ref output_node; output_node.instantiate(); - output_node->name = "Output"; + output_node->set_name("Output"); add_node(output_node); } @@ -421,7 +421,7 @@ Ref BLTAnimationNodeBlendTree::BLTBlendTreeGraph::get_output_n } int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index(const Ref &node) const { - for (int i = 0; i < nodes.size(); i++) { + for (uint32_t i = 0; i < nodes.size(); i++) { if (nodes[i] == node) { return i; } @@ -431,8 +431,8 @@ int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index(const Refname == name) { + for (uint32_t i = 0; i < nodes.size(); i++) { + if (nodes[i]->get_name() == name) { return i; } } @@ -441,32 +441,83 @@ int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index_by_name(const } void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_node(const Ref &node) { - StringName node_base_name = node->name; + StringName node_base_name = node->get_name(); if (node_base_name.is_empty()) { node_base_name = node->get_class_name(); } - node->name = node_base_name; + node->set_name(node_base_name); int number_suffix = 1; - while (find_node_index_by_name(node->name) != -1) { - node->name = vformat("%s %d", node_base_name, number_suffix); + while (find_node_index_by_name(node->get_name()) != -1) { + node->set_name(vformat("%s %d", node_base_name, number_suffix)); number_suffix++; } nodes.push_back(node); - node_connection_info.push_back(NodeConnectionInfo(node.ptr())); + + NodeConnectionInfo connection_info(node.ptr()); + connection_info.input_subtree_node_indices.insert(nodes.size() - 1); + node_connection_info.push_back(connection_info); +} + +bool BLTAnimationNodeBlendTree::BLTBlendTreeGraph::remove_node(const Ref &node) { + if (node == get_output_node()) { + // Output node not allowed to be removed + return false; + } + + int removed_node_index = find_node_index(node); + assert(removed_node_index >= 0); + + // Remove all connections to and from this node + for (int i = static_cast(connections.size()) - 1; i >= 0; i--) { + if (connections[i].source_node == node || connections[i].target_node == node) { + remove_connection(connections[i].source_node, connections[i].target_node, connections[i].target_port_name); + } + } + + // Remove the data directly related to this node + node_connection_info.remove_at(removed_node_index); + nodes.remove_at(removed_node_index); + + // Ensure all indices are cleaned up. + for (NodeConnectionInfo &connection_info : node_connection_info) { + for (unsigned int j = 0; j < connection_info.connected_child_node_index_at_port.size(); j++) { + if (connection_info.connected_child_node_index_at_port[j] > removed_node_index) { + connection_info.connected_child_node_index_at_port[j] = connection_info.connected_child_node_index_at_port[j] - 1; + } + } + + if (connection_info.parent_node_index > removed_node_index) { + connection_info.parent_node_index--; + } + + // Map connected subtrees + HashSet old_indices = connection_info.input_subtree_node_indices; + connection_info.input_subtree_node_indices.clear(); + for (int old_index : old_indices) { + if (old_index > removed_node_index) { + connection_info.input_subtree_node_indices.insert(old_index - 1); + } else { + connection_info.input_subtree_node_indices.insert(old_index); + } + } + } + + return true; } void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::sort_nodes_and_references() { LocalVector sorted_node_indices = get_sorted_node_indices(); - Vector> sorted_nodes; - LocalVector old_node_connection_info = node_connection_info; + LocalVector> sorted_nodes; + LocalVector old_node_connection_info(node_connection_info); for (unsigned int i = 0; i < sorted_node_indices.size(); i++) { int node_index = sorted_node_indices[i]; sorted_nodes.push_back(nodes[node_index]); node_connection_info[i] = old_node_connection_info[node_index]; } + nodes = sorted_nodes; for (NodeConnectionInfo &connection_info : node_connection_info) { @@ -483,6 +534,20 @@ LocalVector BLTAnimationNodeBlendTree::BLTBlendTreeGraph::get_sorted_node_i sort_nodes_recursive(0, result); result.reverse(); + HashSet connected_node_indices; + for (int node_index : result) { + connected_node_indices.insert(node_index); + } + + // Ensure that nodes that are not reachable from the root node are still added to + // the sorted nodes indices. + for (Ref &node : nodes) { + int node_index = find_node_index(node); + if (!connected_node_indices.has(node_index)) { + result.push_back(node_index); + } + } + return result; } @@ -495,18 +560,69 @@ void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::sort_nodes_recursive(int node result.push_back(node_index); } -void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_index_and_update_subtrees_recursive(int node, int node_parent) { - if (node_parent == -1) { +void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_index_and_update_subtrees_recursive(int node_index, int node_parent_index) { + if (node_parent_index == -1) { return; } - node_connection_info[node_parent].input_subtree_node_indices.insert(node); + node_connection_info[node_parent_index].input_subtree_node_indices.insert(node_index); - for (int index : node_connection_info[node].input_subtree_node_indices) { - node_connection_info[node_parent].input_subtree_node_indices.insert(index); + for (int index : node_connection_info[node_index].input_subtree_node_indices) { + node_connection_info[node_parent_index].input_subtree_node_indices.insert(index); } - add_index_and_update_subtrees_recursive(node_parent, node_connection_info[node_parent].parent_node_index); + add_index_and_update_subtrees_recursive(node_parent_index, node_connection_info[node_parent_index].parent_node_index); +} + +void BLTAnimationNodeBlendTree::BLTBlendTreeGraph::remove_subtree_and_update_subtrees_recursive(int node_index, const HashSet &removed_subtree_indices) { + NodeConnectionInfo &connection_info = node_connection_info[node_index]; + + for (int subtree_node_index : removed_subtree_indices) { + connection_info.input_subtree_node_indices.erase(subtree_node_index); + } + + if (connection_info.parent_node_index != -1) { + remove_subtree_and_update_subtrees_recursive(connection_info.parent_node_index, removed_subtree_indices); + } +} + +BLTAnimationNodeBlendTree::ConnectionError BLTAnimationNodeBlendTree::BLTBlendTreeGraph::is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) const { + int source_node_index = find_node_index(source_node); + if (source_node_index == -1) { + print_error("Cannot connect nodes: source node not found."); + return CONNECTION_ERROR_NO_SOURCE_NODE; + } + + if (node_connection_info[source_node_index].parent_node_index != -1) { + print_error("Cannot connect node: source node already has a parent."); + return CONNECTION_ERROR_PARENT_EXISTS; + } + + int target_node_index = find_node_index(target_node); + if (target_node_index == -1) { + print_error("Cannot connect nodes: target node not found."); + return CONNECTION_ERROR_NO_TARGET_NODE; + } + + Vector target_inputs = target_node->get_input_names(); + + if (!target_inputs.has(target_port_name)) { + print_error("Cannot connect nodes: target port not found."); + return CONNECTION_ERROR_TARGET_PORT_NOT_FOUND; + } + + int target_input_port_index = target_node->get_input_index(target_port_name); + if (node_connection_info[target_node_index].connected_child_node_index_at_port[target_input_port_index] != -1) { + print_error("Cannot connect node: target port already connected"); + return CONNECTION_ERROR_TARGET_PORT_ALREADY_CONNECTED; + } + + if (node_connection_info[source_node_index].input_subtree_node_indices.has(target_node_index)) { + print_error("Cannot connect node: connection would create loop."); + return CONNECTION_ERROR_CONNECTION_CREATES_LOOP; + } + + return CONNECTION_OK; } BLTAnimationNodeBlendTree::ConnectionError BLTAnimationNodeBlendTree::BLTBlendTreeGraph::add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { @@ -528,42 +644,154 @@ BLTAnimationNodeBlendTree::ConnectionError BLTAnimationNodeBlendTree::BLTBlendTr return CONNECTION_OK; } -BLTAnimationNodeBlendTree::ConnectionError BLTAnimationNodeBlendTree::BLTBlendTreeGraph::is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) const { +int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_connection_index(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) const { + for (uint32_t i = 0; i < connections.size(); i++) { + if (connections[i].source_node == source_node && connections[i].target_node == target_node && connections[i].target_port_name == target_port_name) { + return i; + } + } + + return -1; +} + +BLTAnimationNodeBlendTree::ConnectionError BLTAnimationNodeBlendTree::BLTBlendTreeGraph::remove_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { int source_node_index = find_node_index(source_node); - if (source_node_index == -1) { - print_error("Cannot connect nodes: source node not found."); - return CONNECTION_ERROR_NO_SOURCE_NODE; - } + NodeConnectionInfo &connection_info = node_connection_info[source_node_index]; - if (node_connection_info[source_node_index].parent_node_index != -1) { - print_error("Cannot connect node: source node already has a parent."); - return CONNECTION_ERROR_PARENT_EXISTS; - } + if (connection_info.parent_node_index != -1) { + NodeConnectionInfo &parent_connection_info = node_connection_info[connection_info.parent_node_index]; + parent_connection_info.input_subtree_node_indices.erase(source_node_index); + parent_connection_info.connected_child_node_index_at_port[target_node->get_input_index(target_port_name)] = -1; - int target_node_index = find_node_index(target_node); - if (target_node_index == -1) { - print_error("Cannot connect nodes: target node not found."); - return CONNECTION_ERROR_NO_TARGET_NODE; - } + remove_subtree_and_update_subtrees_recursive(connection_info.parent_node_index, connection_info.input_subtree_node_indices); - Vector target_inputs; - target_node->get_input_names(target_inputs); + connection_info.parent_node_index = -1; - if (!target_inputs.has(target_port_name)) { - print_error("Cannot connect nodes: target port not found."); - return CONNECTION_ERROR_TARGET_PORT_NOT_FOUND; - } - - int target_input_port_index = target_node->get_input_index(target_port_name); - if (node_connection_info[target_node_index].connected_child_node_index_at_port[target_input_port_index] != -1) { - print_error("Cannot connect node: target port already connected"); - return CONNECTION_ERROR_TARGET_PORT_ALREADY_CONNECTED; - } - - if (node_connection_info[source_node_index].input_subtree_node_indices.has(target_node_index)) { - print_error("Cannot connect node: connection would create loop."); - return CONNECTION_ERROR_CONNECTION_CREATES_LOOP; + uint32_t connection_index = find_connection_index(source_node, target_node, target_port_name); + assert(connection_index >= 0); + connections.remove_at(connection_index); + } else { + return CONNECTION_ERROR_CONNECTION_NOT_FOUND; } return CONNECTION_OK; } + +void BLTAnimationNodeBlendTree::_bind_methods() { + ClassDB::bind_method(D_METHOD("add_node", "animation_node"), &BLTAnimationNodeBlendTree::add_node); + ClassDB::bind_method(D_METHOD("remove_node", "animation_node"), &BLTAnimationNodeBlendTree::remove_node); + ClassDB::bind_method(D_METHOD("get_node", "node_name"), &BLTAnimationNodeBlendTree::get_node); + ClassDB::bind_method(D_METHOD("get_output_node"), &BLTAnimationNodeBlendTree::get_output_node); + ClassDB::bind_method(D_METHOD("get_node_names"), &BLTAnimationNodeBlendTree::get_node_names_as_typed_array); + + ClassDB::bind_method(D_METHOD("is_connection_valid", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::is_connection_valid); + ClassDB::bind_method(D_METHOD("add_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::add_connection); + ClassDB::bind_method(D_METHOD("remove_connection", "source_node", "target_node", "target_port_name"), &BLTAnimationNodeBlendTree::remove_connection); + ClassDB::bind_method(D_METHOD("get_connections"), &BLTAnimationNodeBlendTree::get_connections_as_array); + + ClassDB::bind_method(D_METHOD("set_graph_offset", "graph_offset"), &BLTAnimationNodeBlendTree::set_graph_offset); + ClassDB::bind_method(D_METHOD("get_graph_offset"), &BLTAnimationNodeBlendTree::get_graph_offset); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "graph_offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_graph_offset", "get_graph_offset"); + + BIND_CONSTANT(CONNECTION_OK); + BIND_CONSTANT(CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED); + BIND_CONSTANT(CONNECTION_ERROR_NO_SOURCE_NODE); + BIND_CONSTANT(CONNECTION_ERROR_NO_TARGET_NODE); + BIND_CONSTANT(CONNECTION_ERROR_PARENT_EXISTS); + BIND_CONSTANT(CONNECTION_ERROR_TARGET_PORT_NOT_FOUND); + BIND_CONSTANT(CONNECTION_ERROR_TARGET_PORT_ALREADY_CONNECTED); + BIND_CONSTANT(CONNECTION_ERROR_CONNECTION_CREATES_LOOP); +} + +void BLTAnimationNodeBlendTree::_get_property_list(List *p_list) const { + for (const Ref &node : tree_graph.nodes) { + String prop_name = node->get_name(); + if (prop_name != "Output") { + p_list->push_back(PropertyInfo(Variant::OBJECT, "nodes/" + prop_name + "/node", PROPERTY_HINT_RESOURCE_TYPE, "AnimationNode", PROPERTY_USAGE_NO_EDITOR)); + } + p_list->push_back(PropertyInfo(Variant::VECTOR2, "nodes/" + prop_name + "/graph_offset", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); + } + + p_list->push_back(PropertyInfo(Variant::ARRAY, "node_connections", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); +} + +bool BLTAnimationNodeBlendTree::_get(const StringName &p_name, Variant &r_value) const { + String prop_name = p_name; + if (prop_name.begins_with("nodes/")) { + String node_name = prop_name.get_slicec('/', 1); + String what = prop_name.get_slicec('/', 2); + int node_index = find_node_index_by_name(node_name); + + if (what == "node") { + if (node_index != -1) { + r_value = tree_graph.nodes[node_index]; + return true; + } + } + + if (what == "graph_offset") { + if (node_index != -1) { + r_value = tree_graph.nodes[node_index]->position; + return true; + } + } + } else if (prop_name == "node_connections") { + Array conns; + conns.resize(tree_graph.connections.size() * 3); + + int idx = 0; + for (const BLTBlendTreeConnection &connection : tree_graph.connections) { + conns[idx * 3 + 0] = connection.target_node->get_name(); + conns[idx * 3 + 1] = connection.target_node->get_input_index(connection.target_port_name); + conns[idx * 3 + 2] = connection.source_node->get_name(); + idx++; + } + + r_value = conns; + return true; + } + + return false; +} + +bool BLTAnimationNodeBlendTree::_set(const StringName &p_name, const Variant &p_value) { + String prop_name = p_name; + if (prop_name.begins_with("nodes/")) { + String node_name = prop_name.get_slicec('/', 1); + String what = prop_name.get_slicec('/', 2); + + if (what == "node") { + Ref anode = p_value; + if (anode.is_valid()) { + anode->set_name(node_name); + add_node(anode); + } + return true; + } + + if (what == "graph_offset") { + int node_index = find_node_index_by_name(node_name); + if (node_index > -1) { + tree_graph.nodes[node_index]->position = p_value; + } + return true; + } + } else if (prop_name == "node_connections") { + Array conns = p_value; + ERR_FAIL_COND_V(conns.size() % 3 != 0, false); + + for (int i = 0; i < conns.size(); i += 3) { + int target_node_index = find_node_index_by_name(conns[i]); + int target_node_port_index = conns[i + 1]; + int source_node_index = find_node_index_by_name(conns[i + 2]); + + Ref target_node = tree_graph.nodes[target_node_index]; + Vector target_input_names = target_node->get_input_names(); + + add_connection(tree_graph.nodes[source_node_index], target_node, target_input_names[target_node_port_index]); + } + return true; + } + + return false; +} \ No newline at end of file diff --git a/blendalot_animation_node.h b/blendalot_animation_node.h index a3f4ee7..3672d3f 100644 --- a/blendalot_animation_node.h +++ b/blendalot_animation_node.h @@ -5,6 +5,7 @@ #include "scene/3d/skeleton_3d.h" #include "scene/animation/animation_player.h" +#include "scene/resources/animation_library.h" #include "sync_track.h" #include @@ -104,7 +105,8 @@ struct AnimationData { buffer = other.buffer; } AnimationData(AnimationData &&other) noexcept : - value_buffer_offset(std::exchange(other.value_buffer_offset, AHashMap())), + // We skip copying the offset as that should be identical for all nodes within a BLTAnimationGraph. + // value_buffer_offset(std::exchange(other.value_buffer_offset, AHashMap())), buffer(std::exchange(other.buffer, LocalVector())) { } AnimationData &operator=(const AnimationData &other) { @@ -245,7 +247,7 @@ protected: virtual void set_parameter(const StringName &p_name, const Variant &p_value); virtual Variant get_parameter(const StringName &p_name) const; - virtual void _tree_changed(); + virtual void _node_changed(); virtual void _animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name); virtual void _animation_node_removed(const ObjectID &p_oid, const StringName &p_node); @@ -262,7 +264,6 @@ public: NodeTimeInfo node_time_info; bool active = false; - StringName name; Vector2 position; virtual ~BLTAnimationNode() override = default; @@ -274,6 +275,11 @@ public: virtual void activate_inputs(const Vector> &input_nodes) { // By default, all inputs nodes are activated. for (const Ref &node : input_nodes) { + if (node.ptr() == nullptr) { + // TODO: add checking whether tree can be evaluated, i.e. whether all inputs are properly connected. + continue; + } + node->active = true; node->node_time_info.is_synced = node_time_info.is_synced; } @@ -285,6 +291,7 @@ public: node_time_info.loop_mode = input_nodes[0]->node_time_info.loop_mode; } } + virtual void update_time(double p_time) { if (node_time_info.is_synced) { node_time_info.sync_position = p_time; @@ -293,23 +300,41 @@ public: node_time_info.position += p_time; } } + virtual void evaluate(GraphEvaluationContext &context, const LocalVector &input_datas, AnimationData &output_data) { + GodotProfileZone("AnimationNode::evaluate"); // By default, use the AnimationData of the first input. if (input_datas.size() > 0) { - output_data = *input_datas[0]; + output_data = std::move(*input_datas[0]); } } - virtual void get_input_names(Vector &inputs) const {} + void set_position(const Vector2 &p_position) { + position = p_position; + } + + Vector2 get_position() const { + return position; + } + + virtual Vector get_input_names() const { return {}; } + + TypedArray get_input_names_as_typed_array() const { + TypedArray typed_arr; + Vector vec = get_input_names(); + typed_arr.resize(vec.size()); + for (uint32_t i = 0; i < vec.size(); i++) { + typed_arr[i] = vec[i]; + } + return typed_arr; + } int get_input_index(const StringName &port_name) const { - Vector inputs; - get_input_names(inputs); + Vector inputs = get_input_names(); return inputs.find(port_name); } int get_input_count() const { - Vector inputs; - get_input_names(inputs); + Vector inputs = get_input_names(); return inputs.size(); } @@ -322,9 +347,14 @@ class BLTAnimationNodeSampler : public BLTAnimationNode { public: StringName animation_name; + AnimationPlayer *animation_player = nullptr; - void set_animation(const StringName &p_name); + void set_animation_player(AnimationPlayer *p_player); + bool set_animation(const StringName &p_name); StringName get_animation() const; + AnimationPlayer *get_animation_player() const; + + TypedArray get_animations_as_typed_array() const; private: Ref animation; @@ -337,12 +367,53 @@ protected: static void _bind_methods(); }; +class BLTAnimationNodeTimeScale : public BLTAnimationNode { + GDCLASS(BLTAnimationNodeTimeScale, BLTAnimationNode); + +public: + float scale = 1.0f; + +private: + Ref animation; + + Vector get_input_names() const override { + return { "Input" }; + } + + bool initialize(GraphEvaluationContext &context) override { + node_time_info = {}; + // TODO: it should not be necessary to force looping here. node_time_info.loop_mode = Animation::LOOP_LINEAR; + return true; + } + void calculate_sync_track(const Vector> &input_nodes) override { + if (node_time_info.is_synced) { + node_time_info.sync_track = input_nodes[0]->node_time_info.sync_track; + node_time_info.sync_track.duration *= scale; + } + } + void update_time(double p_time) override { + if (node_time_info.is_synced) { + return; + } + + BLTAnimationNode::update_time(p_time * scale); + } + +protected: + void _get_property_list(List *p_list) const; + bool _get(const StringName &p_name, Variant &r_value) const; + bool _set(const StringName &p_name, const Variant &p_value); + +private: + StringName scale_name = PNAME("scale"); +}; + class BLTAnimationNodeOutput : public BLTAnimationNode { GDCLASS(BLTAnimationNodeOutput, BLTAnimationNode); public: - void get_input_names(Vector &inputs) const override { - inputs.push_back("Input"); + Vector get_input_names() const override { + return { "Output" }; } }; @@ -353,20 +424,26 @@ public: float blend_weight = 0.0f; bool sync = true; - void get_input_names(Vector &inputs) const override { - inputs.push_back("Input0"); - inputs.push_back("Input1"); + Vector get_input_names() const override { + return { "Input0", "Input1" }; } bool initialize(GraphEvaluationContext &context) override { - bool result = BLTAnimationNode::initialize(context); + if (!BLTAnimationNode::initialize(context)) { + return false; + } if (sync) { // TODO: do we always want looping in this case or do we traverse the graph to check what's reasonable? node_time_info.loop_mode = Animation::LOOP_LINEAR; } - return result; + if (node_time_info.loop_mode != Animation::LOOP_LINEAR) { + print_line(vformat("Forcing loop mode to linear on nonde %s", get_name())); + node_time_info.loop_mode = Animation::LOOP_LINEAR; + } + + return true; } void activate_inputs(const Vector> &input_nodes) override { input_nodes[0]->active = true; @@ -379,7 +456,8 @@ public: void calculate_sync_track(const Vector> &input_nodes) override { if (node_time_info.is_synced || sync) { - assert(input_nodes[0]->node_time_info.loop_mode == input_nodes[1]->node_time_info.loop_mode); + // TODO: figure out whether we need to enforce looping mode when syncing is enabled. + // assert(input_nodes[0]->node_time_info.loop_mode == input_nodes[1]->node_time_info.loop_mode); node_time_info.sync_track = SyncTrack::blend(blend_weight, input_nodes[0]->node_time_info.sync_track, input_nodes[1]->node_time_info.sync_track); } } @@ -393,9 +471,9 @@ public: if (!Math::is_zero_approx(node_time_info.sync_track.duration)) { node_time_info.position = Math::fposmod(static_cast(node_time_info.position), node_time_info.sync_track.duration); node_time_info.sync_position = node_time_info.sync_track.calc_sync_from_abs_time(node_time_info.position); - } else { - assert(false && !"Loop mode ping-pong not yet supported"); } + } else { + assert(false && !"Loop mode ping-pong not yet supported"); } } } @@ -423,9 +501,9 @@ private: }; struct BLTBlendTreeConnection { - const Ref source_node = nullptr; - const Ref target_node = nullptr; - const StringName target_port_name = ""; + Ref source_node = nullptr; + Ref target_node = nullptr; + StringName target_port_name = ""; }; class BLTAnimationNodeBlendTree : public BLTAnimationNode { @@ -441,6 +519,7 @@ public: CONNECTION_ERROR_TARGET_PORT_NOT_FOUND, CONNECTION_ERROR_TARGET_PORT_ALREADY_CONNECTED, CONNECTION_ERROR_CONNECTION_CREATES_LOOP, + CONNECTION_ERROR_CONNECTION_NOT_FOUND }; /** @@ -477,32 +556,49 @@ public: } } - void _print_subtree() const; + void _print_subtree() const { + String result = vformat("subtree node indices (%d): ", input_subtree_node_indices.size()); + bool is_first = true; + for (int index : input_subtree_node_indices) { + if (is_first) { + result += vformat("%d", index); + is_first = false; + } else { + result += vformat(", %d", index); + } + } + print_line(result); + } }; - Vector> nodes; // All added nodes + LocalVector> nodes; // All added nodes LocalVector node_connection_info; LocalVector connections; BLTBlendTreeGraph(); Ref get_output_node(); - int find_node_index(const Ref &node) const; int find_node_index_by_name(const StringName &name) const; void sort_nodes_and_references(); LocalVector get_sorted_node_indices(); void sort_nodes_recursive(int node_index, LocalVector &result); - void add_index_and_update_subtrees_recursive(int node, int node_parent); - ConnectionError is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) const; + void add_index_and_update_subtrees_recursive(int node_index, int node_parent_index); + void remove_subtree_and_update_subtrees_recursive(int node, const HashSet &removed_subtree_indices); void add_node(const Ref &node); + bool remove_node(const Ref &node); + + ConnectionError is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) const; ConnectionError add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name); + int find_connection_index(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) const; + ConnectionError remove_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name); }; private: BLTBlendTreeGraph tree_graph; bool tree_initialized = false; + GraphEvaluationContext *_graph_evaluation_context = nullptr; void sort_nodes() { _node_runtime_data.clear(); @@ -511,7 +607,7 @@ private: void setup_runtime_data() { // Add nodes and allocate runtime data - for (int i = 0; i < tree_graph.nodes.size(); i++) { + for (uint32_t i = 0; i < tree_graph.nodes.size(); i++) { const Ref node = tree_graph.nodes[i]; NodeRuntimeData node_runtime_data; @@ -524,13 +620,17 @@ private: } // Populate runtime data (only now is this.nodes populated to retrieve the nodes) - for (int i = 0; i < tree_graph.nodes.size(); i++) { + for (uint32_t i = 0; i < tree_graph.nodes.size(); i++) { Ref node = tree_graph.nodes[i]; NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; for (int port_index = 0; port_index < node->get_input_count(); port_index++) { const int connected_node_index = tree_graph.node_connection_info[i].connected_child_node_index_at_port[port_index]; - node_runtime_data.input_nodes.push_back(tree_graph.nodes[connected_node_index]); + if (connected_node_index == -1) { + node_runtime_data.input_nodes.push_back(nullptr); + } else { + node_runtime_data.input_nodes.push_back(tree_graph.nodes[connected_node_index]); + } } } } @@ -542,6 +642,8 @@ protected: bool _set(const StringName &p_name, const Variant &p_value); public: + Vector2 graph_offset; + struct NodeRuntimeData { Vector> input_nodes; LocalVector input_data; @@ -549,53 +651,144 @@ public: }; LocalVector _node_runtime_data; - Ref get_output_node() const { - return tree_graph.nodes[0]; + void set_graph_offset(const Vector2 &p_graph_offset) { + graph_offset = p_graph_offset; + } + + Vector2 get_graph_offset() const { + return graph_offset; } int find_node_index(const Ref &node) const { return tree_graph.find_node_index(node); } - int find_node_index_by_name(const StringName &name) const { - return tree_graph.find_node_index_by_name(name); + int find_node_index_by_name(const StringName &p_name) const { + return tree_graph.find_node_index_by_name(p_name); } - Ref get_node(int node_index) { - if (node_index < 0 || node_index > tree_graph.nodes.size()) { + void add_node(const Ref &node) { + tree_graph.add_node(node); + + if (_graph_evaluation_context != nullptr) { + node->initialize(*_graph_evaluation_context); + } + } + + void remove_node(const Ref &node) { + if (tree_graph.remove_node(node)) { + _node_changed(); + } + } + + TypedArray get_node_names_as_typed_array() const { + Vector vec; + for (const Ref &node : tree_graph.nodes) { + vec.push_back(node->get_name()); + } + + TypedArray typed_arr; + typed_arr.resize(vec.size()); + for (uint32_t i = 0; i < vec.size(); i++) { + typed_arr[i] = vec[i]; + } + return typed_arr; + } + + Ref get_node(const StringName &node_name) const { + int node_index = tree_graph.find_node_index_by_name(node_name); + + if (node_index >= 0) { + return tree_graph.nodes[node_index]; + } + + return nullptr; + } + + Ref get_node_by_index(int node_index) const { + if (node_index < 0 || node_index > static_cast(tree_graph.nodes.size())) { return nullptr; } return tree_graph.nodes[node_index]; } - void add_node(const Ref &node) { - if (tree_initialized) { - print_error("Cannot add node to BlendTree: BlendTree already initialized."); - return; - } + Ref get_output_node() const { + return tree_graph.nodes[0]; + } - tree_graph.add_node(node); + ConnectionError is_connection_valid(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { + return tree_graph.is_connection_valid(source_node, target_node, target_port_name); } ConnectionError add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { - if (tree_initialized) { - print_error("Cannot add connection to BlendTree: BlendTree already initialized."); - return CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED; + ConnectionError result = tree_graph.add_connection(source_node, target_node, target_port_name); + if (result == CONNECTION_OK) { + _node_changed(); } - return tree_graph.add_connection(source_node, target_node, target_port_name); + return result; } - // overrides from SyncedAnimationNode + ConnectionError remove_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { + ConnectionError result = tree_graph.remove_connection(source_node, target_node, target_port_name); + if (result == CONNECTION_OK) { + _node_changed(); + } + + return result; + } + + Array get_connections_as_array() const { + Array result; + for (const BLTBlendTreeConnection &connection : tree_graph.connections) { + result.push_back(connection.source_node); + result.push_back(connection.target_node); + result.push_back(connection.target_port_name); + } + + return result; + } + + void _tree_node_changed(const StringName &node_name) { + _node_changed(); + } + + // overrides from BLTAnimationNode bool initialize(GraphEvaluationContext &context) override { + tree_initialized = false; + + if (!BLTAnimationNode::initialize(context)) { + return false; + } + + _graph_evaluation_context = &context; + sort_nodes(); setup_runtime_data(); - for (const Ref &node : tree_graph.nodes) { + const HashSet &output_subtree = tree_graph.node_connection_info[0].input_subtree_node_indices; + + for (int i = 0; i < tree_graph.nodes.size(); i++) { + const Ref &node = tree_graph.nodes[i]; + + // Initialize, but skip validation of nodes that are not part of the active tree. + if (!output_subtree.has(i)) { + node->initialize(context); + continue; + } + if (!node->initialize(context)) { return false; } + + const NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; + + for (const Ref &input_node : node_runtime_data.input_nodes) { + if (!input_node.is_valid()) { + return false; + } + } } tree_initialized = true; @@ -607,8 +800,15 @@ public: activate_inputs(const Vector> &input_nodes) override { GodotProfileZone("SyncedBlendTree::activate_inputs"); + // TODO: add checking whether tree can be evaluated, i.e. whether all inputs are properly connected. + if (tree_graph.nodes.size() == 1) { + return; + } + tree_graph.nodes[0]->active = true; - for (int i = 0; i < tree_graph.nodes.size(); i++) { + tree_graph.nodes[0]->node_time_info.is_synced = node_time_info.is_synced; + + for (uint32_t i = 0; i < tree_graph.nodes.size(); i++) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { @@ -622,7 +822,7 @@ public: void calculate_sync_track(const Vector> &input_nodes) override { GodotProfileZone("SyncedBlendTree::calculate_sync_track"); - for (int i = tree_graph.nodes.size() - 1; i > 0; i--) { + for (uint32_t i = tree_graph.nodes.size() - 1; i > 0; i--) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { @@ -632,16 +832,23 @@ public: const NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; node->calculate_sync_track(node_runtime_data.input_nodes); + + if (i == 1) { + node_time_info = node->node_time_info; + } } } void update_time(double p_delta) override { GodotProfileZone("SyncedBlendTree::update_time"); - tree_graph.nodes[0]->node_time_info.delta = p_delta; - tree_graph.nodes[0]->node_time_info.position += p_delta; + BLTAnimationNode::update_time(p_delta); - for (int i = 1; i < tree_graph.nodes.size(); i++) { + tree_graph.nodes[0]->node_time_info.delta = node_time_info.delta; + tree_graph.nodes[0]->node_time_info.position = node_time_info.position; + tree_graph.nodes[0]->node_time_info.sync_position = node_time_info.sync_position; + + for (uint32_t i = 1; i < tree_graph.nodes.size(); i++) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { @@ -659,9 +866,9 @@ public: } void evaluate(GraphEvaluationContext &context, const LocalVector &input_datas, AnimationData &output_data) override { - ZoneScopedN("SyncedBlendTree::evaluate"); + GodotProfileZone("SyncedBlendTree::evaluate"); - for (int i = tree_graph.nodes.size() - 1; i > 0; i--) { + for (uint32_t i = tree_graph.nodes.size() - 1; i > 0; i--) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { @@ -689,6 +896,9 @@ public: for (const int child_index : tree_graph.node_connection_info[i].connected_child_node_index_at_port) { context.animation_data_allocator.free(_node_runtime_data[child_index].output_data); } + + // Node must be deactivated. It'll be activated when actually used next time. + node->active = false; } } diff --git a/blendalot_math_helper.h b/blendalot_math_helper.h new file mode 100644 index 0000000..68c1de0 --- /dev/null +++ b/blendalot_math_helper.h @@ -0,0 +1,22 @@ +// +// Created by martin on 20.02.26. +// + +#ifndef MASTER_BLENDALOT_MATH_HELPER_H +#define MASTER_BLENDALOT_MATH_HELPER_H + +inline int greatest_common_divisor(int a, int b) { + while (b != 0) { + int temp = b; + b = a % b; + a = temp; + } + + return a; +} + +inline int least_common_multiple(int a, int b) { + return (a / greatest_common_divisor(a, b)) * b; +} + +#endif //MASTER_BLENDALOT_MATH_HELPER_H diff --git a/demo/addons/blendalot/animation_graph_editor.gd b/demo/addons/blendalot/animation_graph_editor.gd new file mode 100644 index 0000000..ce7c30e --- /dev/null +++ b/demo/addons/blendalot/animation_graph_editor.gd @@ -0,0 +1,110 @@ +@tool + +extends Control +class_name AnimationGraphEditor + +@onready var breadcrumb_button_container: HBoxContainer = %BreadcrumbButtons +@onready var active_graph_control: Control = %ActiveGraphControl + +var animation_graph:BLTAnimationGraph = null +var animation_graph_root_node:BLTAnimationNode = null +var graph_node_stack:Array[BLTAnimationNode] = [] +var active_graph_edit:Control = null +var active_graph_edit_index = -1 + + +func reset_graph_control(): + for child in active_graph_control.get_children(): + active_graph_control.remove_child(child) + child.queue_free() + + +func edit_animation_root_node(blt_node:BLTAnimationNode): + print("Setting root node") + graph_node_stack = [] + active_graph_edit_index = -1 + truncate_graph_stack(0) + + blt_node.resource_name = "Root" + + if blt_node is BLTAnimationNodeBlendTree: + animation_graph_root_node = blt_node + push_graph_stack(blt_node) + edit_graph(blt_node) + return + + assert(is_instance_valid(animation_graph)) + + push_warning("Cannot edit node %s. Graph type %s not yet supported." % [blt_node.resource_name, blt_node.get_class()]) + + +func push_graph_stack(blt_node:BLTAnimationNode): + graph_node_stack.append(blt_node) + active_graph_edit_index = graph_node_stack.size() - 1 + + var breadcrumb_button:Button = Button.new() + breadcrumb_button_container.add_child(breadcrumb_button) + breadcrumb_button.text = blt_node.resource_name + breadcrumb_button.toggle_mode = true + breadcrumb_button.set_meta("BLTAnimationNode", blt_node) + breadcrumb_button.set_meta("graph_edit_index", active_graph_edit_index) + breadcrumb_button.pressed.connect(on_breadcrumb_button_pressed.bind(active_graph_edit_index)) + + +func truncate_graph_stack(level:int): + graph_node_stack.resize(level) + + var is_above_stack_size = false + for child in breadcrumb_button_container.get_children(): + if is_above_stack_size: + breadcrumb_button_container.remove_child(child) + child.queue_free() + continue + + var button:Button = child as Button + if not is_instance_valid(button): + continue + + if button.get_meta("graph_edit_index") >= graph_node_stack.size(): + is_above_stack_size = true + breadcrumb_button_container.remove_child(child) + child.queue_free() + + +func on_breadcrumb_button_pressed(graph_edit_index:int): + print("on_breadcrumb_button_pressed(%d)" % graph_edit_index) + active_graph_edit_index = graph_edit_index + update_breadcrumb_button_container() + + edit_graph(graph_node_stack[graph_edit_index]) + + +func update_breadcrumb_button_container(): + for child in breadcrumb_button_container.get_children(): + var button:Button = child as Button + if not is_instance_valid(button): + continue + + if button.get_meta("graph_edit_index") == active_graph_edit_index: + button.set_pressed_no_signal(true) + else: + button.set_pressed_no_signal(false) + + +func edit_graph(blt_node:BLTAnimationNode): + if blt_node is BLTAnimationNodeBlendTree: + reset_graph_control() + var blend_tree_graph_edit:BltBlendTreeEditor = preload ("res://addons/blendalot/blend_tree_editor.tscn").instantiate() + active_graph_control.add_child(blend_tree_graph_edit) + blend_tree_graph_edit.edit_blend_tree(blt_node) + blend_tree_graph_edit.edit_subgraph.connect(handle_subgraph_edit) + active_graph_edit = blend_tree_graph_edit + else: + push_error("Cannot edit graph of node type %s" % blt_node.get_class()) + + +func handle_subgraph_edit(blt_node:BLTAnimationNode): + print("handling subgraph edit of node %s" % blt_node.resource_name) + truncate_graph_stack(active_graph_edit_index + 1) + push_graph_stack(blt_node) + edit_graph(blt_node) diff --git a/demo/addons/blendalot/animation_graph_editor.gd.uid b/demo/addons/blendalot/animation_graph_editor.gd.uid new file mode 100644 index 0000000..027e630 --- /dev/null +++ b/demo/addons/blendalot/animation_graph_editor.gd.uid @@ -0,0 +1 @@ +uid://bxxipuj2s5gxu diff --git a/demo/addons/blendalot/animation_graph_editor.tscn b/demo/addons/blendalot/animation_graph_editor.tscn new file mode 100644 index 0000000..13ab95f --- /dev/null +++ b/demo/addons/blendalot/animation_graph_editor.tscn @@ -0,0 +1,83 @@ +[gd_scene format=3 uid="uid://bk5mssvanwjnh"] + +[ext_resource type="Script" uid="uid://bxxipuj2s5gxu" path="res://addons/blendalot/animation_graph_editor.gd" id="1_un1ur"] +[ext_resource type="PackedScene" uid="uid://cptd46rpm0gl3" path="res://addons/blendalot/blend_tree_editor.tscn" id="2_utax0"] + +[node name="AnimationGraphEditor" type="VBoxContainer" unique_id=768619585] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_un1ur") + +[node name="DebugContainer" type="HBoxContainer" parent="." unique_id=1984631897] +visible = false +layout_mode = 2 + +[node name="ResetGraphButton" type="Button" parent="DebugContainer" unique_id=670074781] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Reset" + +[node name="FileNameLineEdit" type="LineEdit" parent="DebugContainer" unique_id=724929522] +unique_name_in_owner = true +custom_minimum_size = Vector2(250, 0) +layout_mode = 2 +text = "editor_test_tree.tres" + +[node name="SaveButton" type="Button" parent="DebugContainer" unique_id=706843675] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Save +" + +[node name="LoadButton" type="Button" parent="DebugContainer" unique_id=1467831200] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Load +" + +[node name="ReinitializeButton" type="Button" parent="DebugContainer" unique_id=281924859] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Reinitialize" + +[node name="TreeOptionButton" type="OptionButton" parent="DebugContainer" unique_id=2103827540] +unique_name_in_owner = true +layout_mode = 2 +selected = 0 +item_count = 2 +popup/item_0/text = "AnimationSampler" +popup/item_0/id = 1 +popup/item_1/text = "Blend2" +popup/item_1/id = 2 + +[node name="InstantiateTreeButton" type="Button" parent="DebugContainer" unique_id=735069321] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Instantiate" + +[node name="NavigationBar" type="MarginContainer" parent="." unique_id=815609909] +layout_mode = 2 + +[node name="Panel" type="Panel" parent="NavigationBar" unique_id=996737763] +layout_mode = 2 + +[node name="BreadcrumbButtons" type="HBoxContainer" parent="NavigationBar" unique_id=1375619232] +unique_name_in_owner = true +layout_mode = 2 + +[node name="PathLabel" type="Label" parent="NavigationBar/BreadcrumbButtons" unique_id=1544570774] +layout_mode = 2 +text = "Path:" + +[node name="ActiveGraphControl" type="Control" parent="." unique_id=1769528277] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="BlendTreeEditor" parent="ActiveGraphControl" unique_id=1313738200 instance=ExtResource("2_utax0")] +layout_mode = 1 diff --git a/demo/addons/blendalot/blend_tree_editor.gd b/demo/addons/blendalot/blend_tree_editor.gd new file mode 100644 index 0000000..e1f1b34 --- /dev/null +++ b/demo/addons/blendalot/blend_tree_editor.gd @@ -0,0 +1,275 @@ +@tool + +extends Control +class_name BltBlendTreeEditor + +@onready var blend_tree_graph_edit: GraphEdit = %BlendTreeGraphEdit +@onready var add_node_popup_menu: PopupMenu = %AddNodePopupMenu + +signal edit_subgraph(blt_node:BLTAnimationNode) +signal graph_changed() + +var blend_tree:BLTAnimationNodeBlendTree + +var blend_tree_node_to_graph_node = {} +var graph_node_to_blend_tree_node = {} + +var selected_nodes = {} +var last_selected_graph_node:GraphNode = null +var new_node_position:Vector2 = Vector2.ZERO + +var registered_nodes = [ + "BLTAnimationNodeSampler", + "BLTAnimationNodeBlend2", + "BLTAnimationNodeBlendTree", + ] + + +func _ready() -> void: + add_node_popup_menu.clear(true) + + for node_name in registered_nodes: + add_node_popup_menu.add_item(node_name) + + +func _reset_editor(): + for child in blend_tree_graph_edit.get_children(): + if child.name == "_connection_layer": + continue + + child.get_parent().remove_child(child) + child.queue_free() + + blend_tree_graph_edit.clear_connections() + + blend_tree = null + blend_tree_node_to_graph_node = {} + graph_node_to_blend_tree_node = {} + selected_nodes = {} + + +func edit_blend_tree(blt_blend_tree:BLTAnimationNodeBlendTree): + _reset_editor() + blend_tree = blt_blend_tree + blend_tree_graph_edit.scroll_offset = blend_tree.graph_offset + + _update_editor_nodes_from_blend_tree() + _update_editor_connections_from_blend_tree() + + +func _update_editor_nodes_from_blend_tree(): + for node_name in blend_tree.get_node_names(): + var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) + var graph_node:GraphNode = create_graph_node_for_blt_node(blend_tree_node) + blend_tree_graph_edit.add_child(graph_node) + + blend_tree_node_to_graph_node[blend_tree_node] = graph_node + graph_node_to_blend_tree_node[graph_node] = blend_tree_node + + +func _update_editor_connections_from_blend_tree(): + var connection_array = blend_tree.get_connections() + + for i in range(len(connection_array) / 3): + var source_node:BLTAnimationNode = connection_array[i * 3] + var target_node:BLTAnimationNode = connection_array[i * 3 + 1] + var target_port = connection_array[i * 3 + 2] + + var source_graph_node = blend_tree_node_to_graph_node[source_node] + + var connect_result = blend_tree_graph_edit.connect_node(source_node.resource_name, 0, target_node.resource_name, target_node.get_input_index(target_port), true) + + +func create_graph_node_for_blt_node(blt_node: BLTAnimationNode) -> GraphNode: + var result_graph_node:GraphNode = GraphNode.new() + result_graph_node.name = blt_node.resource_name + result_graph_node.title = blt_node.resource_name + result_graph_node.position_offset = blt_node.position + + var result_slot_offset = 0 + + if (blt_node.get_class() != "BLTAnimationNodeOutput"): + result_slot_offset = 1 + var output_slot_label:Label = Label.new() + output_slot_label.text = "Result" + result_graph_node.add_child(output_slot_label) + result_graph_node.set_slot(0, false, 1, Color.WHITE, true, 1, Color.WHITE) + + if blt_node.get_class() == "BLTAnimationNodeBlendTree": + result_graph_node.gui_input.connect(_on_node_gui_input.bind(result_graph_node)) + + var inputs = blt_node.get_input_names() + for i in range(len(inputs)): + var slot_label:Label = Label.new() + slot_label.text = inputs[i] + result_graph_node.add_child(slot_label) + result_graph_node.set_slot(i + result_slot_offset, true, 1, Color.WHITE, false, 1, Color.BLACK) + + if blt_node.get_class() == "BLTAnimationNodeSampler": + var animation_sampler_node:BLTAnimationNodeSampler = blt_node as BLTAnimationNodeSampler + var animation_selector_button = OptionButton.new() + var animation_player:AnimationPlayer = animation_sampler_node.get_animation_player() + for animation_name in animation_player.get_animation_list(): + animation_selector_button.add_item(animation_name) + if animation_name == animation_sampler_node.animation: + animation_selector_button.select(animation_selector_button.item_count - 1) + + animation_selector_button.item_selected.connect(_on_animation_select.bind(animation_sampler_node, animation_selector_button)) + + result_graph_node.add_child(animation_selector_button) + + blt_node.node_changed.connect(_trigger_graph_changed) + + return result_graph_node + + +func _trigger_graph_changed(_node_name): + graph_changed.emit() + + +func _remove_node_connections(graph_node:GraphNode): + var node_connections:Array = [] + + for connection:Dictionary in blend_tree_graph_edit.connections: + if connection["from_node"] == graph_node.name or connection["to_node"] == graph_node.name: + node_connections.append(connection) + + for node_connection:Dictionary in node_connections: + print("Removing connection %s" % str(node_connection)) + blend_tree_graph_edit.disconnect_node(node_connection["from_node"], node_connection["from_port"], node_connection["to_node"], node_connection["to_port"]) + + +# +# GraphEdit signal handling +# +func _on_blend_tree_graph_edit_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void: + print("Trying to connect '%s' port %d to node '%s' port %d" % [from_node, from_port, to_node, to_port]) + + var source_node:BLTAnimationNode = blend_tree.get_node(from_node) + var target_node:BLTAnimationNode = blend_tree.get_node(to_node) + + if target_node == null: + push_error("Invalid connection, target node %s not found." % to_node) + return + + var target_node_port_name = target_node.get_input_names()[to_port] + + var connection_result = blend_tree.is_connection_valid(source_node, target_node, target_node_port_name) + if connection_result != blend_tree.CONNECTION_OK: + push_error("Could not add connection (error %d)" % connection_result) + return + + blend_tree.add_connection(source_node, target_node, target_node_port_name) + + var connect_result = blend_tree_graph_edit.connect_node(from_node, from_port, to_node, to_port, true) + print("graph connect result: " + str(connect_result)) + + print("Success!") + + +func _on_blend_tree_graph_edit_disconnection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void: + var blend_tree_source_node = blend_tree.get_node(from_node) + var blend_tree_target_node = blend_tree.get_node(to_node) + var target_port_name = blend_tree_target_node.get_input_names()[to_port] + blend_tree.remove_connection(blend_tree_source_node, blend_tree_target_node, target_port_name) + + blend_tree_graph_edit.disconnect_node(from_node, from_port, to_node, to_port) + + +func _on_blend_tree_graph_edit_delete_nodes_request(nodes: Array[StringName]) -> void: + for node_name:StringName in nodes: + print("remove node '%s'" % node_name) + var blend_tree_node:BLTAnimationNode = blend_tree.get_node(node_name) + + if blend_tree_node == null: + push_error("Cannot delete node '%s': node not found." % node_name) + continue + + if blend_tree_node == blend_tree.get_output_node(): + push_warning("Output node not allowed to be removed.") + continue + + blend_tree_node.node_changed.disconnect(_trigger_graph_changed) + + var graph_node:GraphNode = blend_tree_node_to_graph_node[blend_tree_node] + blend_tree.remove_node(blend_tree_node) + blend_tree_node_to_graph_node.erase(blend_tree_node) + + _remove_node_connections(graph_node) + graph_node_to_blend_tree_node.erase(graph_node) + blend_tree_graph_edit.remove_child(graph_node) + _on_blend_tree_graph_edit_node_deselected(graph_node) + + EditorInterface.get_inspector().edit(null) + + +func _on_blend_tree_graph_edit_end_node_move() -> void: + for graph_node:GraphNode in selected_nodes.keys(): + graph_node_to_blend_tree_node[graph_node].position = graph_node.position_offset + + +func _on_blend_tree_graph_edit_node_deselected(graph_node: Node) -> void: + if selected_nodes.has(graph_node): + selected_nodes.erase(graph_node) + + +func _on_blend_tree_graph_edit_node_selected(graph_node: Node) -> void: + selected_nodes[graph_node] = graph_node + last_selected_graph_node = graph_node + EditorInterface.get_inspector().edit(graph_node_to_blend_tree_node[graph_node]) + + +func _on_blend_tree_graph_edit_scroll_offset_changed(offset: Vector2) -> void: + if is_instance_valid(blend_tree): + blend_tree.graph_offset = offset + + +# +# AddNodePopupMenu +# +func _on_blend_tree_graph_edit_popup_request(at_position: Vector2) -> void: + add_node_popup_menu.position = get_screen_position() + get_local_mouse_position() + add_node_popup_menu.reset_size() + add_node_popup_menu.popup() + new_node_position = blend_tree_graph_edit.scroll_offset + at_position + + +func _on_add_node_popup_menu_index_pressed(index: int) -> void: + var new_blend_tree_node: BLTAnimationNode = ClassDB.instantiate(registered_nodes[index]) + blend_tree.add_node(new_blend_tree_node) + + var graph_node:GraphNode = create_graph_node_for_blt_node(new_blend_tree_node) + blend_tree_graph_edit.add_child(graph_node) + + graph_node_to_blend_tree_node[graph_node] = new_blend_tree_node + blend_tree_node_to_graph_node[new_blend_tree_node] = graph_node + + if new_node_position != Vector2.INF: + graph_node.position_offset = new_node_position + new_blend_tree_node.position = new_node_position + + new_node_position = Vector2.INF + + +# +# Handle Node double click +# +func _on_node_gui_input(input_event:InputEvent, graph_node:GraphNode): + # print("Got input event on graph node %s!" % graph_node.name) + + var mouse_button_event:InputEventMouseButton = input_event as InputEventMouseButton + if mouse_button_event and mouse_button_event.double_click: + _on_node_double_click(graph_node) + +func _on_node_double_click(graph_node:GraphNode): + var blend_tree_node:BLTAnimationNode = graph_node_to_blend_tree_node[graph_node] + + if blend_tree_node is BLTAnimationNodeBlendTree: + edit_subgraph.emit(blend_tree_node) + +# +# Animation selection for BltAnimationNodeSampler +# +func _on_animation_select(index:int, blt_node_sampler:BLTAnimationNodeSampler, option_button:OptionButton): + blt_node_sampler.animation = option_button.get_item_text(index) + blt_node_sampler.node_changed.emit(blt_node_sampler.resource_name) diff --git a/demo/addons/blendalot/blend_tree_editor.gd.uid b/demo/addons/blendalot/blend_tree_editor.gd.uid new file mode 100644 index 0000000..c2a2a36 --- /dev/null +++ b/demo/addons/blendalot/blend_tree_editor.gd.uid @@ -0,0 +1 @@ +uid://dr0ndqekm21gy diff --git a/demo/addons/blendalot/blend_tree_editor.tscn b/demo/addons/blendalot/blend_tree_editor.tscn new file mode 100644 index 0000000..c00b0a8 --- /dev/null +++ b/demo/addons/blendalot/blend_tree_editor.tscn @@ -0,0 +1,52 @@ +[gd_scene format=3 uid="uid://cptd46rpm0gl3"] + +[ext_resource type="Script" uid="uid://dr0ndqekm21gy" path="res://addons/blendalot/blend_tree_editor.gd" id="1_0srhh"] + +[node name="BlendTreeEditor" type="Control" unique_id=1313738200] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_0srhh") + +[node name="Panel" type="Panel" parent="." unique_id=758924321] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 + +[node name="AddNodePopupMenu" type="PopupMenu" parent="Panel" unique_id=108570539] +unique_name_in_owner = true +oversampling_override = 1.0 +item_count = 3 +item_0/text = "BLTAnimationNodeSampler" +item_0/id = 0 +item_1/text = "BLTAnimationNodeBlend2" +item_1/id = 1 +item_2/text = "BLTAnimationNodeBlendTree" +item_2/id = 2 + +[node name="BlendTreeGraphEdit" type="GraphEdit" parent="Panel" unique_id=391120290] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +right_disconnects = true + +[connection signal="index_pressed" from="Panel/AddNodePopupMenu" to="." method="_on_add_node_popup_menu_index_pressed"] +[connection signal="connection_request" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_connection_request"] +[connection signal="delete_nodes_request" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_delete_nodes_request"] +[connection signal="disconnection_request" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_disconnection_request"] +[connection signal="end_node_move" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_end_node_move"] +[connection signal="node_deselected" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_deselected"] +[connection signal="node_selected" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_node_selected"] +[connection signal="popup_request" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_popup_request"] +[connection signal="scroll_offset_changed" from="Panel/BlendTreeGraphEdit" to="." method="_on_blend_tree_graph_edit_scroll_offset_changed"] diff --git a/demo/addons/blendalot/blendalot_main_panel.gd b/demo/addons/blendalot/blendalot_main_panel.gd deleted file mode 100644 index 46e7bdf..0000000 --- a/demo/addons/blendalot/blendalot_main_panel.gd +++ /dev/null @@ -1,16 +0,0 @@ -@tool -extends Control - - -# Called when the node enters the scene tree for the first time. -func _ready() -> void: - pass # Replace with function body. - - -# Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta: float) -> void: - pass - - -func _on_hit_me_button_pressed() -> void: - print("Hello from the main screen plugin!") diff --git a/demo/addons/blendalot/blendalot_main_panel.gd.uid b/demo/addons/blendalot/blendalot_main_panel.gd.uid deleted file mode 100644 index 40b1784..0000000 --- a/demo/addons/blendalot/blendalot_main_panel.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dvulvuytt81lw diff --git a/demo/addons/blendalot/blendalot_main_panel.tscn b/demo/addons/blendalot/blendalot_main_panel.tscn deleted file mode 100644 index 796e64c..0000000 --- a/demo/addons/blendalot/blendalot_main_panel.tscn +++ /dev/null @@ -1,33 +0,0 @@ -[gd_scene load_steps=2 format=3 uid="uid://31c6depvs0y1"] - -[ext_resource type="Script" uid="uid://dvulvuytt81lw" path="res://addons/blendalot/blendalot_main_panel.gd" id="1_427jg"] - -[node name="BlendalotMainPanel" type="Control" unique_id=1259518158] -layout_mode = 3 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 -script = ExtResource("1_427jg") - -[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=2044593527] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="HitMeButton" type="Button" parent="VBoxContainer" unique_id=1060776498] -layout_mode = 2 -size_flags_horizontal = 0 -text = "Hit me!" - -[node name="GraphFrame" type="GraphFrame" parent="VBoxContainer" unique_id=184673233] -layout_mode = 2 -size_flags_vertical = 3 - -[connection signal="pressed" from="VBoxContainer/HitMeButton" to="." method="_on_hit_me_button_pressed"] diff --git a/demo/addons/blendalot/blendalot_plugin.gd b/demo/addons/blendalot/blendalot_plugin.gd index 47c4336..b58122f 100644 --- a/demo/addons/blendalot/blendalot_plugin.gd +++ b/demo/addons/blendalot/blendalot_plugin.gd @@ -1,9 +1,8 @@ @tool extends EditorPlugin -const MainPanel = preload("res://addons/blendalot/blendalot_main_panel.tscn") - -var main_panel_instance +var editor_dock:EditorDock = null +var animation_graph_editor:AnimationGraphEditor = null func _enable_plugin() -> void: # Add autoloads here. @@ -16,25 +15,29 @@ func _disable_plugin() -> void: func _enter_tree() -> void: - main_panel_instance = MainPanel.instantiate() - # Add the main panel to the editor's main viewport. - EditorInterface.get_editor_main_screen().add_child(main_panel_instance) - # Hide the main panel. Very much required. - _make_visible(false) + editor_dock = EditorDock.new() + editor_dock.title = "Animation Graph" + editor_dock.default_slot = EditorDock.DOCK_SLOT_BOTTOM + animation_graph_editor = preload ("res://addons/blendalot/animation_graph_editor.tscn").instantiate() + editor_dock.add_child(animation_graph_editor) + add_dock(editor_dock) func _exit_tree() -> void: - if main_panel_instance: - main_panel_instance.queue_free() + remove_dock(editor_dock) + editor_dock.queue_free() + editor_dock = null + + animation_graph_editor.queue_free() + animation_graph_editor = null -func _has_main_screen(): - return true +func _has_main_screen() -> bool: + return false func _make_visible(visible): - if main_panel_instance: - main_panel_instance.visible = visible + pass func _get_plugin_name(): @@ -43,3 +46,19 @@ func _get_plugin_name(): func _get_plugin_icon(): return EditorInterface.get_editor_theme().get_icon("Node", "EditorIcons") + + +func _handles(obj: Object) -> bool: + return obj is BLTAnimationNodeBlendTree + + +func _edit(object: Object): + if not is_instance_valid(animation_graph_editor): + push_error("Cannot edit object as AnimationGraphEditor is not initialized") + return + + if object is BLTAnimationNodeBlendTree: + animation_graph_editor.edit_animation_root_node(object) + return + + print("Cannot (yet) edit object " + str(object)) diff --git a/demo/animation_tree_walk_limp.tres b/demo/animation_tree_walk_limp.tres index a41f2f6..82e9dd0 100644 --- a/demo/animation_tree_walk_limp.tres +++ b/demo/animation_tree_walk_limp.tres @@ -1,4 +1,4 @@ -[gd_resource type="AnimationNodeBlendTree" load_steps=4 format=3 uid="uid://dqy0dgwsm8t46"] +[gd_resource type="AnimationNodeBlendTree" format=3 uid="uid://dqy0dgwsm8t46"] [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_h2yge"] animation = &"Limping-InPlace" diff --git a/demo/animation_tree_walk_run.tres b/demo/animation_tree_walk_run.tres index f8e0b34..bf53c65 100644 --- a/demo/animation_tree_walk_run.tres +++ b/demo/animation_tree_walk_run.tres @@ -1,4 +1,4 @@ -[gd_resource type="AnimationNodeBlendTree" load_steps=4 format=3 uid="uid://vsf71o82lkld"] +[gd_resource type="AnimationNodeBlendTree" format=3 uid="uid://vsf71o82lkld"] [sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_h2yge"] animation = &"Run-InPlace" @@ -8,7 +8,28 @@ animation = &"Walk-InPlace" [sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_lquwl"] +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_vyt75"] + +[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_hom0r"] + +[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_vyt75"] + +[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_1rfsi"] +nodes/BlendTree/node = SubResource("AnimationNodeBlendTree_vyt75") +nodes/BlendTree/position = Vector2(694.9995, 215.55058) + +[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_8tpve"] +graph_offset = Vector2(-782, 179.47485) +nodes/Animation/node = SubResource("AnimationNodeAnimation_vyt75") +nodes/Animation/position = Vector2(-320, 140) +nodes/Blend2/node = SubResource("AnimationNodeBlend2_hom0r") +nodes/Blend2/position = Vector2(-115.66393, 127.37674) +nodes/BlendTree/node = SubResource("AnimationNodeBlendTree_1rfsi") +nodes/BlendTree/position = Vector2(-480, 400) +node_connections = [&"output", 0, &"Blend2", &"Blend2", 0, &"Animation", &"Blend2", 1, &"BlendTree"] + [resource] +graph_offset = Vector2(-217.4643, 82.84979) nodes/output/position = Vector2(540, 140) nodes/Animation/node = SubResource("AnimationNodeAnimation_1bvp3") nodes/Animation/position = Vector2(120, 80) @@ -16,4 +37,6 @@ nodes/Animation/position = Vector2(120, 80) "nodes/Animation 2/position" = Vector2(80, 320) nodes/Blend2/node = SubResource("AnimationNodeBlend2_lquwl") nodes/Blend2/position = Vector2(360, 180) +nodes/BlendTree/node = SubResource("AnimationNodeBlendTree_8tpve") +nodes/BlendTree/position = Vector2(778.0867, 295.33868) node_connections = [&"output", 0, &"Blend2", &"Blend2", 0, &"Animation", &"Blend2", 1, &"Animation 2"] diff --git a/demo/main.gd b/demo/main.gd index 9934242..d4963cc 100644 --- a/demo/main.gd +++ b/demo/main.gd @@ -12,19 +12,6 @@ extends Node3D func _ready() -> void: blend_weight_slider.value = 0.5 - var blend_tree: BLTAnimationNodeBlendTree = BLTAnimationNodeBlendTree.new() - var output_node: BLTAnimationNodeOutput = blend_tree.get_output_node() - var sampler_node_1: BLTAnimationNodeSampler = BLTAnimationNodeSampler.new() - - sampler_node_1.animation = "animation_library/Walk-InPlace" - - blend_tree.add_node(sampler_node_1) - var result = blend_tree.add_connection(sampler_node_1, output_node, "Input") - var anim_graph: BLTAnimationGraph = mixamo_amy_walk_run_synced.get_node("SyncedAnimationGraph") - - anim_graph.tree_root = blend_tree - - # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta: float) -> void: pass diff --git a/demo/main.tscn b/demo/main.tscn index 12222c3..9e8744d 100644 --- a/demo/main.tscn +++ b/demo/main.tscn @@ -1,12 +1,11 @@ -[gd_scene load_steps=14 format=3 uid="uid://svj53e2xoio"] +[gd_scene format=3 uid="uid://svj53e2xoio"] [ext_resource type="PackedScene" uid="uid://d1xcqdqr1qeu6" path="res://assets/MixamoAmy.glb" id="1_0xm2m"] [ext_resource type="Script" uid="uid://bjvgqujpqumj7" path="res://main.gd" id="1_1bvp3"] [ext_resource type="AnimationLibrary" uid="uid://dwubn740aqx51" path="res://animation_library.res" id="3_1bvp3"] [ext_resource type="AnimationNodeBlendTree" uid="uid://dqy0dgwsm8t46" path="res://animation_tree_walk_limp.tres" id="3_272bh"] -[ext_resource type="BLTAnimationNodeBlendTree" uid="uid://2qfwr1xkiw0s" path="res://synced_blend_tree_walk_limp.tres" id="4_lquwl"] -[ext_resource type="BLTAnimationNodeBlendTree" uid="uid://qsk64ax2o47f" path="res://synced_blend_tree_walk_run.tres" id="5_7mycd"] [ext_resource type="AnimationNodeBlendTree" uid="uid://vsf71o82lkld" path="res://animation_tree_walk_run.tres" id="6_5vw27"] +[ext_resource type="BLTAnimationNodeBlendTree" uid="uid://2qfwr1xkiw0s" path="res://synced_blend_tree_walk_limp.tres" id="6_272bh"] [sub_resource type="Theme" id="Theme_272bh"] default_font_size = 30 @@ -32,6 +31,40 @@ sky = SubResource("Sky_1bvp3") tonemap_mode = 2 glow_enabled = true +[sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_7mycd"] +resource_name = "BLTAnimationNodeBlend2" +position = Vector2(-320, -40) +blend_amount = 0.81 + +[sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_272bh"] +resource_name = "BLTAnimationNodeSampler" +position = Vector2(-490, 7) +animation = &"animation_library/Walk-InPlace" + +[sub_resource type="BLTAnimationNodeBlendTree" id="BLTAnimationNodeBlendTree_5vw27"] +resource_name = "BLTAnimationNodeBlendTree" +position = Vector2(-640, -20) +graph_offset = Vector2(-760.67163, -24.823944) +nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_272bh") +nodes/BLTAnimationNodeSampler/graph_offset = Vector2(-490, 7) +node_connections = ["Output", 0, "BLTAnimationNodeSampler"] + +[sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_kek77"] +resource_name = "BLTAnimationNodeSampler" +position = Vector2(-620, 140) +animation = &"animation_library/Run-InPlace" + +[sub_resource type="BLTAnimationNodeBlendTree" id="BLTAnimationNodeBlendTree_7mycd"] +resource_name = "Root" +graph_offset = Vector2(-869, -71) +nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_7mycd") +nodes/BLTAnimationNodeBlend2/graph_offset = Vector2(-320, -40) +nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_kek77") +nodes/BLTAnimationNodeSampler/graph_offset = Vector2(-620, 140) +nodes/BLTAnimationNodeBlendTree/node = SubResource("BLTAnimationNodeBlendTree_5vw27") +nodes/BLTAnimationNodeBlendTree/graph_offset = Vector2(-640, -20) +node_connections = ["Output", 0, "BLTAnimationNodeBlend2", "BLTAnimationNodeBlend2", 0, "BLTAnimationNodeBlendTree", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler"] + [node name="Main" type="Node3D" unique_id=933302313] script = ExtResource("1_1bvp3") @@ -137,6 +170,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.6, 0, 0) tree_root = ExtResource("6_5vw27") anim_player = NodePath("../AnimationPlayer") parameters/Blend2/blend_amount = 0.0 +parameters/BlendTree/Blend2/blend_amount = 0.0 [node name="MixamoAmyWalkLimpSynced" parent="Characters" unique_id=1018815116 instance=ExtResource("1_0xm2m")] unique_name_in_owner = true @@ -147,22 +181,145 @@ libraries/animation_library = ExtResource("3_1bvp3") [node name="SyncedAnimationGraph" type="BLTAnimationGraph" parent="Characters/MixamoAmyWalkLimpSynced" unique_id=1866796918] animation_player = NodePath("../AnimationPlayer2") -tree_root = ExtResource("4_lquwl") +tree_root = ExtResource("6_272bh") skeleton = NodePath("../Armature/Skeleton3D") -parameters/BLTAnimationNodeBlend2/blend_amount = 0.4 +parameters/BLTAnimationNodeBlend2/blend_amount = 1.0 [node name="MixamoAmyWalkRunSynced" parent="Characters" unique_id=2088190993 instance=ExtResource("1_0xm2m")] unique_name_in_owner = true transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 0, 0) +[node name="Skeleton3D" parent="Characters/MixamoAmyWalkRunSynced/Armature" parent_id_path=PackedInt32Array(2088190993, 1791722621) index="0" unique_id=1831928682] +bones/2/position = Vector3(0, 0, 0) +bones/2/rotation = Quaternion(0, 0, 0, 1) +bones/3/position = Vector3(0, 0, 0) +bones/3/rotation = Quaternion(0, 0, 0, 1) +bones/4/position = Vector3(0, 0, 0) +bones/4/rotation = Quaternion(0, 0, 0, 1) +bones/5/position = Vector3(0, 0, 0) +bones/5/rotation = Quaternion(0, 0, 0, 1) +bones/6/position = Vector3(0, 0, 0) +bones/6/rotation = Quaternion(0, 0, 0, 1) +bones/7/position = Vector3(0, 0, 0) +bones/7/rotation = Quaternion(0, 0, 0, 1) +bones/8/position = Vector3(0, 0, 0) +bones/9/position = Vector3(0, 0, 0) +bones/9/rotation = Quaternion(0, 0, 0, 1) +bones/10/position = Vector3(0, 0, 0) +bones/10/rotation = Quaternion(0, 0, 0, 1) +bones/11/position = Vector3(0, 0, 0) +bones/11/rotation = Quaternion(0, 0, 0, 1) +bones/12/position = Vector3(0, 0, 0) +bones/12/rotation = Quaternion(0, 0, 0, 1) +bones/13/position = Vector3(0, 0, 0) +bones/13/rotation = Quaternion(0, 0, 0, 1) +bones/14/position = Vector3(0, 0, 0) +bones/14/rotation = Quaternion(0, 0, 0, 1) +bones/15/position = Vector3(0, 0, 0) +bones/15/rotation = Quaternion(0, 0, 0, 1) +bones/16/position = Vector3(0, 0, 0) +bones/17/position = Vector3(0, 0, 0) +bones/17/rotation = Quaternion(0, 0, 0, 1) +bones/18/position = Vector3(0, 0, 0) +bones/18/rotation = Quaternion(0, 0, 0, 1) +bones/19/position = Vector3(0, 0, 0) +bones/19/rotation = Quaternion(0, 0, 0, 1) +bones/20/position = Vector3(0, 0, 0) +bones/21/position = Vector3(0, 0, 0) +bones/21/rotation = Quaternion(0, 0, 0, 1) +bones/22/position = Vector3(0, 0, 0) +bones/22/rotation = Quaternion(0, 0, 0, 1) +bones/23/position = Vector3(0, 0, 0) +bones/23/rotation = Quaternion(0, 0, 0, 1) +bones/24/position = Vector3(0, 0, 0) +bones/25/position = Vector3(0, 0, 0) +bones/25/rotation = Quaternion(0, 0, 0, 1) +bones/26/position = Vector3(0, 0, 0) +bones/26/rotation = Quaternion(0, 0, 0, 1) +bones/27/position = Vector3(0, 0, 0) +bones/27/rotation = Quaternion(0, 0, 0, 1) +bones/28/position = Vector3(0, 0, 0) +bones/29/position = Vector3(0, 0, 0) +bones/29/rotation = Quaternion(0, 0, 0, 1) +bones/30/position = Vector3(0, 0, 0) +bones/30/rotation = Quaternion(0, 0, 0, 1) +bones/31/position = Vector3(0, 0, 0) +bones/31/rotation = Quaternion(0, 0, 0, 1) +bones/32/position = Vector3(0, 0, 0) +bones/33/position = Vector3(0, 0, 0) +bones/33/rotation = Quaternion(0, 0, 0, 1) +bones/34/position = Vector3(0, 0, 0) +bones/34/rotation = Quaternion(0, 0, 0, 1) +bones/35/position = Vector3(0, 0, 0) +bones/35/rotation = Quaternion(0, 0, 0, 1) +bones/36/position = Vector3(0, 0, 0) +bones/36/rotation = Quaternion(0, 0, 0, 1) +bones/37/position = Vector3(0, 0, 0) +bones/37/rotation = Quaternion(0, 0, 0, 1) +bones/38/position = Vector3(0, 0, 0) +bones/38/rotation = Quaternion(0, 0, 0, 1) +bones/39/position = Vector3(0, 0, 0) +bones/39/rotation = Quaternion(0, 0, 0, 1) +bones/40/position = Vector3(0, 0, 0) +bones/41/position = Vector3(0, 0, 0) +bones/41/rotation = Quaternion(0, 0, 0, 1) +bones/42/position = Vector3(0, 0, 0) +bones/42/rotation = Quaternion(0, 0, 0, 1) +bones/43/position = Vector3(0, 0, 0) +bones/43/rotation = Quaternion(0, 0, 0, 1) +bones/44/position = Vector3(0, 0, 0) +bones/45/position = Vector3(0, 0, 0) +bones/45/rotation = Quaternion(0, 0, 0, 1) +bones/46/position = Vector3(0, 0, 0) +bones/46/rotation = Quaternion(0, 0, 0, 1) +bones/47/position = Vector3(0, 0, 0) +bones/47/rotation = Quaternion(0, 0, 0, 1) +bones/48/position = Vector3(0, 0, 0) +bones/49/position = Vector3(0, 0, 0) +bones/49/rotation = Quaternion(0, 0, 0, 1) +bones/50/position = Vector3(0, 0, 0) +bones/50/rotation = Quaternion(0, 0, 0, 1) +bones/51/position = Vector3(0, 0, 0) +bones/51/rotation = Quaternion(0, 0, 0, 1) +bones/52/position = Vector3(0, 0, 0) +bones/53/position = Vector3(0, 0, 0) +bones/53/rotation = Quaternion(0, 0, 0, 1) +bones/54/position = Vector3(0, 0, 0) +bones/54/rotation = Quaternion(0, 0, 0, 1) +bones/55/position = Vector3(0, 0, 0) +bones/55/rotation = Quaternion(0, 0, 0, 1) +bones/56/position = Vector3(0, 0, 0) +bones/57/position = Vector3(0, 0, 0) +bones/57/rotation = Quaternion(0, 0, 0, 1) +bones/58/position = Vector3(0, 0, 0) +bones/58/rotation = Quaternion(0, 0, 0, 1) +bones/59/position = Vector3(0, 0, 0) +bones/59/rotation = Quaternion(0, 0, 0, 1) +bones/60/position = Vector3(0, 0, 0) +bones/60/rotation = Quaternion(0, 0, 0, 1) +bones/61/position = Vector3(0, 0, 0) +bones/62/position = Vector3(0, 0, 0) +bones/62/rotation = Quaternion(0, 0, 0, 1) +bones/63/position = Vector3(0, 0, 0) +bones/63/rotation = Quaternion(0, 0, 0, 1) +bones/64/position = Vector3(0, 0, 0) +bones/64/rotation = Quaternion(0, 0, 0, 1) +bones/65/position = Vector3(0, 0, 0) +bones/65/rotation = Quaternion(0, 0, 0, 1) +bones/66/position = Vector3(0, 0, 0) + +[node name="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" index="1" unique_id=66984852] +active = false + [node name="AnimationPlayer2" type="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1255239074] +active = false libraries/animation_library = ExtResource("3_1bvp3") [node name="SyncedAnimationGraph" type="BLTAnimationGraph" parent="Characters/MixamoAmyWalkRunSynced" unique_id=1602406394] animation_player = NodePath("../AnimationPlayer2") -tree_root = ExtResource("5_7mycd") +tree_root = SubResource("BLTAnimationNodeBlendTree_7mycd") skeleton = NodePath("../Armature/Skeleton3D") -parameters/BLTAnimationNodeBlend2/blend_amount = 0.4 +parameters/BLTAnimationNodeBlend2/blend_amount = 0.81 [connection signal="value_changed" from="UI/MarginContainer/HBoxContainer/BlendWeightSlider" to="." method="_on_blend_weight_slider_value_changed"] diff --git a/demo/project.godot b/demo/project.godot index 69fbb8d..6e39ec1 100644 --- a/demo/project.godot +++ b/demo/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="Synced Blend Tree Test" run/main_scene="uid://svj53e2xoio" -config/features=PackedStringArray("4.5", "Forward Plus") +config/features=PackedStringArray("4.6", "Forward Plus") config/icon="res://icon.svg" [display] @@ -23,3 +23,7 @@ window/size/viewport_height=1024 [dotnet] project/assembly_name="Synced Blend Tree Test" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/blendalot/plugin.cfg") diff --git a/demo/synced_blend_tree_node.tres b/demo/synced_blend_tree_node.tres index e3124c5..3dacf6f 100644 --- a/demo/synced_blend_tree_node.tres +++ b/demo/synced_blend_tree_node.tres @@ -1,12 +1,15 @@ -[gd_resource type="BLTAnimationNodeBlendTree" load_steps=4 format=3] +[gd_resource type="BLTAnimationNodeBlendTree" format=3] [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_bvt3d"] +resource_name = "BLTAnimationNodeSampler 1" animation = &"animation_library/TestAnimationB" [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_sntl5"] +resource_name = "BLTAnimationNodeSampler" animation = &"animation_library/TestAnimationA" [sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_n4m28"] +resource_name = "Blend2" sync = false blend_amount = 0.5 sync = false @@ -18,4 +21,4 @@ nodes/Blend2/position = Vector2(0, 0) "nodes/BLTAnimationNodeSampler 1/position" = Vector2(0, 0) nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_sntl5") nodes/BLTAnimationNodeSampler/position = Vector2(0, 0) -node_connections = [&"Blend2", 0, &"BLTAnimationNodeSampler", &"Blend2", 1, &"BLTAnimationNodeSampler 1", &"Output", 0, &"Blend2"] +node_connections = ["Blend2", 0, "BLTAnimationNodeSampler", "Blend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "Blend2"] diff --git a/demo/synced_blend_tree_walk_limp.tres b/demo/synced_blend_tree_walk_limp.tres index fd2c576..b23c127 100644 --- a/demo/synced_blend_tree_walk_limp.tres +++ b/demo/synced_blend_tree_walk_limp.tres @@ -1,19 +1,25 @@ -[gd_resource type="BLTAnimationNodeBlendTree" load_steps=4 format=3 uid="uid://2qfwr1xkiw0s"] +[gd_resource type="BLTAnimationNodeBlendTree" format=3 uid="uid://2qfwr1xkiw0s"] [sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_bvt3d"] -blend_amount = 0.4 +resource_name = "BLTAnimationNodeBlend2" +graph_offset = Vector2(-600, 180) +blend_amount = 1.0 [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_sntl5"] +resource_name = "BLTAnimationNodeSampler 1" +graph_offset = Vector2(-1200, 560) animation = &"animation_library/Limping-InPlace" [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_n4m28"] +resource_name = "BLTAnimationNodeSampler" +graph_offset = Vector2(-1300, -40) animation = &"animation_library/Walk-InPlace" [resource] nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_bvt3d") -nodes/BLTAnimationNodeBlend2/position = Vector2(0, 0) +nodes/BLTAnimationNodeBlend2/graph_offset = Vector2(-600, 180) "nodes/BLTAnimationNodeSampler 1/node" = SubResource("BLTAnimationNodeSampler_sntl5") -"nodes/BLTAnimationNodeSampler 1/position" = Vector2(0, 0) +"nodes/BLTAnimationNodeSampler 1/graph_offset" = Vector2(-1200, 560) nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_n4m28") -nodes/BLTAnimationNodeSampler/position = Vector2(0, 0) -node_connections = [&"BLTAnimationNodeBlend2", 0, &"BLTAnimationNodeSampler", &"BLTAnimationNodeBlend2", 1, &"BLTAnimationNodeSampler 1", &"Output", 0, &"BLTAnimationNodeBlend2"] +nodes/BLTAnimationNodeSampler/graph_offset = Vector2(-1300, -40) +node_connections = ["BLTAnimationNodeBlend2", 0, "BLTAnimationNodeSampler", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "BLTAnimationNodeBlend2"] diff --git a/demo/synced_blend_tree_walk_run.tres b/demo/synced_blend_tree_walk_run.tres index 10d6c6e..7171fb9 100644 --- a/demo/synced_blend_tree_walk_run.tres +++ b/demo/synced_blend_tree_walk_run.tres @@ -1,19 +1,25 @@ -[gd_resource type="BLTAnimationNodeBlendTree" load_steps=4 format=3 uid="uid://qsk64ax2o47f"] +[gd_resource type="BLTAnimationNodeBlendTree" format=3 uid="uid://qsk64ax2o47f"] [sub_resource type="BLTAnimationNodeBlend2" id="BLTAnimationNodeBlend2_bvt3d"] -blend_amount = 0.4 +resource_name = "BLTAnimationNodeBlend2" +graph_offset = Vector2(-360, 140) [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_sntl5"] +resource_name = "BLTAnimationNodeSampler 1" +graph_offset = Vector2(-1140, 440) animation = &"animation_library/Run-InPlace" [sub_resource type="BLTAnimationNodeSampler" id="BLTAnimationNodeSampler_n4m28"] +resource_name = "BLTAnimationNodeSampler" +graph_offset = Vector2(-1080, -40) animation = &"animation_library/Walk-InPlace" [resource] +nodes/Output/position = Vector2(180, 80) nodes/BLTAnimationNodeBlend2/node = SubResource("BLTAnimationNodeBlend2_bvt3d") -nodes/BLTAnimationNodeBlend2/position = Vector2(0, 0) +nodes/BLTAnimationNodeBlend2/position = Vector2(-360, 140) "nodes/BLTAnimationNodeSampler 1/node" = SubResource("BLTAnimationNodeSampler_sntl5") -"nodes/BLTAnimationNodeSampler 1/position" = Vector2(0, 0) +"nodes/BLTAnimationNodeSampler 1/position" = Vector2(-1140, 440) nodes/BLTAnimationNodeSampler/node = SubResource("BLTAnimationNodeSampler_n4m28") -nodes/BLTAnimationNodeSampler/position = Vector2(0, 0) -node_connections = [&"BLTAnimationNodeBlend2", 0, &"BLTAnimationNodeSampler", &"BLTAnimationNodeBlend2", 1, &"BLTAnimationNodeSampler 1", &"Output", 0, &"BLTAnimationNodeBlend2"] +nodes/BLTAnimationNodeSampler/position = Vector2(-1080, -40) +node_connections = ["BLTAnimationNodeBlend2", 0, "BLTAnimationNodeSampler", "BLTAnimationNodeBlend2", 1, "BLTAnimationNodeSampler 1", "Output", 0, "BLTAnimationNodeBlend2"] diff --git a/doc/design.md b/doc/design.md index 4ae8c5f..7d84fb3 100644 --- a/doc/design.md +++ b/doc/design.md @@ -111,7 +111,7 @@ Some nodes have special names in the Blend Tree: Except for the output node of a Blend Tree the following properties hold: * all Blend Tree nodes only operate on properties they own and any other data (e.g. inputs and outputs) are specified - via arguments to `SyncedAnimationNode::evaluate(context, inputs, output)` function of the node. + via arguments to `BLTAnimationNode::evaluate(context, inputs, output)` function of the node. Advantages: diff --git a/register_types.cpp b/register_types.cpp index e33bf83..730d3c2 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -12,6 +12,7 @@ void initialize_blendalot_animgraph_module(ModuleInitializationLevel p_level) { ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); ClassDB::register_class(); } diff --git a/sync_track.h b/sync_track.h index 7a1f3f2..2cdb2e0 100644 --- a/sync_track.h +++ b/sync_track.h @@ -2,6 +2,7 @@ #include "core/templates/local_vector.h" +#include "blendalot_math_helper.h" #include #include @@ -21,7 +22,7 @@ * duration. Blended SyncTracks always have their first interval start at t = 0.0s. */ struct SyncTrack { - static constexpr int cSyncTrackMaxIntervals = 8; + static constexpr int cSyncTrackMaxIntervals = 32; SyncTrack() : duration(0.f), num_intervals(1) { @@ -59,6 +60,12 @@ struct SyncTrack { } double calc_ratio_from_sync_time(double sync_time) const { + // When blending SyncTracks with differing numbers of intervals the resulting SyncTrack may have + // additional repeats of the animation (=> "virtual sync periods", https://youtu.be/Jkv0pbp0ckQ?t=8178). + // + // Therefore, we first have to transform it back to the numbers of intervals we actually have. + sync_time = fmod(sync_time, num_intervals); + float interval_ratio = fmod(sync_time, 1.0f); int interval = int(sync_time - interval_ratio); @@ -126,19 +133,32 @@ struct SyncTrack { */ static SyncTrack blend(float weight, const SyncTrack &track_A, const SyncTrack &track_B) { - assert(track_A.num_intervals == track_B.num_intervals); + if (Math::is_zero_approx(weight)) { + return track_A; + } + + if (Math::is_zero_approx(1.0 - weight)) { + return track_B; + } SyncTrack result; - result.num_intervals = track_A.num_intervals; - result.duration = - (1.0f - weight) * track_A.duration + weight * track_B.duration; + if (track_A.num_intervals != track_B.num_intervals) { + result.num_intervals = least_common_multiple(track_A.num_intervals, track_B.num_intervals); + } else { + result.num_intervals = track_A.num_intervals; + } + assert(result.num_intervals < cSyncTrackMaxIntervals); + float track_A_repeats = static_cast(result.num_intervals / track_A.num_intervals); + float track_B_repeats = static_cast(result.num_intervals / track_B.num_intervals); + + result.duration = (1.0f - weight) * (track_A.duration * track_A_repeats) + weight * (track_B.duration * track_B_repeats); result.interval_start_ratio[0] = 0.f; for (int i = 0; i < result.num_intervals; i++) { - float interval_duration_A = track_A.interval_duration_ratio[i]; - float interval_duration_B = track_B.interval_duration_ratio[i]; + float interval_duration_A = track_A.interval_duration_ratio[i % track_A.num_intervals] / track_A_repeats; + float interval_duration_B = track_B.interval_duration_ratio[i % track_B.num_intervals] / track_B_repeats; result.interval_duration_ratio[i] = (1.0f - weight) * interval_duration_A + weight * interval_duration_B; @@ -152,8 +172,6 @@ struct SyncTrack { } } - assert(result.num_intervals < cSyncTrackMaxIntervals); - return result; } }; \ No newline at end of file diff --git a/tests/test_synced_animation_graph.h b/tests/test_blendalot_animgraph.h similarity index 50% rename from tests/test_synced_animation_graph.h rename to tests/test_blendalot_animgraph.h index f3923a9..e78377d 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_blendalot_animgraph.h @@ -6,7 +6,7 @@ #include "tests/test_macros.h" -struct SyncedAnimationGraphFixture { +struct BlendTreeFixture { Node *character_node; Skeleton3D *skeleton_node; AnimationPlayer *player_node; @@ -15,13 +15,14 @@ struct SyncedAnimationGraphFixture { Ref test_animation_a; Ref test_animation_b; + Ref test_animation_c; Ref test_animation_sync_a; Ref test_animation_sync_b; Ref animation_library; - BLTAnimationGraph *synced_animation_graph; - SyncedAnimationGraphFixture() { + BLTAnimationGraph *animation_graph; + BlendTreeFixture() { BLTAnimationGraph *scene_animation_graph = dynamic_cast(SceneTree::get_singleton()->get_root()->find_child("SyncedAnimationGraphFixtureTestNode", true, false)); if (scene_animation_graph == nullptr) { @@ -50,12 +51,12 @@ struct SyncedAnimationGraphFixture { SceneTree::get_singleton()->get_root()->add_child(player_node); - synced_animation_graph = memnew(BLTAnimationGraph); - synced_animation_graph->set_name("SyncedAnimationGraphFixtureTestNode"); - SceneTree::get_singleton()->get_root()->add_child(synced_animation_graph); + animation_graph = memnew(BLTAnimationGraph); + animation_graph->set_name("SyncedAnimationGraphFixtureTestNode"); + SceneTree::get_singleton()->get_root()->add_child(animation_graph); - synced_animation_graph->set_animation_player(player_node->get_path()); - synced_animation_graph->set_skeleton(skeleton_node->get_path()); + animation_graph->set_animation_player(player_node->get_path()); + animation_graph->set_skeleton(skeleton_node->get_path()); } void setup_animations() { @@ -80,6 +81,16 @@ struct SyncedAnimationGraphFixture { animation_library->add_animation("TestAnimationB", test_animation_b); + test_animation_c = memnew(Animation); + track_index = test_animation_c->add_track(Animation::TYPE_POSITION_3D); + CHECK(track_index == 0); + test_animation_c->track_insert_key(track_index, 0.0, Vector3(0., 0., 0.)); + test_animation_c->track_insert_key(track_index, 3.0, Vector3(2., 4., 6.)); + test_animation_c->track_set_path(track_index, NodePath(vformat("%s:%s", skeleton_node->get_path().get_concatenated_names(), "Hips"))); + test_animation_c->set_loop_mode(Animation::LOOP_LINEAR); + + animation_library->add_animation("TestAnimationC", test_animation_c); + test_animation_sync_a = memnew(Animation); track_index = test_animation_sync_a->add_track(Animation::TYPE_POSITION_3D); CHECK(track_index == 0); @@ -112,8 +123,8 @@ struct SyncedAnimationGraphFixture { } void assign_scene_variables() { - synced_animation_graph = dynamic_cast(SceneTree::get_singleton()->get_root()->find_child("SyncedAnimationGraphFixtureTestNode", true, false)); - REQUIRE(synced_animation_graph); + animation_graph = dynamic_cast(SceneTree::get_singleton()->get_root()->find_child("SyncedAnimationGraphFixtureTestNode", true, false)); + REQUIRE(animation_graph); character_node = (SceneTree::get_singleton()->get_root()->find_child("CharacterNode", true, false)); REQUIRE(character_node != nullptr); skeleton_node = dynamic_cast((SceneTree::get_singleton()->get_root()->find_child("Skeleton", true, false))); @@ -139,34 +150,34 @@ struct SyncedAnimationGraphFixture { } }; -namespace TestSyncedAnimationGraph { +namespace TestBlendalotAnimationGraph { -TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { +TEST_CASE("[Blendalot][BlendTree] Test BlendTree construction") { BLTAnimationNodeBlendTree::BLTBlendTreeGraph tree_constructor; Ref animation_sampler_node0; animation_sampler_node0.instantiate(); - animation_sampler_node0->name = "Sampler0"; + animation_sampler_node0->set_name("Sampler0"); tree_constructor.add_node(animation_sampler_node0); Ref animation_sampler_node1; animation_sampler_node1.instantiate(); - animation_sampler_node1->name = "Sampler1"; + animation_sampler_node1->set_name("Sampler1"); tree_constructor.add_node(animation_sampler_node1); Ref animation_sampler_node2; animation_sampler_node2.instantiate(); - animation_sampler_node2->name = "Sampler2"; + animation_sampler_node2->set_name("Sampler2"); tree_constructor.add_node(animation_sampler_node2); Ref node_blend0; node_blend0.instantiate(); - node_blend0->name = "Blend0"; + node_blend0->set_name("Blend0"); tree_constructor.add_node(node_blend0); Ref node_blend1; node_blend1.instantiate(); - node_blend1->name = "Blend1"; + node_blend1->set_name("Blend1"); tree_constructor.add_node(node_blend1); // Tree @@ -201,7 +212,7 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { CHECK(tree_constructor.node_connection_info[blend1_index].input_subtree_node_indices.has(blend0_index)); // Perform remaining connections - CHECK(BLTAnimationNodeBlendTree::CONNECTION_OK == tree_constructor.add_connection(node_blend1, tree_constructor.get_output_node(), "Input")); + CHECK(BLTAnimationNodeBlendTree::CONNECTION_OK == tree_constructor.add_connection(node_blend1, tree_constructor.get_output_node(), "Output")); CHECK(BLTAnimationNodeBlendTree::CONNECTION_OK == tree_constructor.add_connection(animation_sampler_node2, node_blend1, "Input1")); // Output node must have all nodes in its subtree: @@ -213,15 +224,15 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { tree_constructor.sort_nodes_and_references(); - // Check that for node i all input nodes have a node index j > i. + // Check that for node i all input nodes have a node index j >= i (i is part of the subtree) for (unsigned int i = 0; i < tree_constructor.nodes.size(); i++) { for (int input_index : tree_constructor.node_connection_info[i].input_subtree_node_indices) { - CHECK(input_index > i); + CHECK(input_index >= i); } } } -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] Test AnimationData blending") { +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot] Test AnimationData blending") { AnimationData data_t0; data_t0.allocate_track_values(test_animation_a, skeleton_node); data_t0.sample_from_animation(test_animation_a, skeleton_node, 0.0); @@ -256,12 +267,12 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph } } -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] SyncedAnimationGraph evaluation with an AnimationSampler as root node") { +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot] SyncedAnimationGraph evaluation with an AnimationSampler as root node") { Ref animation_sampler_node; animation_sampler_node.instantiate(); animation_sampler_node->animation_name = "animation_library/TestAnimationA"; - synced_animation_graph->set_root_animation_node(animation_sampler_node); + animation_graph->set_root_animation_node(animation_sampler_node); Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; @@ -278,7 +289,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph CHECK(hip_bone_position.z == doctest::Approx(0.03)); } -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree] BlendTree evaluation with a AnimationSamplerNode connected to the output") { +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTree] BlendTree evaluation with a AnimationSamplerNode connected to the output") { Ref synced_blend_tree_node; synced_blend_tree_node.instantiate(); @@ -287,11 +298,11 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph animation_sampler_node->animation_name = "animation_library/TestAnimationA"; synced_blend_tree_node->add_node(animation_sampler_node); - REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(animation_sampler_node, synced_blend_tree_node->get_output_node(), "Input")); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(animation_sampler_node, synced_blend_tree_node->get_output_node(), "Output")); - synced_blend_tree_node->initialize(synced_animation_graph->get_context()); + synced_blend_tree_node->initialize(animation_graph->get_context()); - synced_animation_graph->set_root_animation_node(synced_blend_tree_node); + animation_graph->set_root_animation_node(synced_blend_tree_node); Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; @@ -308,7 +319,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph CHECK(hip_bone_position.z == doctest::Approx(0.03)); } -TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph][BlendTree][Blend2Node] BlendTree evaluation with a Blend2Node connected to the output") { +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTree][Blend2Node] BlendTree evaluation with a Blend2Node connected to the output") { Ref synced_blend_tree_node; synced_blend_tree_node.instantiate(); @@ -329,20 +340,19 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph // Blend2 Ref blend2_node; blend2_node.instantiate(); - blend2_node->name = "Blend2"; + blend2_node->set_name("Blend2"); blend2_node->blend_weight = 0.5; blend2_node->sync = false; synced_blend_tree_node->add_node(blend2_node); // Connect nodes - Vector blend2_inputs; - blend2_node->get_input_names(blend2_inputs); + Vector blend2_inputs = blend2_node->get_input_names(); REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(animation_sampler_node_a, blend2_node, blend2_inputs[0])); REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(animation_sampler_node_b, blend2_node, blend2_inputs[1])); - REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(blend2_node, synced_blend_tree_node->get_output_node(), "Input")); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == synced_blend_tree_node->add_connection(blend2_node, synced_blend_tree_node->get_output_node(), "Output")); - synced_blend_tree_node->initialize(synced_animation_graph->get_context()); + synced_blend_tree_node->initialize(animation_graph->get_context()); int blend2_node_index = synced_blend_tree_node->find_node_index(blend2_node); const BLTAnimationNodeBlendTree::NodeRuntimeData &blend2_runtime_data = synced_blend_tree_node->_node_runtime_data[blend2_node_index]; @@ -350,7 +360,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph CHECK(blend2_runtime_data.input_nodes[0] == animation_sampler_node_a); CHECK(blend2_runtime_data.input_nodes[1] == animation_sampler_node_b); - synced_animation_graph->set_root_animation_node(synced_blend_tree_node); + animation_graph->set_root_animation_node(synced_blend_tree_node); SUBCASE("Perform default evaluation") { Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; @@ -388,9 +398,9 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph animation_sampler_node_a->animation_name = "animation_library/TestAnimationSyncA"; animation_sampler_node_b->animation_name = "animation_library/TestAnimationSyncB"; blend2_node->sync = true; - synced_blend_tree_node->initialize(synced_animation_graph->get_context()); + synced_blend_tree_node->initialize(animation_graph->get_context()); - REQUIRE(synced_animation_graph->get_root_animation_node().ptr() == synced_blend_tree_node.ptr()); + REQUIRE(animation_graph->get_root_animation_node().ptr() == synced_blend_tree_node.ptr()); // By blending both animations we get a SyncTrack of duration 1.5s with the following // intervals: @@ -437,13 +447,13 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph Ref loaded_synced_blend_tree = ResourceLoader::load("synced_blend_tree_node.tres"); REQUIRE(loaded_synced_blend_tree.is_valid()); - Ref loaded_blend2_node = loaded_synced_blend_tree->get_node(loaded_synced_blend_tree->find_node_index_by_name("Blend2")); + Ref loaded_blend2_node = loaded_synced_blend_tree->get_node_by_index(loaded_synced_blend_tree->find_node_index_by_name("Blend2")); REQUIRE(loaded_blend2_node.is_valid()); CHECK(loaded_blend2_node->sync == false); CHECK(loaded_blend2_node->blend_weight == blend2_node->blend_weight); - loaded_synced_blend_tree->initialize(synced_animation_graph->get_context()); - synced_animation_graph->set_root_animation_node(loaded_synced_blend_tree); + loaded_synced_blend_tree->initialize(animation_graph->get_context()); + animation_graph->set_root_animation_node(loaded_synced_blend_tree); // Re-evaluate using a different time. All animation samplers will start again from 0. SceneTree::get_singleton()->process(0.2); @@ -456,4 +466,266 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph } } -} //namespace TestSyncedAnimationGraph \ No newline at end of file +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][ChangeConnectivity] BlendTreeGraph with various nodes and connections that are removed") { + BLTAnimationNodeBlendTree::BLTBlendTreeGraph blend_tree_graph; + + // TestAnimationA + Ref animation_sampler_node_a; + animation_sampler_node_a.instantiate(); + animation_sampler_node_a->animation_name = "animation_library/TestAnimationA"; + + blend_tree_graph.add_node(animation_sampler_node_a); + + // TestAnimationB + Ref animation_sampler_node_b; + animation_sampler_node_b.instantiate(); + animation_sampler_node_b->animation_name = "animation_library/TestAnimationB"; + + blend_tree_graph.add_node(animation_sampler_node_b); + + // TestAnimationB + Ref animation_sampler_node_c; + animation_sampler_node_c.instantiate(); + animation_sampler_node_c->animation_name = "animation_library/TestAnimationC"; + + blend_tree_graph.add_node(animation_sampler_node_c); + + // Blend2A + Ref blend2_node_a; + blend2_node_a.instantiate(); + blend2_node_a->set_name("Blend2A"); + blend2_node_a->blend_weight = 0.5; + blend2_node_a->sync = false; + + blend_tree_graph.add_node(blend2_node_a); + + // Blend2B + Ref blend2_node_b; + blend2_node_b.instantiate(); + blend2_node_b->set_name("Blend2A"); + blend2_node_b->blend_weight = 0.5; + blend2_node_b->sync = false; + + blend_tree_graph.add_node(blend2_node_b); + + // Connect nodes: Subgraph Output, Blend2A, SamplerA + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_a, blend_tree_graph.get_output_node(), "Output")); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_a, blend2_node_a, "Input0")); + + // Connect nodes: Subgraph Blend2A, SamplerB, SamplerC + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_b, blend2_node_b, "Input0")); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(animation_sampler_node_c, blend2_node_b, "Input1")); + + SUBCASE("Add and remove a connection") { + HashSet subgraph_output_initial = blend_tree_graph.node_connection_info[0].input_subtree_node_indices; + HashSet subgraph_blend2a_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices; + HashSet subgraph_blend2b_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices; + + // Add and remove connection + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_b, blend2_node_a, "Input1")); + blend_tree_graph.remove_connection(blend2_node_b, blend2_node_a, "Input1"); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.is_connection_valid(blend2_node_b, blend2_node_a, "Input1")); + + // Check that we have the same subgraphs as before the connection + CHECK(subgraph_output_initial == blend_tree_graph.node_connection_info[0].input_subtree_node_indices); + CHECK(subgraph_blend2a_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices); + CHECK(subgraph_blend2b_initial == blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_b)].input_subtree_node_indices); + + // Check that the connection is not present anymore. + for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { + bool connection_equals_removed_connection = connection.source_node == blend2_node_b && connection.target_node == blend2_node_a && connection.target_port_name == "Input1"; + CHECK(connection_equals_removed_connection == false); + } + } + + SUBCASE("Remove a node") { + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_graph.add_connection(blend2_node_b, blend2_node_a, "Input1")); + + int animation_sampler_node_b_index_pre_remove = blend_tree_graph.find_node_index(animation_sampler_node_b); + int blend2_node_a_index_pre_remove = blend_tree_graph.find_node_index(blend2_node_a); + int blend2_node_b_index_pre_remove = blend_tree_graph.find_node_index(blend2_node_b); + + CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 6); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_pre_remove].input_subtree_node_indices.size() == 5); + + SUBCASE("Removing the output node does nothing") { + int num_nodes = blend_tree_graph.nodes.size(); + int num_connections = blend_tree_graph.connections.size(); + CHECK(blend_tree_graph.remove_node(blend_tree_graph.get_output_node()) == false); + CHECK(blend_tree_graph.connections.size() == num_connections); + CHECK(blend_tree_graph.nodes.size() == num_nodes); + } + + SUBCASE("Remove a node with no children") { + blend_tree_graph.remove_node(animation_sampler_node_a); + + for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { + bool is_connection_with_removed_node = connection.source_node == animation_sampler_node_a || connection.target_node == animation_sampler_node_a; + CHECK(is_connection_with_removed_node == false); + } + + int animation_sampler_node_b_index_post_remove = blend_tree_graph.find_node_index(animation_sampler_node_b); + int blend2_node_a_index_post_remove = blend_tree_graph.find_node_index(blend2_node_a); + int blend2_node_b_index_post_remove = blend_tree_graph.find_node_index(blend2_node_b); + + CHECK(blend_tree_graph.find_node_index(animation_sampler_node_a) == -1); + CHECK(blend2_node_b_index_post_remove == blend2_node_b_index_pre_remove - 1); + CHECK(animation_sampler_node_b_index_post_remove == animation_sampler_node_b_index_pre_remove - 1); + + CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 5); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].input_subtree_node_indices.size() == 4); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].connected_child_node_index_at_port[0] == -1); + CHECK(blend_tree_graph.node_connection_info[blend2_node_a_index_post_remove].connected_child_node_index_at_port[1] == blend2_node_b_index_post_remove); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(blend2_node_b_index_post_remove)); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(animation_sampler_node_b_index_post_remove)); + } + + SUBCASE("Remove a node with parent and children") { + int num_nodes = blend_tree_graph.nodes.size(); + blend_tree_graph.remove_node(blend2_node_a); + blend_tree_graph.sort_nodes_and_references(); + + CHECK(blend_tree_graph.nodes.size() == num_nodes - 1); + + for (const BLTBlendTreeConnection &connection : blend_tree_graph.connections) { + bool is_connection_with_removed_node = connection.source_node == blend2_node_a || connection.target_node == blend2_node_a; + CHECK(is_connection_with_removed_node == false); + } + + int animation_sampler_node_b_index_post_remove = blend_tree_graph.find_node_index(animation_sampler_node_b); + int animation_sampler_node_c_index_post_remove = blend_tree_graph.find_node_index(animation_sampler_node_c); + int blend2_node_b_index_post_remove = blend_tree_graph.find_node_index(blend2_node_b); + + CHECK(blend_tree_graph.find_node_index(blend2_node_a) == -1); + CHECK(blend2_node_b_index_post_remove == blend2_node_b_index_pre_remove - 1); + CHECK(animation_sampler_node_b_index_post_remove == animation_sampler_node_b_index_pre_remove); + + CHECK(blend_tree_graph.node_connection_info[0].input_subtree_node_indices.size() == 1); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.size() == 3); + blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove]._print_subtree(); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(blend2_node_b_index_post_remove)); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(animation_sampler_node_b_index_post_remove)); + CHECK(blend_tree_graph.node_connection_info[blend2_node_b_index_post_remove].input_subtree_node_indices.has(animation_sampler_node_c_index_post_remove)); + } + } + + SUBCASE("Check evaluation of graph with modified connections") { + Ref blend_tree_node; + blend_tree_node.instantiate(); + blend_tree_node->add_node(animation_sampler_node_a); + blend_tree_node->add_node(animation_sampler_node_b); + blend_tree_node->add_node(animation_sampler_node_c); + blend_tree_node->add_node(blend2_node_a); + + animation_graph->set_root_animation_node(blend_tree_node); + GraphEvaluationContext &graph_context = animation_graph->get_context(); + CHECK(blend_tree_node->initialize(graph_context) == false); + + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_node->add_connection(animation_sampler_node_a, blend_tree_node->get_output_node(), "Output")); + CHECK(blend_tree_node->initialize(graph_context) == true); + + AnimationData *graph_output = graph_context.animation_data_allocator.allocate(); + blend_tree_node->activate_inputs(Vector>()); + blend_tree_node->calculate_sync_track(Vector>()); + blend_tree_node->update_time(0.825); + blend_tree_node->evaluate(graph_context, LocalVector(), *graph_output); + + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_node->add_connection(animation_sampler_node_b, blend2_node_a, "Input0")); + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_node->add_connection(animation_sampler_node_c, blend2_node_a, "Input1")); + + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_node->remove_connection(animation_sampler_node_a, blend_tree_node->get_output_node(), "Output")); + + REQUIRE(BLTAnimationNodeBlendTree::CONNECTION_OK == blend_tree_node->add_connection(blend2_node_a, blend_tree_node->get_output_node(), "Output")); + CHECK(blend_tree_node->initialize(graph_context) == true); + + blend_tree_node->activate_inputs(Vector>()); + blend_tree_node->calculate_sync_track(Vector>()); + blend_tree_node->update_time(0.825); + blend_tree_node->evaluate(graph_context, LocalVector(), *graph_output); + } +} + +TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][EmbeddedBlendTree] BlendTree with an embedded BlendTree subgraph") { + // Embedded BlendTree + Ref embedded_blend_tree; + embedded_blend_tree.instantiate(); + + // TestAnimationB + Ref animation_sampler_node_b; + animation_sampler_node_b.instantiate(); + + embedded_blend_tree->add_node(animation_sampler_node_b); + embedded_blend_tree->add_connection(animation_sampler_node_b, embedded_blend_tree->get_output_node(), "Output"); + + Ref blend_tree; + blend_tree.instantiate(); + + // Blend2 + Ref blend2; + blend2.instantiate(); + blend2->set_name("Blend2"); + blend_tree->add_node(blend2); + + // TestAnimationA + Ref animation_sampler_node_a; + animation_sampler_node_a.instantiate(); + + blend_tree->add_node(animation_sampler_node_a); + + blend_tree->add_node(embedded_blend_tree); + + blend_tree->add_connection(animation_sampler_node_a, blend2, "Input0"); + blend_tree->add_connection(embedded_blend_tree, blend2, "Input1"); + blend_tree->add_connection(blend2, blend_tree->get_output_node(), "Output"); + + SUBCASE("Perform regular blend") { + animation_sampler_node_b->animation_name = "animation_library/TestAnimationB"; + animation_sampler_node_a->animation_name = "animation_library/TestAnimationA"; + blend2->blend_weight = 0.5; + blend2->sync = false; + + // Trigger initialization + animation_graph->set_root_animation_node(blend_tree); + GraphEvaluationContext &graph_context = animation_graph->get_context(); + REQUIRE(blend_tree->initialize(graph_context)); + + // Perform evaluation + AnimationData *graph_output = graph_context.animation_data_allocator.allocate(); + blend_tree->activate_inputs(Vector>()); + blend_tree->calculate_sync_track(Vector>()); + blend_tree->update_time(0.1); + blend_tree->evaluate(graph_context, LocalVector(), *graph_output); + + // Check values + AnimationData::TransformTrackValue *hip_transform_value = graph_output->get_value(test_animation_a->get_tracks()[0]->thash); + CHECK(hip_transform_value->loc[0] == doctest::Approx(0.15)); + CHECK(hip_transform_value->loc[1] == doctest::Approx(0.3)); + CHECK(hip_transform_value->loc[2] == doctest::Approx(0.45)); + } + SUBCASE("Perform synced blend") { + animation_sampler_node_b->animation_name = "animation_library/TestAnimationSyncA"; + animation_sampler_node_a->animation_name = "animation_library/TestAnimationSyncB"; + blend2->blend_weight = 0.5; + blend2->sync = true; + + // Trigger initialization + animation_graph->set_root_animation_node(blend_tree); + GraphEvaluationContext &graph_context = animation_graph->get_context(); + REQUIRE(blend_tree->initialize(graph_context)); + + // Perform evaluation + AnimationData *graph_output = graph_context.animation_data_allocator.allocate(); + blend_tree->activate_inputs(Vector>()); + blend_tree->calculate_sync_track(Vector>()); + blend_tree->update_time(0.825); + blend_tree->evaluate(graph_context, LocalVector(), *graph_output); + + // Check values + AnimationData::TransformTrackValue *hip_transform_value = graph_output->get_value(test_animation_a->get_tracks()[0]->thash); + CHECK(hip_transform_value->loc[0] == doctest::Approx(1.5)); + CHECK(hip_transform_value->loc[1] == doctest::Approx(3.0)); + CHECK(hip_transform_value->loc[2] == doctest::Approx(4.5)); + } +} + +} //namespace TestBlendalotAnimationGraph \ No newline at end of file diff --git a/tests/test_sync_track.h b/tests/test_sync_track.h index 83fa35a..4dea640 100644 --- a/tests/test_sync_track.h +++ b/tests/test_sync_track.h @@ -4,9 +4,9 @@ #include "tests/test_macros.h" -namespace TestSyncedAnimationGraph { +namespace TestBlendalotAnimationGraph { -TEST_CASE("[SyncedAnimationGraph][SyncTrack] Basic") { +TEST_CASE("[Blendalot][SyncTrack] Basic") { SyncTrack track_a; track_a.num_intervals = 2; track_a.duration = 2.0; @@ -84,7 +84,7 @@ TEST_CASE("[SyncedAnimationGraph][SyncTrack] Basic") { } } -TEST_CASE("[SyncedAnimationGraph][SyncTrack] Create Sync Track from markers") { +TEST_CASE("[Blendalot][SyncTrack] Create Sync Track from markers") { SyncTrack track = SyncTrack::create_from_markers(2.0f, { 0.9f, 0.2f }); WHEN("Querying Ratios") { @@ -138,7 +138,7 @@ TEST_CASE("[SyncedAnimationGraph][SyncTrack] Create Sync Track from markers") { } } -TEST_CASE("[SyncedAnimationGraph][SyncTrack] Sync Track blending") { +TEST_CASE("[Blendalot][SyncTrack] Sync Track blending") { SyncTrack track_a = SyncTrack::create_from_markers(2.0, { 0., 0.6, 1.8 }); SyncTrack track_b = SyncTrack::create_from_markers(1.5f, { 1.05, 1.35, 0.3 }); @@ -203,4 +203,47 @@ TEST_CASE("[SyncedAnimationGraph][SyncTrack] Sync Track blending") { } } -} //namespace TestSyncedAnimationGraph \ No newline at end of file +TEST_CASE("[Blendalot][SyncTrack] Sync Track blending non-matching interval count") { + SyncTrack track_a = SyncTrack::create_from_markers(2.0, { 0., 0.6, 1.8 }); + SyncTrack track_b = SyncTrack::create_from_markers(1.5f, { 1.05 }); + + WHEN("Blending two synctracks with weight 0.") { + SyncTrack blended = SyncTrack::blend(0.f, track_a, track_b); + + blended.duration = track_a.duration; + blended.interval_start_ratio[0] = 0.0; + for (int i = 0; i < track_a.num_intervals; i++) { + CHECK(blended.interval_duration_ratio[i] == track_a.interval_duration_ratio[i]); + } + } + WHEN("Blending two synctracks with weight 1.") { + SyncTrack blended = SyncTrack::blend(1.f, track_a, track_b); + + blended.duration = track_b.duration; + blended.interval_start_ratio[0] = 0.0; + for (int i = 0; i < track_b.num_intervals; i++) { + CHECK(blended.interval_duration_ratio[i] == track_b.interval_duration_ratio[i]); + } + } + + WHEN("Blending with weight 0.2") { + float weight = 0.2f; + SyncTrack blended = SyncTrack::blend(weight, track_a, track_b); + + float track_a_repeats = static_cast(blended.num_intervals / track_a.num_intervals); + float track_b_repeats = static_cast(blended.num_intervals / track_b.num_intervals); + + CHECK( + blended.duration == doctest::Approx(2.5)); + CHECK( + blended.interval_start_ratio[0] == 0.0); + CHECK( + blended.interval_duration_ratio[0] == doctest::Approx((1.0 - weight) * track_a.interval_duration_ratio[0] / track_a_repeats + weight * track_b.interval_duration_ratio[0] / track_b_repeats)); + CHECK( + blended.interval_duration_ratio[1] == doctest::Approx((1.0 - weight) * track_a.interval_duration_ratio[1] / track_a_repeats + weight * track_b.interval_duration_ratio[0] / track_b_repeats)); + CHECK( + blended.interval_duration_ratio[2] == doctest::Approx((1.0 - weight) * track_a.interval_duration_ratio[2] / track_a_repeats + weight * track_b.interval_duration_ratio[0] / track_b_repeats)); + } +} + +} //namespace TestBlendalotAnimationGraph \ No newline at end of file