Compare commits

...

4 Commits

Author SHA1 Message Date
Martin Felis
1fca7cfe88 Initial support for animation graph parameters editable in the editor. 2025-12-31 13:47:45 +01:00
Martin Felis
05c1bae346 Preparation of demo project to actually run SyncedBlendTrees. 2025-12-31 13:39:38 +01:00
Martin Felis
ae2e2787cd Minor code cleanup. 2025-12-29 16:02:06 +01:00
Martin Felis
8a47bc5508 Minor documentation improvement. 2025-12-29 15:55:29 +01:00
8 changed files with 345 additions and 222 deletions

Binary file not shown.

View File

@ -1,8 +1,9 @@
[gd_scene load_steps=4 format=3 uid="uid://svj53e2xoio"] [gd_scene load_steps=5 format=3 uid="uid://svj53e2xoio"]
[ext_resource type="PackedScene" uid="uid://d1xcqdqr1qeu6" path="res://assets/MixamoAmy.glb" id="1_0xm2m"] [ext_resource type="PackedScene" uid="uid://d1xcqdqr1qeu6" path="res://assets/MixamoAmy.glb" id="1_0xm2m"]
[ext_resource type="AnimationNodeBlendTree" uid="uid://c7o0gt3li5p4g" path="res://walk_limp_blend_tree.tres" id="2_h2yge"] [ext_resource type="AnimationNodeBlendTree" uid="uid://dbkgln7hoxxc8" path="res://embedded_statemachine.tres" id="2_h2yge"]
[ext_resource type="AnimationLibrary" uid="uid://dwubn740aqx51" path="res://animation_library.res" id="3_h2yge"] [ext_resource type="AnimationLibrary" uid="uid://dwubn740aqx51" path="res://animation_library.res" id="3_h2yge"]
[ext_resource type="SyncedBlendTree" uid="uid://cjeho6848x43q" path="res://synced_blend_tree_node.tres" id="4_1bvp3"]
[node name="Node3D" type="Node3D"] [node name="Node3D" type="Node3D"]
@ -14,6 +15,8 @@ root_node = NodePath("../MixamoAmy")
tree_root = ExtResource("2_h2yge") tree_root = ExtResource("2_h2yge")
anim_player = NodePath("../MixamoAmy/AnimationPlayer") anim_player = NodePath("../MixamoAmy/AnimationPlayer")
parameters/Blend2/blend_amount = 0.44 parameters/Blend2/blend_amount = 0.44
"parameters/Embedded StateMachine/conditions/is_healthy" = false
"parameters/Embedded StateMachine/conditions/is_limping" = false
[node name="AnimationPlayer" type="AnimationPlayer" parent="."] [node name="AnimationPlayer" type="AnimationPlayer" parent="."]
active = false active = false
@ -23,8 +26,9 @@ libraries = {
} }
[node name="SyncedAnimationGraph" type="SyncedAnimationGraph" parent="."] [node name="SyncedAnimationGraph" type="SyncedAnimationGraph" parent="."]
active = false animation_player = NodePath("../AnimationPlayer")
animation_tree = NodePath("../AnimationTree") tree_root = ExtResource("4_1bvp3")
skeleton = NodePath("../MixamoAmy/Armature/Skeleton3D") skeleton = NodePath("../MixamoAmy/Armature/Skeleton3D")
parameters/AnimationBlend2Node/blend_amount = 0.24
[editable path="MixamoAmy"] [editable path="MixamoAmy"]

View File

