Initial support for animation graph parameters editable in the editor.

This commit is contained in:
Martin Felis 2025-12-31 13:47:45 +01:00
parent 05c1bae346
commit 1fca7cfe88
5 changed files with 287 additions and 39 deletions

View File

@ -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<SyncedAnimationNode> p_node) const {
ERR_FAIL_COND(p_node.is_null());
List<PropertyInfo> plist;
p_node->get_parameter_list(&plist);
for (PropertyInfo &pinfo : plist) {
StringName key = pinfo.name;
if (!property_map.has(p_base_path + key)) {
Pair<Variant, bool> 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<Ref<SyncedAnimationNode>, StringName>(p_node, key);
pinfo.name = p_base_path + key;
properties.push_back(pinfo);
}
List<Ref<SyncedAnimationNode>> children;
p_node->get_child_nodes(&children);
for (const Ref<SyncedAnimationNode> &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<SyncedAnimationGraph *>(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<AnimationMixer::AnimationCallbackModeProcess>((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<Variant, bool> &prop = property_map[p_name];
Variant value = p_value;
if (Animation::validate_type_match(prop.first, value)) {
Pair<Ref<SyncedAnimationNode>, 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<PropertyInfo> *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<SyncedAnimationNode> &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<SyncedAnimationNode> 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<SyncedAnimationNode> &p_animation_node) {
if (graph_root_node != p_animation_node) {
graph_root_node = p_animation_node;
_setup_graph();
}
}
Ref<SyncedAnimationNode> 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<Ref<SyncedAnimationNode>>());
graph_root_node->calculate_sync_track(Vector<Ref<SyncedAnimationNode>>());
graph_root_node->update_time(p_delta);
graph_root_node->evaluate(graph_context, LocalVector<AnimationData *>(), graph_output);
_update_properties();
root_animation_node->activate_inputs(Vector<Ref<SyncedAnimationNode>>());
root_animation_node->calculate_sync_track(Vector<Ref<SyncedAnimationNode>>());
root_animation_node->update_time(p_delta);
root_animation_node->evaluate(graph_context, LocalVector<AnimationData *>(), 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() {

View File

@ -12,16 +12,31 @@ class SyncedAnimationGraph : public Node {
private:
NodePath animation_player_path;
Ref<SyncedAnimationNode> root_animation_node;
NodePath skeleton_path;
GraphEvaluationContext graph_context = {};
Ref<SyncedAnimationNode> graph_root_node = nullptr;
AnimationData graph_output;
mutable List<PropertyInfo> properties;
mutable AHashMap<StringName, Pair<Variant, bool>> property_map; // Property value and read-only flag.
mutable AHashMap<StringName, Pair<Ref<SyncedAnimationNode>, StringName>> property_node_map;
mutable bool properties_dirty = true;
void _update_properties() const;
void _update_properties_for_node(const String &p_base_path, Ref<SyncedAnimationNode> 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<PropertyInfo> *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<SyncedAnimationNode> &p_animation_node);
Ref<SyncedAnimationNode> get_root_animation_node() const;
void set_skeleton(const NodePath &p_path);
NodePath get_skeleton() const;
void set_graph_root_node(const Ref<SyncedAnimationNode> &p_animation_node);
Ref<SyncedAnimationNode> get_graph_root_node() const;
void set_callback_mode_process(AnimationMixer::AnimationCallbackModeProcess p_mode);
AnimationMixer::AnimationCallbackModeProcess get_callback_mode_process() const;

View File

@ -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<PropertyInfo> *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<PropertyInfo> *p_list) const {
for (const Ref<SyncedAnimationNode> &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> &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<AnimationData *> &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<PropertyInfo> *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<PropertyInfo> *p_list) const {
p_list->push_back(PropertyInfo(Variant::FLOAT, blend_amount, PROPERTY_HINT_RANGE, "0,1,0.01,or_less,or_greater"));
}

View File

@ -209,6 +209,20 @@ class SyncedAnimationNode : public Resource {
friend class SyncedAnimationGraph;
protected:
static void _bind_methods();
virtual void get_parameter_list(List<PropertyInfo> *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<Ref<SyncedAnimationNode>> 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<StringName> &inputs) const {}
int get_node_input_index(const StringName &port_name) const {
int get_input_index(const StringName &port_name) const {
Vector<StringName> inputs;
get_input_names(inputs);
return inputs.find(port_name);
}
int get_node_input_count() const {
int get_input_count() const {
Vector<StringName> 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<Ref<SyncedAnimationNode>> *r_child_nodes) const {}
};
class AnimationSamplerNode : public SyncedAnimationNode {
@ -299,7 +316,7 @@ public:
private:
Ref<Animation> animation;
void initialize(GraphEvaluationContext &context) override;
bool initialize(GraphEvaluationContext &context) override;
void evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &inputs, AnimationData &output) override;
protected:
@ -336,6 +353,11 @@ public:
protected:
static void _bind_methods();
void get_parameter_list(List<PropertyInfo> *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<PropertyInfo> *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<SyncedAnimationNode> 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<SyncedAnimationNode> 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<SyncedAnimationNode> &node : tree_graph.nodes) {
node->initialize(context);
if (!node->initialize(context)) {
return false;
}
}
tree_initialized = true;
return true;
}
void activate_inputs(Vector<Ref<SyncedAnimationNode>> input_nodes) override {
@ -731,4 +757,10 @@ public:
}
}
}
void get_child_nodes(List<Ref<SyncedAnimationNode>> *r_child_nodes) const override {
for (const Ref<SyncedAnimationNode> &node : tree_graph.nodes) {
r_child_nodes->push_back(node.ptr());
}
}
};

View File

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