From 1fca7cfe88ab67cc654dcf4b5c2300eda7573f65 Mon Sep 17 00:00:00 2001 From: Martin Felis Date: Wed, 31 Dec 2025 13:47:45 +0100 Subject: [PATCH] Initial support for animation graph parameters editable in the editor. --- synced_animation_graph.cpp | 172 +++++++++++++++++++++++++--- synced_animation_graph.h | 23 +++- synced_animation_node.cpp | 69 ++++++++++- synced_animation_node.h | 54 +++++++-- tests/test_synced_animation_graph.h | 8 +- 5 files changed, 287 insertions(+), 39 deletions(-) diff --git a/synced_animation_graph.cpp b/synced_animation_graph.cpp index be4dc6f..c52eb94 100644 --- a/synced_animation_graph.cpp +++ b/synced_animation_graph.cpp @@ -20,12 +20,136 @@ void SyncedAnimationGraph::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "animation_player", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "AnimationPlayer"), "set_animation_player", "get_animation_player"); ADD_SIGNAL(MethodInfo(SNAME("animation_player_changed"))); + ClassDB::bind_method(D_METHOD("set_tree_root", "animation_node"), &SyncedAnimationGraph::set_root_animation_node); + ClassDB::bind_method(D_METHOD("get_tree_root"), &SyncedAnimationGraph::get_root_animation_node); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "tree_root", PROPERTY_HINT_RESOURCE_TYPE, "SyncedAnimationNode"), "set_tree_root", "get_tree_root"); + ClassDB::bind_method(D_METHOD("set_skeleton", "skeleton"), &SyncedAnimationGraph::set_skeleton); ClassDB::bind_method(D_METHOD("get_skeleton"), &SyncedAnimationGraph::get_skeleton); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "skeleton", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Skeleton3D"), "set_skeleton", "get_skeleton"); ADD_SIGNAL(MethodInfo(SNAME("skeleton_changed"))); } +void SyncedAnimationGraph::_update_properties_for_node(const String &p_base_path, Ref p_node) const { + ERR_FAIL_COND(p_node.is_null()); + + List plist; + p_node->get_parameter_list(&plist); + for (PropertyInfo &pinfo : plist) { + StringName key = pinfo.name; + + if (!property_map.has(p_base_path + key)) { + Pair param; + param.first = p_node->get_parameter_default_value(key); + param.second = p_node->is_parameter_read_only(key); + property_map[p_base_path + key] = param; + } + + property_node_map[p_base_path + key] = Pair, StringName>(p_node, key); + + pinfo.name = p_base_path + key; + properties.push_back(pinfo); + } + + List> children; + p_node->get_child_nodes(&children); + + for (const Ref &child_node : children) { + _update_properties_for_node(p_base_path + child_node->name + "/", child_node); + } +} + +void SyncedAnimationGraph::_update_properties() const { + if (!properties_dirty) { + return; + } + + properties.clear(); + property_map.clear(); + property_node_map.clear(); + + if (root_animation_node.is_valid()) { + _update_properties_for_node(Animation::PARAMETERS_BASE_PATH, root_animation_node); + } + + properties_dirty = false; + + const_cast(this)->notify_property_list_changed(); +} + +bool SyncedAnimationGraph::_set(const StringName &p_name, const Variant &p_value) { +#ifndef DISABLE_DEPRECATED + String name = p_name; + if (name == "process_callback") { + set_callback_mode_process(static_cast((int)p_value)); + return true; + } +#endif // DISABLE_DEPRECATED + if (properties_dirty) { + _update_properties(); + } + + if (property_map.has(p_name)) { + if (is_inside_tree() && property_map[p_name].second) { + return false; // Prevent to set property by user. + } + Pair &prop = property_map[p_name]; + Variant value = p_value; + if (Animation::validate_type_match(prop.first, value)) { + Pair, StringName> property_node = property_node_map[p_name]; + if (!property_node.first.is_valid()) { + print_error(vformat("Cannot set property '%s' node not found.", p_name)); + return false; + } + property_node.first->set_parameter(property_node.second, value); + + // also set value in the graph's copy of the value. Should probably be removed at some point... + prop.first = value; + } + return true; + } + + return false; +} + +bool SyncedAnimationGraph::_get(const StringName &p_name, Variant &r_ret) const { +#ifndef DISABLE_DEPRECATED + if (p_name == "process_callback") { + r_ret = get_callback_mode_process(); + return true; + } +#endif // DISABLE_DEPRECATED + if (properties_dirty) { + _update_properties(); + } + + if (property_map.has(p_name)) { + r_ret = property_map[p_name].first; + return true; + } + + return false; +} + +void SyncedAnimationGraph::_get_property_list(List *p_list) const { + if (properties_dirty) { + _update_properties(); + } + + for (const PropertyInfo &E : properties) { + p_list->push_back(E); + } +} + +void SyncedAnimationGraph::_tree_changed() { + if (properties_dirty) { + return; + } + + callable_mp(this, &SyncedAnimationGraph::_update_properties).call_deferred(); + properties_dirty = true; +} + void SyncedAnimationGraph::_notification(int p_what) { switch (p_what) { case Node::NOTIFICATION_READY: { @@ -132,6 +256,27 @@ NodePath SyncedAnimationGraph::get_animation_player() const { return animation_player_path; } +void SyncedAnimationGraph::set_root_animation_node(const Ref &p_animation_node) { + if (root_animation_node.is_valid()) { + root_animation_node->disconnect(SNAME("tree_changed"), callable_mp(this, &SyncedAnimationGraph::_tree_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, &SyncedAnimationGraph::_tree_changed)); + } + + properties_dirty = true; + + update_configuration_warnings(); +} + +Ref SyncedAnimationGraph::get_root_animation_node() const { + return root_animation_node; +} + void SyncedAnimationGraph::set_skeleton(const NodePath &p_path) { skeleton_path = p_path; if (p_path.is_empty()) { @@ -152,26 +297,17 @@ NodePath SyncedAnimationGraph::get_skeleton() const { return skeleton_path; } -void SyncedAnimationGraph::set_graph_root_node(const Ref &p_animation_node) { - if (graph_root_node != p_animation_node) { - graph_root_node = p_animation_node; - _setup_graph(); - } -} - -Ref SyncedAnimationGraph::get_graph_root_node() const { - return graph_root_node; -} - void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) { - if (!graph_root_node.is_valid()) { + if (!root_animation_node.is_valid()) { return; } - graph_root_node->activate_inputs(Vector>()); - graph_root_node->calculate_sync_track(Vector>()); - graph_root_node->update_time(p_delta); - graph_root_node->evaluate(graph_context, LocalVector(), graph_output); + _update_properties(); + + root_animation_node->activate_inputs(Vector>()); + root_animation_node->calculate_sync_track(Vector>()); + root_animation_node->update_time(p_delta); + root_animation_node->evaluate(graph_context, LocalVector(), graph_output); _apply_animation_data(graph_output); } @@ -242,11 +378,11 @@ void SyncedAnimationGraph::_cleanup_evaluation_context() { } void SyncedAnimationGraph::_setup_graph() { - if (graph_context.animation_player == nullptr || graph_context.skeleton_3d == nullptr || !graph_root_node.is_valid()) { + if (graph_context.animation_player == nullptr || graph_context.skeleton_3d == nullptr || !root_animation_node.is_valid()) { return; } - graph_root_node->initialize(graph_context); + root_animation_node->initialize(graph_context); } SyncedAnimationGraph::SyncedAnimationGraph() { diff --git a/synced_animation_graph.h b/synced_animation_graph.h index 406da7e..489a62a 100644 --- a/synced_animation_graph.h +++ b/synced_animation_graph.h @@ -12,16 +12,31 @@ class SyncedAnimationGraph : public Node { private: NodePath animation_player_path; + Ref root_animation_node; NodePath skeleton_path; GraphEvaluationContext graph_context = {}; - Ref graph_root_node = nullptr; AnimationData graph_output; + mutable List properties; + mutable AHashMap> property_map; // Property value and read-only flag. + mutable AHashMap, StringName>> property_node_map; + + mutable bool properties_dirty = true; + + void _update_properties() const; + void _update_properties_for_node(const String &p_base_path, Ref p_node) const; + + void _tree_changed(); + protected: void _notification(int p_what); static void _bind_methods(); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List *p_list) const; + /* ---- General settings for animation ---- */ AnimationMixer::AnimationCallbackModeProcess callback_mode_process = AnimationMixer::ANIMATION_CALLBACK_MODE_PROCESS_IDLE; AnimationMixer::AnimationCallbackModeMethod callback_mode_method = AnimationMixer::ANIMATION_CALLBACK_MODE_METHOD_DEFERRED; @@ -40,12 +55,12 @@ public: void set_animation_player(const NodePath &p_path); NodePath get_animation_player() const; + void set_root_animation_node(const Ref &p_animation_node); + Ref get_root_animation_node() const; + void set_skeleton(const NodePath &p_path); NodePath get_skeleton() const; - void set_graph_root_node(const Ref &p_animation_node); - Ref get_graph_root_node() const; - void set_callback_mode_process(AnimationMixer::AnimationCallbackModeProcess p_mode); AnimationMixer::AnimationCallbackModeProcess get_callback_mode_process() const; diff --git a/synced_animation_node.cpp b/synced_animation_node.cpp index f215af9..b16deeb 100644 --- a/synced_animation_node.cpp +++ b/synced_animation_node.cpp @@ -4,6 +4,42 @@ #include "synced_animation_node.h" +void SyncedAnimationNode::_bind_methods() { + ADD_SIGNAL(MethodInfo("tree_changed")); + 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"))); +} + +void SyncedAnimationNode::get_parameter_list(List *r_list) const { +} + +Variant SyncedAnimationNode::get_parameter_default_value(const StringName &p_parameter) const { + return Variant(); +} + +bool SyncedAnimationNode::is_parameter_read_only(const StringName &p_parameter) const { + return false; +} + +void SyncedAnimationNode::set_parameter(const StringName &p_name, const Variant &p_value) { +} + +Variant SyncedAnimationNode::get_parameter(const StringName &p_name) const { + return Variant(); +} + +void SyncedAnimationNode::_tree_changed() { + emit_signal(SNAME("tree_changed")); +} + +void SyncedAnimationNode::_animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name) { + emit_signal(SNAME("animation_node_renamed"), p_oid, p_old_name, p_new_name); +} + +void SyncedAnimationNode::_animation_node_removed(const ObjectID &p_oid, const StringName &p_node) { + emit_signal(SNAME("animation_node_removed"), p_oid, p_node); +} + void SyncedBlendTree::_get_property_list(List *p_list) const { for (const Ref &node : tree_graph.nodes) { String prop_name = node->name; @@ -43,7 +79,7 @@ bool SyncedBlendTree::_get(const StringName &p_name, Variant &r_value) const { int idx = 0; for (const BlendTreeConnection &connection : tree_graph.connections) { conns[idx * 3 + 0] = connection.target_node->name; - conns[idx * 3 + 1] = connection.target_node->get_node_input_index(connection.target_port_name); + conns[idx * 3 + 1] = connection.target_node->get_input_index(connection.target_port_name); conns[idx * 3 + 2] = connection.source_node->name; idx++; } @@ -159,10 +195,17 @@ void AnimationData::sample_from_animation(const Ref &animation, const } } -void AnimationSamplerNode::initialize(GraphEvaluationContext &context) { +bool AnimationSamplerNode::initialize(GraphEvaluationContext &context) { 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)); + return false; + } + node_time_info.length = animation->get_length(); node_time_info.loop_mode = Animation::LOOP_LINEAR; + + return true; } void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) { @@ -207,6 +250,28 @@ void AnimationBlend2Node::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "sync"), "set_use_sync", "is_using_sync"); } +void AnimationBlend2Node::get_parameter_list(List *p_list) const { + p_list->push_back(PropertyInfo(Variant::FLOAT, blend_amount, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater")); +} + +void AnimationBlend2Node::set_parameter(const StringName &p_name, const Variant &p_value) { + _set(p_name, p_value); +} + +Variant AnimationBlend2Node::get_parameter(const StringName &p_name) const { + Variant result; + _get(p_name, result); + return result; +} + +Variant AnimationBlend2Node::get_parameter_default_value(const StringName &p_parameter) const { + if (p_parameter == blend_amount) { + return blend_weight; + } + + return Variant(); +} + void AnimationBlend2Node::_get_property_list(List *p_list) const { p_list->push_back(PropertyInfo(Variant::FLOAT, blend_amount, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater")); } diff --git a/synced_animation_node.h b/synced_animation_node.h index ba49f04..801bbf2 100644 --- a/synced_animation_node.h +++ b/synced_animation_node.h @@ -209,6 +209,20 @@ class SyncedAnimationNode : public Resource { friend class SyncedAnimationGraph; +protected: + static void _bind_methods(); + + virtual void get_parameter_list(List *r_list) const; + virtual Variant get_parameter_default_value(const StringName &p_parameter) const; + virtual bool is_parameter_read_only(const StringName &p_parameter) const; + + 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 _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); + public: struct NodeTimeInfo { double length = 0.0; @@ -228,7 +242,7 @@ public: Vector2 position; virtual ~SyncedAnimationNode() override = default; - virtual void initialize(GraphEvaluationContext &context) {} + virtual bool initialize(GraphEvaluationContext &context) { return true; } virtual void activate_inputs(Vector> input_nodes) { // By default, all inputs nodes are activated. @@ -275,16 +289,19 @@ public: bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node); virtual void get_input_names(Vector &inputs) const {} - int get_node_input_index(const StringName &port_name) const { + int get_input_index(const StringName &port_name) const { Vector inputs; get_input_names(inputs); return inputs.find(port_name); } - int get_node_input_count() const { + int get_input_count() const { Vector inputs; get_input_names(inputs); return inputs.size(); } + + // Creates a list of nodes nested within the current node. E.g. all nodes within a BlendTree node. + virtual void get_child_nodes(List> *r_child_nodes) const {} }; class AnimationSamplerNode : public SyncedAnimationNode { @@ -299,7 +316,7 @@ public: private: Ref animation; - void initialize(GraphEvaluationContext &context) override; + bool initialize(GraphEvaluationContext &context) override; void evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) override; protected: @@ -336,6 +353,11 @@ public: protected: static void _bind_methods(); + void get_parameter_list(List *p_list) const override; + Variant get_parameter_default_value(const StringName &p_parameter) const override; + void set_parameter(const StringName &p_name, const Variant &p_value) override; + Variant get_parameter(const StringName &p_name) const override; + 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); @@ -361,7 +383,7 @@ struct BlendTreeGraph { explicit NodeConnectionInfo(const SyncedAnimationNode *node) { parent_node_index = -1; - for (int i = 0; i < node->get_node_input_count(); i++) { + for (int i = 0; i < node->get_input_count(); i++) { connected_child_node_index_at_port.push_back(-1); } } @@ -507,7 +529,7 @@ struct BlendTreeGraph { int source_node_index = find_node_index(source_node); int target_node_index = find_node_index(target_node); - int target_input_port_index = target_node->get_node_input_index(target_port_name); + int target_input_port_index = target_node->get_input_index(target_port_name); node_connection_info[source_node_index].parent_node_index = target_node_index; node_connection_info[target_node_index].connected_child_node_index_at_port[target_input_port_index] = source_node_index; @@ -544,7 +566,7 @@ struct BlendTreeGraph { return false; } - int target_input_port_index = target_node->get_node_input_index(target_port_name); + 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 false; @@ -576,7 +598,7 @@ class SyncedBlendTree : public SyncedAnimationNode { const Ref node = tree_graph.nodes[i]; NodeRuntimeData node_runtime_data; - for (int ni = 0; ni < node->get_node_input_count(); ni++) { + for (int ni = 0; ni < node->get_input_count(); ni++) { node_runtime_data.input_data.push_back(nullptr); } @@ -589,7 +611,7 @@ class SyncedBlendTree : public SyncedAnimationNode { Ref node = tree_graph.nodes[i]; NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; - for (int port_index = 0; port_index < node->get_node_input_count(); port_index++) { + 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]); } @@ -640,15 +662,19 @@ public: } // overrides from SyncedAnimationNode - void initialize(GraphEvaluationContext &context) override { + bool initialize(GraphEvaluationContext &context) override { sort_nodes(); setup_runtime_data(); for (const Ref &node : tree_graph.nodes) { - node->initialize(context); + if (!node->initialize(context)) { + return false; + } } tree_initialized = true; + + return true; } void activate_inputs(Vector> input_nodes) override { @@ -731,4 +757,10 @@ public: } } } + + void get_child_nodes(List> *r_child_nodes) const override { + for (const Ref &node : tree_graph.nodes) { + r_child_nodes->push_back(node.ptr()); + } + } }; diff --git a/tests/test_synced_animation_graph.h b/tests/test_synced_animation_graph.h index 5ff7fa4..ef3799e 100644 --- a/tests/test_synced_animation_graph.h +++ b/tests/test_synced_animation_graph.h @@ -182,7 +182,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph animation_sampler_node.instantiate(); animation_sampler_node->animation_name = "animation_library/TestAnimationA"; - synced_animation_graph->set_graph_root_node(animation_sampler_node); + synced_animation_graph->set_root_animation_node(animation_sampler_node); Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; @@ -212,7 +212,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph synced_blend_tree_node->initialize(synced_animation_graph->get_context()); - synced_animation_graph->set_graph_root_node(synced_blend_tree_node); + synced_animation_graph->set_root_animation_node(synced_blend_tree_node); Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; @@ -269,7 +269,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_graph_root_node(synced_blend_tree_node); + synced_animation_graph->set_root_animation_node(synced_blend_tree_node); Vector3 hip_bone_position = skeleton_node->get_bone_global_pose(hip_bone_index).origin; @@ -295,7 +295,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph REQUIRE(loaded_synced_blend_tree.is_valid()); loaded_synced_blend_tree->initialize(synced_animation_graph->get_context()); - synced_animation_graph->set_graph_root_node(loaded_synced_blend_tree); + synced_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);