@ -83,8 +83,10 @@ invalid.
flowchart LR flowchart LR
AnimationB --> TimeScale("TimeScale AnimationB --> TimeScale("TimeScale
---- ----
*scale*") [ ] scale")
AnimationA --> Blend2 AnimationA --> Blend2("Blend2
----
[ ] blend_amount")
TimeScale --> Blend2 TimeScale --> Blend2
Blend2 --> Output Blend2 --> Output
``` ```
@ -94,11 +96,12 @@ evaluation of the Blend Tree it can be used to retrieve the result (i.e. Animati
Some nodes have special names in the Blend Tree: Some nodes have special names in the Blend Tree:
* **Root node** The output node is also called the root node of the graph. * **Root node** The output node is also called the root node of the Blend Tree.
* **Leaf nodes** These are the nodes that have no inputs. In the example these are the nodes AnimationA and AnimationB. * **Leaf nodes** These are the nodes that have no inputs. In the example these are the nodes AnimationA and AnimationB.
* **Parent and child node** For two nodes A and B where B is the node that is connected to the Animation Data output * **Parent and child node** For two nodes A and B where B is the node that is connected to the Animation Data output
port of A port of A
is called the parent node. The output port has no parent and in the example above The Blend2 node is the parent of is called the parent node. The Output node (= Root node) has no parent. In the example above the Blend2 node is the
parent of
both AnimationA and TimeScale. Conversely, AnimationA and TimeScale are child nodes of the Blend2 node. both AnimationA and TimeScale. Conversely, AnimationA and TimeScale are child nodes of the Blend2 node.
## Blend Tree Evaluation Process ## Blend Tree Evaluation Process
@ -125,134 +128,11 @@ Disadvantages:
Evaluation of the Blend Tree happens in multiple phases to ensure we have syncing dependent timing information available Evaluation of the Blend Tree happens in multiple phases to ensure we have syncing dependent timing information available
before performing the actual evaluation. Essentially the Blend Tree has to call the following function on all nodes: before performing the actual evaluation. Essentially the Blend Tree has to call the following function on all nodes:
1. ActivateInputs(): right to left (i.e. from the root node via depth first to the leaf nodes) 1. `ActivateInputs(Vector<Node> inputs)`: right to left (i.e. from the root node via depth first to the leaf nodes)
2. CalculateSyncTracks(): left to right (leaf nodes to root node) 2. `CalculateSyncTracks(Vector<Node> inputs)`: left to right (leaf nodes to root node)
3. UpdateTime(): right to left 3. UpdateTime(): right to left
4. Evaluate(): left to right 4. Evaluate(): left to right
```c++
// BlendTree.h
class BlendTree: public SyncedAnimationNode {
private:
Vector<SyncedAnimationNode*> nodes;
Vector<int> node_parent; // node_parent[i] is the index of the parent of node i.
Vector<AnimationData*> node_output; // output for each node
Vector<Vector<SyncedAnimationNode*>> node_input_nodes;
Vector<Vector<AnimationData*>> node_input_data; // list of inputs for all nodes.
int get_index_for_node(const SyncedAnimationNode& node);
};
// BlendTree.cpp
void BlendTree::initialize_tree() {
for (int i = 0; ci < num_connections; i++) {
const Connection& connection = connections[i];
connection.target_node->set_input_node(connection.target_port_name, connection.source_node);
}
}
void BlendTree::activate_inputs() {
nodes[0]->activate_inputs();
for (int i = 1; i < nodes.size(); i++) {
if (nodes[i]->is_active()) {
nodes[i]->activate_inputs(node_input_nodes[i]);
}
}
}
void BlendTree::calculate_sync_tracks() {
for (int i = nodes.size() - 1; i > 0; i--) {
if (nodes[i]->is_active()) {
nodes[i]->calculate_sync_track();
}
}
}
void BlendTree::update_time() {
for (int i = 1; i < nodes.size(); i++) {
if (nodes[i]->is_active()) {
if (nodes[i]->is_synced()) {
nodes[i]->update_time(node_parents[i]->node_time_info);
} else {
nodes[i]->update_time(node_parents[i]->node_time_info);
}
}
}
}
void BlendTree::evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {
for (int i = nodes.size() - 1; i > 0; i--) {
if (nodes[i]->is_active()) {
node_output[i] = AnimationDataPool::allocate();
nodes[i]->evaluate(context, node_inputs[i], node_output[i]);
// node[i] is done, so we can deallocate the output handles of all input nodes of node[i].
for (AnimationGraphnNode& input_node: input_nodes[i]) {
AnimationDataPool::deallocate(node_output[input_node.index]);
}
nodes[i]->set_active(false);
}
}
std::move(output, nodes[0].output);
}
// Blend2Node.cpp
void Blend2Node::activate_inputs() {
input_node_0->set_active(weight < 1.0 - EPS);
input_node_1->set_active(weight > EPS);
}
void Blend2Node::calculate_sync_track() {
if (input_node_0->is_active()) {
sync_track = input_node_0->sync_track;
}
if (input_node_1->is_active()) {
sync_track.blend(input_node_1->sync_track, blend_weight);
}
}
void Blend2Node::update_time(SyncedAnimationNode::NodeTimeInfo time_info) {
if (!sync_enabled) {
node_time_info.position = node_time_info.position + time_info.delta;
} else {
// TODO
}
}
void Blend2Node::evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {
assert(inputs.size() == 2);
output = lerp(inputs[0]->get_output(), inputs[1], blend_weight);
}
// TimeScaleNode.cpp
void TimeScaleNode::activate_inputs() {
input_node_0->set_active(true);
}
void TimeScaleNode::calculate_sync_track() {
sync_track = input_node_0.sync_track;
sync_track.duration *= time_scale;
}
void TimeScaleNode::update_time(SyncedAnimationNode::NodeTimeInfo time_info) {
if (!sync_enabled) {
node_time_info.position = node_time_info.position + time_info.delta;
} else {
// TODO
}
}
void TimeScaleNode::evaluate(GraphEvaluationContext &context, const Vector<AnimationData*>& inputs, AnimationData &output) {
assert(inputs.size() == 1);
output = inputs[0]->duplicate();
}
```
## State Machines ## State Machines
```plantuml ```plantuml
@ -309,7 +189,7 @@ We use the term "value data" to distinguish from Animation Data.
### Description ### Description
Current AnimationTree nodes have a single designated output port. A node cannot extract a value that then gets used as Current AnimationTree nodes have a single designated output port. A node cannot extract a value that then gets used as
input at a laters tage in the graph. input at a later stage in the graph.
**Depends on**: "Generalized data connections". **Depends on**: "Generalized data connections".
@ -320,7 +200,7 @@ input at a laters tage in the graph.
### Effects on graph topology ### Effects on graph topology
* Increases Node complexity: * Increases Node complexity for handling output ports. Nodes may have the following output ports:
* AnimOutput * AnimOutput
* AnimOutput + Data * AnimOutput + Data
* Data * Data

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_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"))); 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("set_skeleton", "skeleton"), &SyncedAnimationGraph::set_skeleton);
ClassDB::bind_method(D_METHOD("get_skeleton"), &SyncedAnimationGraph::get_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_PROPERTY(PropertyInfo(Variant::NODE_PATH, "skeleton", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Skeleton3D"), "set_skeleton", "get_skeleton");
ADD_SIGNAL(MethodInfo(SNAME("skeleton_changed"))); 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) { void SyncedAnimationGraph::_notification(int p_what) {
switch (p_what) { switch (p_what) {
case Node::NOTIFICATION_READY: { case Node::NOTIFICATION_READY: {
@ -132,6 +256,27 @@ NodePath SyncedAnimationGraph::get_animation_player() const {
return animation_player_path; 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) { void SyncedAnimationGraph::set_skeleton(const NodePath &p_path) {
skeleton_path = p_path; skeleton_path = p_path;
if (p_path.is_empty()) { if (p_path.is_empty()) {
@ -152,26 +297,17 @@ NodePath SyncedAnimationGraph::get_skeleton() const {
return skeleton_path; 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) { void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) {
if (!graph_root_node.is_valid()) { if (!root_animation_node.is_valid()) {
return; return;
} }
graph_root_node->activate_inputs(Vector<Ref<SyncedAnimationNode>>()); _update_properties();
graph_root_node->calculate_sync_track(Vector<Ref<SyncedAnimationNode>>());
graph_root_node->update_time(p_delta); root_animation_node->activate_inputs(Vector<Ref<SyncedAnimationNode>>());
graph_root_node->evaluate(graph_context, LocalVector<AnimationData *>(), graph_output); 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); _apply_animation_data(graph_output);
} }
@ -242,11 +378,11 @@ void SyncedAnimationGraph::_cleanup_evaluation_context() {
} }
void SyncedAnimationGraph::_setup_graph() { 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; return;
} }
graph_root_node->initialize(graph_context); root_animation_node->initialize(graph_context);
} }
SyncedAnimationGraph::SyncedAnimationGraph() { SyncedAnimationGraph::SyncedAnimationGraph() {

View File

@ -12,16 +12,31 @@ class SyncedAnimationGraph : public Node {
private: private:
NodePath animation_player_path; NodePath animation_player_path;
Ref<SyncedAnimationNode> root_animation_node;
NodePath skeleton_path; NodePath skeleton_path;
GraphEvaluationContext graph_context = {}; GraphEvaluationContext graph_context = {};
Ref<SyncedAnimationNode> graph_root_node = nullptr;
AnimationData graph_output; 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: protected:
void _notification(int p_what); void _notification(int p_what);
static void _bind_methods(); 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 ---- */ /* ---- General settings for animation ---- */
AnimationMixer::AnimationCallbackModeProcess callback_mode_process = AnimationMixer::ANIMATION_CALLBACK_MODE_PROCESS_IDLE; AnimationMixer::AnimationCallbackModeProcess callback_mode_process = AnimationMixer::ANIMATION_CALLBACK_MODE_PROCESS_IDLE;
AnimationMixer::AnimationCallbackModeMethod callback_mode_method = AnimationMixer::ANIMATION_CALLBACK_MODE_METHOD_DEFERRED; AnimationMixer::AnimationCallbackModeMethod callback_mode_method = AnimationMixer::ANIMATION_CALLBACK_MODE_METHOD_DEFERRED;
@ -40,12 +55,12 @@ public:
void set_animation_player(const NodePath &p_path); void set_animation_player(const NodePath &p_path);
NodePath get_animation_player() const; 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); void set_skeleton(const NodePath &p_path);
NodePath get_skeleton() const; 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); void set_callback_mode_process(AnimationMixer::AnimationCallbackModeProcess p_mode);
AnimationMixer::AnimationCallbackModeProcess get_callback_mode_process() const; AnimationMixer::AnimationCallbackModeProcess get_callback_mode_process() const;

View File

@ -4,8 +4,44 @@
#include "synced_animation_node.h" #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 { void SyncedBlendTree::_get_property_list(List<PropertyInfo> *p_list) const {
for (const Ref<SyncedAnimationNode> &node : nodes) { for (const Ref<SyncedAnimationNode> &node : tree_graph.nodes) {
String prop_name = node->name; String prop_name = node->name;
if (prop_name != "Output") { 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::OBJECT, "nodes/" + prop_name + "/node", PROPERTY_HINT_RESOURCE_TYPE, "AnimationNode", PROPERTY_USAGE_NO_EDITOR));
@ -25,25 +61,25 @@ bool SyncedBlendTree::_get(const StringName &p_name, Variant &r_value) const {
if (what == "node") { if (what == "node") {
if (node_index != -1) { if (node_index != -1) {
r_value = nodes[node_index]; r_value = tree_graph.nodes[node_index];
return true; return true;
} }
} }
if (what == "position") { if (what == "position") {
if (node_index != -1) { if (node_index != -1) {
r_value = nodes[node_index]->position; r_value = tree_graph.nodes[node_index]->position;
return true; return true;
} }
} }
} else if (prop_name == "node_connections") { } else if (prop_name == "node_connections") {
Array conns; Array conns;
conns.resize(tree_builder.connections.size() * 3); conns.resize(tree_graph.connections.size() * 3);
int idx = 0; int idx = 0;
for (const BlendTreeConnection &connection : tree_builder.connections) { for (const BlendTreeConnection &connection : tree_graph.connections) {
conns[idx * 3 + 0] = connection.target_node->name; 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; conns[idx * 3 + 2] = connection.source_node->name;
idx++; idx++;
} }
@ -73,7 +109,7 @@ bool SyncedBlendTree::_set(const StringName &p_name, const Variant &p_value) {
if (what == "position") { if (what == "position") {
int node_index = find_node_index_by_name(node_name); int node_index = find_node_index_by_name(node_name);
if (node_index > -1) { if (node_index > -1) {
tree_builder.nodes[node_index]->position = p_value; tree_graph.nodes[node_index]->position = p_value;
} }
return true; return true;
} }
@ -86,11 +122,11 @@ bool SyncedBlendTree::_set(const StringName &p_name, const Variant &p_value) {
int target_node_port_index = conns[i + 1]; int target_node_port_index = conns[i + 1];
int source_node_index = find_node_index_by_name(conns[i + 2]); int source_node_index = find_node_index_by_name(conns[i + 2]);
Ref<SyncedAnimationNode> target_node = tree_builder.nodes[target_node_index]; Ref<SyncedAnimationNode> target_node = tree_graph.nodes[target_node_index];
Vector<StringName> target_input_names; Vector<StringName> target_input_names;
target_node->get_input_names(target_input_names); target_node->get_input_names(target_input_names);
add_connection(tree_builder.nodes[source_node_index], target_node, target_input_names[target_node_port_index]); add_connection(tree_graph.nodes[source_node_index], target_node, target_input_names[target_node_port_index]);
} }
return true; return true;
} }
@ -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); 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.length = animation->get_length();
node_time_info.loop_mode = Animation::LOOP_LINEAR; node_time_info.loop_mode = Animation::LOOP_LINEAR;
return true;
} }
void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &inputs, AnimationData &output) { 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"); 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 { 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")); 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; 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: public:
struct NodeTimeInfo { struct NodeTimeInfo {
double length = 0.0; double length = 0.0;
@ -228,7 +242,7 @@ public:
Vector2 position; Vector2 position;
virtual ~SyncedAnimationNode() override = default; 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) { virtual void activate_inputs(Vector<Ref<SyncedAnimationNode>> input_nodes) {
// By default, all inputs nodes are activated. // By default, all inputs nodes are activated.
@ -275,21 +289,19 @@ public:
bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node); bool set_input_node(const StringName &socket_name, SyncedAnimationNode *node);
virtual void get_input_names(Vector<StringName> &inputs) const {} 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; Vector<StringName> inputs;
get_input_names(inputs); get_input_names(inputs);
return inputs.find(port_name); return inputs.find(port_name);
} }
int get_node_input_count() const { int get_input_count() const {
Vector<StringName> inputs; Vector<StringName> inputs;
get_input_names(inputs); get_input_names(inputs);
return inputs.size(); return inputs.size();
} }
//protected: // Creates a list of nodes nested within the current node. E.g. all nodes within a BlendTree node.
// void _get_property_list(List<PropertyInfo> *p_list) const; virtual void get_child_nodes(List<Ref<SyncedAnimationNode>> *r_child_nodes) const {}
// bool _get(const StringName &p_name, Variant &r_value) const;
// bool _set(const StringName &p_name, const Variant &p_value);
}; };
class AnimationSamplerNode : public SyncedAnimationNode { class AnimationSamplerNode : public SyncedAnimationNode {
@ -304,7 +316,7 @@ public:
private: private:
Ref<Animation> animation; Ref<Animation> animation;
void initialize(GraphEvaluationContext &context) override; bool initialize(GraphEvaluationContext &context) override;
void evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &inputs, AnimationData &output) override; void evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &inputs, AnimationData &output) override;
protected: protected:
@ -341,6 +353,11 @@ public:
protected: protected:
static void _bind_methods(); 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; void _get_property_list(List<PropertyInfo> *p_list) const;
bool _get(const StringName &p_name, Variant &r_value) const; bool _get(const StringName &p_name, Variant &r_value) const;
bool _set(const StringName &p_name, const Variant &p_value); bool _set(const StringName &p_name, const Variant &p_value);
@ -353,10 +370,10 @@ struct BlendTreeConnection {
}; };
/** /**
* @class BlendTreeBuilder * @class BlendTreeGraph
* Helper class that is used to build runtime blend trees and also to validate connections. * Helper class that is used to build runtime blend trees and also to validate connections.
*/ */
struct BlendTreeBuilder { struct BlendTreeGraph {
struct NodeConnectionInfo { struct NodeConnectionInfo {
int parent_node_index = -1; int parent_node_index = -1;
HashSet<int> input_subtree_node_indices; // Contains all nodes down to the tree leaves that influence this node. HashSet<int> input_subtree_node_indices; // Contains all nodes down to the tree leaves that influence this node.
@ -366,7 +383,7 @@ struct BlendTreeBuilder {
explicit NodeConnectionInfo(const SyncedAnimationNode *node) { explicit NodeConnectionInfo(const SyncedAnimationNode *node) {
parent_node_index = -1; 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); connected_child_node_index_at_port.push_back(-1);
} }
} }
@ -405,7 +422,7 @@ struct BlendTreeBuilder {
LocalVector<NodeConnectionInfo> node_connection_info; LocalVector<NodeConnectionInfo> node_connection_info;
LocalVector<BlendTreeConnection> connections; LocalVector<BlendTreeConnection> connections;
BlendTreeBuilder() { BlendTreeGraph() {
Ref<OutputNode> output_node; Ref<OutputNode> output_node;
output_node.instantiate(); output_node.instantiate();
output_node->name = "Output"; output_node->name = "Output";
@ -512,7 +529,7 @@ struct BlendTreeBuilder {
int source_node_index = find_node_index(source_node); int source_node_index = find_node_index(source_node);
int target_node_index = find_node_index(target_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[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; node_connection_info[target_node_index].connected_child_node_index_at_port[target_input_port_index] = source_node_index;
@ -549,7 +566,7 @@ struct BlendTreeBuilder {
return false; 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) { 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"); print_error("Cannot connect node: target port already connected");
return false; return false;
@ -567,25 +584,21 @@ struct BlendTreeBuilder {
class SyncedBlendTree : public SyncedAnimationNode { class SyncedBlendTree : public SyncedAnimationNode {
GDCLASS(SyncedBlendTree, SyncedAnimationNode); GDCLASS(SyncedBlendTree, SyncedAnimationNode);
Vector<Ref<SyncedAnimationNode>> nodes; BlendTreeGraph tree_graph;
BlendTreeBuilder tree_builder;
bool tree_initialized = false; bool tree_initialized = false;
void sort_nodes() { void sort_nodes() {
nodes.clear();
_node_runtime_data.clear(); _node_runtime_data.clear();
tree_builder.sort_nodes_and_references(); tree_graph.sort_nodes_and_references();
} }
void setup_runtime_data() { void setup_runtime_data() {
// Add nodes and allocate runtime data // Add nodes and allocate runtime data
for (int i = 0; i < tree_builder.nodes.size(); i++) { for (int i = 0; i < tree_graph.nodes.size(); i++) {
const Ref<SyncedAnimationNode> node = tree_builder.nodes[i]; const Ref<SyncedAnimationNode> node = tree_graph.nodes[i];
nodes.push_back(node);
NodeRuntimeData node_runtime_data; 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); node_runtime_data.input_data.push_back(nullptr);
} }
@ -594,13 +607,13 @@ class SyncedBlendTree : public SyncedAnimationNode {
} }
// Populate runtime data (only now is this.nodes populated to retrieve the nodes) // Populate runtime data (only now is this.nodes populated to retrieve the nodes)
for (int i = 0; i < nodes.size(); i++) { for (int i = 0; i < tree_graph.nodes.size(); i++) {
Ref<SyncedAnimationNode> node = nodes[i]; Ref<SyncedAnimationNode> node = tree_graph.nodes[i];
NodeRuntimeData &node_runtime_data = _node_runtime_data[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_builder.node_connection_info[i].connected_child_node_index_at_port[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(nodes[connected_node_index]); node_runtime_data.input_nodes.push_back(tree_graph.nodes[connected_node_index]);
} }
} }
} }
@ -619,15 +632,15 @@ public:
LocalVector<NodeRuntimeData> _node_runtime_data; LocalVector<NodeRuntimeData> _node_runtime_data;
Ref<SyncedAnimationNode> get_output_node() const { Ref<SyncedAnimationNode> get_output_node() const {
return tree_builder.nodes[0]; return tree_graph.nodes[0];
} }
int find_node_index(const Ref<SyncedAnimationNode> &node) const { int find_node_index(const Ref<SyncedAnimationNode> &node) const {
return tree_builder.find_node_index(node); return tree_graph.find_node_index(node);
} }
int find_node_index_by_name(const StringName &name) const { int find_node_index_by_name(const StringName &name) const {
return tree_builder.find_node_index_by_name(name); return tree_graph.find_node_index_by_name(name);
} }
void add_node(const Ref<SyncedAnimationNode> &node) { void add_node(const Ref<SyncedAnimationNode> &node) {
@ -636,7 +649,7 @@ public:
return; return;
} }
tree_builder.add_node(node); tree_graph.add_node(node);
} }
bool add_connection(const Ref<SyncedAnimationNode> &source_node, const Ref<SyncedAnimationNode> &target_node, const StringName &target_port_name) { bool add_connection(const Ref<SyncedAnimationNode> &source_node, const Ref<SyncedAnimationNode> &target_node, const StringName &target_port_name) {
@ -645,25 +658,29 @@ public:
return false; return false;
} }
return tree_builder.add_connection(source_node, target_node, target_port_name); return tree_graph.add_connection(source_node, target_node, target_port_name);
} }
// overrides from SyncedAnimationNode // overrides from SyncedAnimationNode
void initialize(GraphEvaluationContext &context) override { bool initialize(GraphEvaluationContext &context) override {
sort_nodes(); sort_nodes();
setup_runtime_data(); setup_runtime_data();
for (Ref<SyncedAnimationNode> node : nodes) { for (const Ref<SyncedAnimationNode> &node : tree_graph.nodes) {
node->initialize(context); if (!node->initialize(context)) {
return false;
}
} }
tree_initialized = true; tree_initialized = true;
return true;
} }
void activate_inputs(Vector<Ref<SyncedAnimationNode>> input_nodes) override { void activate_inputs(Vector<Ref<SyncedAnimationNode>> input_nodes) override {
nodes[0]->active = true; tree_graph.nodes[0]->active = true;
for (int i = 0; i < nodes.size(); i++) { for (int i = 0; i < tree_graph.nodes.size(); i++) {
Ref<SyncedAnimationNode> node = nodes[i]; const Ref<SyncedAnimationNode> &node = tree_graph.nodes[i];
if (!node->active) { if (!node->active) {
continue; continue;
@ -675,8 +692,8 @@ public:
} }
void calculate_sync_track(Vector<Ref<SyncedAnimationNode>> input_nodes) override { void calculate_sync_track(Vector<Ref<SyncedAnimationNode>> input_nodes) override {
for (int i = nodes.size() - 1; i > 0; i--) { for (int i = tree_graph.nodes.size() - 1; i > 0; i--) {
Ref<SyncedAnimationNode> node = nodes[i]; const Ref<SyncedAnimationNode> &node = tree_graph.nodes[i];
if (!node->active) { if (!node->active) {
continue; continue;
@ -689,17 +706,17 @@ public:
} }
void update_time(double p_delta) override { void update_time(double p_delta) override {
nodes[0]->node_time_info.delta = p_delta; tree_graph.nodes[0]->node_time_info.delta = p_delta;
nodes[0]->node_time_info.position += p_delta; tree_graph.nodes[0]->node_time_info.position += p_delta;
for (int i = 1; i < nodes.size(); i++) { for (int i = 1; i < tree_graph.nodes.size(); i++) {
Ref<SyncedAnimationNode> node = nodes[i]; const Ref<SyncedAnimationNode> &node = tree_graph.nodes[i];
if (!node->active) { if (!node->active) {
continue; continue;
} }
Ref<SyncedAnimationNode> node_parent = nodes[tree_builder.node_connection_info[i].parent_node_index]; const Ref<SyncedAnimationNode> &node_parent = tree_graph.nodes[tree_graph.node_connection_info[i].parent_node_index];
if (node->node_time_info.is_synced) { if (node->node_time_info.is_synced) {
node->update_time(node_parent->node_time_info.position); node->update_time(node_parent->node_time_info.position);
@ -710,8 +727,8 @@ public:
} }
void evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &input_datas, AnimationData &output_data) override { void evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &input_datas, AnimationData &output_data) override {
for (int i = nodes.size() - 1; i > 0; i--) { for (int i = tree_graph.nodes.size() - 1; i > 0; i--) {
const Ref<SyncedAnimationNode> &node = nodes[i]; const Ref<SyncedAnimationNode> &node = tree_graph.nodes[i];
if (!node->active) { if (!node->active) {
continue; continue;
@ -721,7 +738,7 @@ public:
// Populate the inputs // Populate the inputs
for (unsigned int j = 0; j < node_runtime_data.input_data.size(); j++) { for (unsigned int j = 0; j < node_runtime_data.input_data.size(); j++) {
int child_index = tree_builder.node_connection_info[i].connected_child_node_index_at_port[j]; int child_index = tree_graph.node_connection_info[i].connected_child_node_index_at_port[j];
node_runtime_data.input_data[j] = _node_runtime_data[child_index].output_data; node_runtime_data.input_data[j] = _node_runtime_data[child_index].output_data;
} }
@ -735,9 +752,15 @@ public:
node->evaluate(context, node_runtime_data.input_data, *node_runtime_data.output_data); node->evaluate(context, node_runtime_data.input_data, *node_runtime_data.output_data);
// All inputs have been consumed and can now be freed. // All inputs have been consumed and can now be freed.
for (int child_index : tree_builder.node_connection_info[i].connected_child_node_index_at_port) { for (const int child_index : tree_graph.node_connection_info[i].connected_child_node_index_at_port) {
memfree(_node_runtime_data[child_index].output_data); memfree(_node_runtime_data[child_index].output_data);
} }
} }
} }
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

@ -71,7 +71,7 @@ struct SyncedAnimationGraphFixture {
namespace TestSyncedAnimationGraph { namespace TestSyncedAnimationGraph {
TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") { TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") {
BlendTreeBuilder tree_constructor; BlendTreeGraph tree_constructor;
Ref<AnimationSamplerNode> animation_sampler_node0; Ref<AnimationSamplerNode> animation_sampler_node0;
animation_sampler_node0.instantiate(); animation_sampler_node0.instantiate();
@ -182,7 +182,7 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph
animation_sampler_node.instantiate(); animation_sampler_node.instantiate();
animation_sampler_node->animation_name = "animation_library/TestAnimationA"; 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; 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_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; 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[0] == animation_sampler_node_a);
CHECK(blend2_runtime_data.input_nodes[1] == animation_sampler_node_b); 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; 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()); REQUIRE(loaded_synced_blend_tree.is_valid());
loaded_synced_blend_tree->initialize(synced_animation_graph->get_context()); 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. // Re-evaluate using a different time. All animation samplers will start again from 0.
SceneTree::get_singleton()->process(0.2); SceneTree::get_singleton()->process(0.2);