Compare commits

..

29 Commits

Author SHA1 Message Date
Martin Felis
20331d0765 Properly deactivate nodes to ensure proper evaluation after changed node connections. 2026-02-25 22:26:52 +01:00
Martin Felis
4a2ef2493d Fixed incomplete comment. 2026-02-23 21:25:13 +01:00
Martin Felis
d57fe50d5f Added BLTAnimationNodeTimeScale. 2026-02-22 00:56:53 +01:00
Martin Felis
3dd1ce42df Added initial support for blending of SyncTracks with differing numbers of intervals.
Not sure that the resulting blends are correct, but leave it for now.
2026-02-21 21:30:54 +01:00
Martin Felis
c7660c7b19 Replace SyncedAnimationGraph to BLTAnimationGraph. 2026-02-20 22:55:30 +01:00
Martin Felis
095f1e5d0c Animations now selectable using OptionButton. 2026-02-20 13:40:12 +01:00
Martin Felis
0198847fd1 Disconnection works again in blend tree editor. 2026-02-20 12:34:49 +01:00
Martin Felis
a098bc1171 Added test for synced blending of embedded blend tree. 2026-02-19 19:05:14 +01:00
Martin Felis
d01c6fb474 Minor editor fixes. 2026-02-19 19:03:56 +01:00
Martin Felis
06198d595f Added test for embedded blend tree evaluation. 2026-02-18 22:13:51 +01:00
Martin Felis
be6e021198 Restructured Animation Graph Editor to facilitate hierarchical graphs (i.e. embedded sub graphs). 2026-02-17 23:08:36 +01:00
Martin Felis
7255c99ef7 BLTAnimationNodeBlend2.sync is no more exposed as parameter, only as property. 2026-02-05 22:29:00 +01:00
Martin Felis
2cee55037b Renamed BLTAnimationNode::position to BLTAnimationNode::graph_offset and made it hidden in the editor by default. 2026-02-05 22:26:25 +01:00
Martin Felis
f3db8b67d0 Fixed compilation with latest master. 2026-02-05 21:55:18 +01:00
Martin Felis
14329e606e Properly initialize animation player when loading a scene. 2026-02-02 20:38:55 +01:00
Martin Felis
f07b54e42b Minor fixes and typos. 2026-02-02 18:40:32 +01:00
Martin Felis
00ea4b8b7e BlendTreeEditor starts to be usable. 2026-02-02 16:17:33 +01:00
Martin Felis
f1a42302a6 Nodes now properly keep their positions in the blend tree. 2026-02-01 10:39:37 +01:00
Martin Felis
a764222c02 Added missing parameter to BLTAnimationNode::get_input_index binding. 2026-02-01 10:16:18 +01:00
Martin Felis
a2295680d1 Ensure nodes that are added to the tree but not within the subtree of the root are still referenced after sorting. 2026-02-01 10:15:48 +01:00
Martin Felis
0554691e46 WIP: making BlendTree Editor usable. 2026-01-30 15:33:27 +01:00
Martin Felis
6330e34ea5 Renaming SyncedAnimationGraph -> Blendalot 2026-01-29 23:27:36 +01:00
Martin Felis
89c3c38757 Made BlendTree Editor more robust. 2026-01-29 23:21:38 +01:00
Martin Felis
50243eafba Minor additional work on the BlendTreeEditor. 2026-01-28 21:05:16 +01:00
Martin Felis
4c428a865a BlendTree nodes can now be removed. 2026-01-28 21:04:46 +01:00
Martin Felis
1e7dd4ba45 BlendTree connection can now be removed. 2026-01-25 00:05:32 +01:00
Martin Felis
d3fe4afc57 WIP: BlendTree editor. 2026-01-24 15:39:12 +01:00
Martin Felis
fd13c53e52 Exposed additional functions to GDScript to implement a GDScript based EditorPlugin. 2026-01-24 15:38:27 +01:00
Martin Felis
5d0bf10ce7 Prevent crash when trying to evaluate without an existing Skeleton3D. 2026-01-23 12:05:39 +01:00
28 changed files with 1920 additions and 435 deletions

View File

@ -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<BLTAnimationNode> &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<PropertyInfo> *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<AnimationPlayer>(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<BLTAnimationNode> &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<BLTAnimationNode> 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<Skeleton3D>(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<Animation::TypeHash, size_t> &K : output_data.value_buffer_offset) {
const AnimationData::TrackValue *track_value = output_data.get_value<AnimationData::TrackValue>(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<AnimationPlayer>(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<Skeleton3D>(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() {

View File

@ -19,11 +19,12 @@ private:
mutable AHashMap<StringName, Pair<Ref<BLTAnimationNode>, 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<BLTAnimationNode> 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();

View File

@ -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<PropertyInfo> *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<PropertyInfo> *p_list) const {
for (const Ref<BLTAnimationNode> &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<BLTAnimationNode> 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<BLTAnimationNode> target_node = tree_graph.nodes[target_node_index];
Vector<StringName> 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> &animation, const Skeleton3D *skeleton_3d, double p_time) {
GodotProfileZone("AnimationData::sample_from_animation");
const LocalVector<Animation::Track *> tracks = animation->get_tracks();
const LocalVector<Animation::Track *> &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> &animation, const Skeleton3D *skeleton_3d) {
GodotProfileZone("AnimationData::allocate_track_values");
const LocalVector<Animation::Track *> tracks = animation->get_tracks();
const LocalVector<Animation::Track *> &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<Animation> &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<float> 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<float> 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<StringName> BLTAnimationNodeSampler::get_animations_as_typed_array() const {
TypedArray<StringName> typed_arr;
if (animation_player == nullptr) {
print_error(vformat("BLTAnimationNodeSampler '%s' not yet initialized", get_name()));
return typed_arr;
}
Vector<StringName> vec;
List<StringName> animation_libraries;
animation_player->get_animation_library_list(&animation_libraries);
for (const StringName &library_name : animation_libraries) {
Ref<AnimationLibrary> library = animation_player->get_animation_library(library_name);
List<StringName> 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<PropertyInfo> *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<AnimationData *> &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<PropertyInfo> *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<BLTAnimationNodeOutput> output_node;
output_node.instantiate();
output_node->name = "Output";
output_node->set_name("Output");
add_node(output_node);
}
@ -421,7 +421,7 @@ Ref<BLTAnimationNode> BLTAnimationNodeBlendTree::BLTBlendTreeGraph::get_output_n
}
int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index(const Ref<BLTAnimationNode> &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 Ref<BLTA
}
int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_node_index_by_name(const StringName &name) const {
for (int i = 0; i < nodes.size(); i++) {
if (nodes[i]->name == 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<BLTAnimationNode> &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<BLTAnimationNode> &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<int>(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<int> 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<int> sorted_node_indices = get_sorted_node_indices();
Vector<Ref<BLTAnimationNode>> sorted_nodes;
LocalVector<NodeConnectionInfo> old_node_connection_info = node_connection_info;
LocalVector<Ref<BLTAnimationNode>> sorted_nodes;
LocalVector<NodeConnectionInfo> 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<int> BLTAnimationNodeBlendTree::BLTBlendTreeGraph::get_sorted_node_i
sort_nodes_recursive(0, result);
result.reverse();
HashSet<int> 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<BLTAnimationNode> &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<int> &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<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &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<StringName> 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<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &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<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &target_node, StringName target_port_name) const {
int BLTAnimationNodeBlendTree::BLTBlendTreeGraph::find_connection_index(const Ref<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &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<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &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<StringName> 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<PropertyInfo> *p_list) const {
for (const Ref<BLTAnimationNode> &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<BLTAnimationNode> 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<BLTAnimationNode> target_node = tree_graph.nodes[target_node_index];
Vector<StringName> 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;
}

View File

@ -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 <cassert>
@ -104,7 +105,8 @@ struct AnimationData {
buffer = other.buffer;
}
AnimationData(AnimationData &&other) noexcept :
value_buffer_offset(std::exchange(other.value_buffer_offset, AHashMap<Animation::TypeHash, size_t, HashHasher>())),
// 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<Animation::TypeHash, size_t, HashHasher>())),
buffer(std::exchange(other.buffer, LocalVector<uint8_t>())) {
}
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<Ref<BLTAnimationNode>> &input_nodes) {
// By default, all inputs nodes are activated.
for (const Ref<BLTAnimationNode> &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<AnimationData *> &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<StringName> &inputs) const {}
void set_position(const Vector2 &p_position) {
position = p_position;
}
Vector2 get_position() const {
return position;
}
virtual Vector<StringName> get_input_names() const { return {}; }
TypedArray<StringName> get_input_names_as_typed_array() const {
TypedArray<StringName> typed_arr;
Vector<StringName> 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<StringName> inputs;
get_input_names(inputs);
Vector<StringName> inputs = get_input_names();
return inputs.find(port_name);
}
int get_input_count() const {
Vector<StringName> inputs;
get_input_names(inputs);
Vector<StringName> 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<StringName> get_animations_as_typed_array() const;
private:
Ref<Animation> 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> animation;
Vector<StringName> 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<Ref<BLTAnimationNode>> &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<PropertyInfo> *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<StringName> &inputs) const override {
inputs.push_back("Input");
Vector<StringName> 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<StringName> &inputs) const override {
inputs.push_back("Input0");
inputs.push_back("Input1");
Vector<StringName> 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<Ref<BLTAnimationNode>> &input_nodes) override {
input_nodes[0]->active = true;
@ -379,7 +456,8 @@ public:
void calculate_sync_track(const Vector<Ref<BLTAnimationNode>> &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<float>(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<BLTAnimationNode> source_node = nullptr;
const Ref<BLTAnimationNode> target_node = nullptr;
const StringName target_port_name = "";
Ref<BLTAnimationNode> source_node = nullptr;
Ref<BLTAnimationNode> 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<Ref<BLTAnimationNode>> nodes; // All added nodes
LocalVector<Ref<BLTAnimationNode>> nodes; // All added nodes
LocalVector<NodeConnectionInfo> node_connection_info;
LocalVector<BLTBlendTreeConnection> connections;
BLTBlendTreeGraph();
Ref<BLTAnimationNode> get_output_node();
int find_node_index(const Ref<BLTAnimationNode> &node) const;
int find_node_index_by_name(const StringName &name) const;
void sort_nodes_and_references();
LocalVector<int> get_sorted_node_indices();
void sort_nodes_recursive(int node_index, LocalVector<int> &result);
void add_index_and_update_subtrees_recursive(int node, int node_parent);
ConnectionError is_connection_valid(const Ref<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &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<int> &removed_subtree_indices);
void add_node(const Ref<BLTAnimationNode> &node);
bool remove_node(const Ref<BLTAnimationNode> &node);
ConnectionError is_connection_valid(const Ref<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &target_node, StringName target_port_name) const;
ConnectionError add_connection(const Ref<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &target_node, const StringName &target_port_name);
int find_connection_index(const Ref<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &target_node, const StringName &target_port_name) const;
ConnectionError remove_connection(const Ref<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &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<BLTAnimationNode> 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<BLTAnimationNode> 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<Ref<BLTAnimationNode>> input_nodes;
LocalVector<AnimationData *> input_data;
@ -549,53 +651,144 @@ public:
};
LocalVector<NodeRuntimeData> _node_runtime_data;
Ref<BLTAnimationNode> 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<BLTAnimationNode> &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<BLTAnimationNode> get_node(int node_index) {
if (node_index < 0 || node_index > tree_graph.nodes.size()) {
void add_node(const Ref<BLTAnimationNode> &node) {
tree_graph.add_node(node);
if (_graph_evaluation_context != nullptr) {
node->initialize(*_graph_evaluation_context);
}
}
void remove_node(const Ref<BLTAnimationNode> &node) {
if (tree_graph.remove_node(node)) {
_node_changed();
}
}
TypedArray<StringName> get_node_names_as_typed_array() const {
Vector<StringName> vec;
for (const Ref<BLTAnimationNode> &node : tree_graph.nodes) {
vec.push_back(node->get_name());
}
TypedArray<StringName> 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<BLTAnimationNode> 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<BLTAnimationNode> get_node_by_index(int node_index) const {
if (node_index < 0 || node_index > static_cast<int>(tree_graph.nodes.size())) {
return nullptr;
}
return tree_graph.nodes[node_index];
}
void add_node(const Ref<BLTAnimationNode> &node) {
if (tree_initialized) {
print_error("Cannot add node to BlendTree: BlendTree already initialized.");
return;
}
Ref<BLTAnimationNode> get_output_node() const {
return tree_graph.nodes[0];
}
tree_graph.add_node(node);
ConnectionError is_connection_valid(const Ref<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &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<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &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<BLTAnimationNode> &source_node, const Ref<BLTAnimationNode> &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<BLTAnimationNode> &node : tree_graph.nodes) {
const HashSet<int> &output_subtree = tree_graph.node_connection_info[0].input_subtree_node_indices;
for (int i = 0; i < tree_graph.nodes.size(); i++) {
const Ref<BLTAnimationNode> &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<BLTAnimationNode> &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<Ref<BLTAnimationNode>> &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<BLTAnimationNode> &node = tree_graph.nodes[i];
if (!node->active) {
@ -622,7 +822,7 @@ public:
void calculate_sync_track(const Vector<Ref<BLTAnimationNode>> &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<BLTAnimationNode> &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<BLTAnimationNode> &node = tree_graph.nodes[i];
if (!node->active) {
@ -659,9 +866,9 @@ public:
}
void evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &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<BLTAnimationNode> &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;
}
}

22
blendalot_math_helper.h Normal file
View File

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

View File

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

View File

@ -0,0 +1 @@
uid://bxxipuj2s5gxu

View File

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

View File

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

View File

@ -0,0 +1 @@
uid://dr0ndqekm21gy

View File

@ -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"]

View File

@ -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!")

View File

@ -1 +0,0 @@
uid://dvulvuytt81lw

View File

@ -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"]

View File

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

View File

@ -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"

View File

@ -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"]

View File

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

View File

@ -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"]

View File

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

View File

@ -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"]

View File

@ -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"]

View File

@ -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"]

View File

@ -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:

View File

@ -12,6 +12,7 @@ void initialize_blendalot_animgraph_module(ModuleInitializationLevel p_level) {
ClassDB::register_class<BLTAnimationNodeOutput>();
ClassDB::register_class<BLTAnimationNodeBlendTree>();
ClassDB::register_class<BLTAnimationNodeSampler>();
ClassDB::register_class<BLTAnimationNodeTimeScale>();
ClassDB::register_class<BLTAnimationNodeBlend2>();
}

View File

@ -2,6 +2,7 @@
#include "core/templates/local_vector.h"
#include "blendalot_math_helper.h"
#include <cassert>
#include <cmath>
@ -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<float>(result.num_intervals / track_A.num_intervals);
float track_B_repeats = static_cast<float>(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;
}
};

View File

@ -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<Animation> test_animation_a;
Ref<Animation> test_animation_b;
Ref<Animation> test_animation_c;
Ref<Animation> test_animation_sync_a;
Ref<Animation> test_animation_sync_b;
Ref<AnimationLibrary> animation_library;
BLTAnimationGraph *synced_animation_graph;
SyncedAnimationGraphFixture() {
BLTAnimationGraph *animation_graph;
BlendTreeFixture() {
BLTAnimationGraph *scene_animation_graph = dynamic_cast<BLTAnimationGraph *>(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<BLTAnimationGraph *>(SceneTree::get_singleton()->get_root()->find_child("SyncedAnimationGraphFixtureTestNode", true, false));
REQUIRE(synced_animation_graph);
animation_graph = dynamic_cast<BLTAnimationGraph *>(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<Skeleton3D *>((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<BLTAnimationNodeSampler> 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<BLTAnimationNodeSampler> 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<BLTAnimationNodeSampler> 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<BLTAnimationNodeBlend2> node_blend0;
node_blend0.instantiate();
node_blend0->name = "Blend0";
node_blend0->set_name("Blend0");
tree_constructor.add_node(node_blend0);
Ref<BLTAnimationNodeBlend2> 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<BLTAnimationNodeSampler> 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<BLTAnimationNodeBlendTree> 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<BLTAnimationNodeBlendTree> synced_blend_tree_node;
synced_blend_tree_node.instantiate();
@ -329,20 +340,19 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph
// Blend2
Ref<BLTAnimationNodeBlend2> 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<StringName> blend2_inputs;
blend2_node->get_input_names(blend2_inputs);
Vector<StringName> 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<BLTAnimationNodeBlendTree> loaded_synced_blend_tree = ResourceLoader::load("synced_blend_tree_node.tres");
REQUIRE(loaded_synced_blend_tree.is_valid());
Ref<BLTAnimationNodeBlend2> loaded_blend2_node = loaded_synced_blend_tree->get_node(loaded_synced_blend_tree->find_node_index_by_name("Blend2"));
Ref<BLTAnimationNodeBlend2> 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
TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][ChangeConnectivity] BlendTreeGraph with various nodes and connections that are removed") {
BLTAnimationNodeBlendTree::BLTBlendTreeGraph blend_tree_graph;
// TestAnimationA
Ref<BLTAnimationNodeSampler> 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<BLTAnimationNodeSampler> 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<BLTAnimationNodeSampler> 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<BLTAnimationNodeBlend2> 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<BLTAnimationNodeBlend2> 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<int> subgraph_output_initial = blend_tree_graph.node_connection_info[0].input_subtree_node_indices;
HashSet<int> subgraph_blend2a_initial = blend_tree_graph.node_connection_info[blend_tree_graph.find_node_index(blend2_node_a)].input_subtree_node_indices;
HashSet<int> 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<BLTAnimationNodeBlendTree> 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<Ref<BLTAnimationNode>>());
blend_tree_node->calculate_sync_track(Vector<Ref<BLTAnimationNode>>());
blend_tree_node->update_time(0.825);
blend_tree_node->evaluate(graph_context, LocalVector<AnimationData *>(), *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<Ref<BLTAnimationNode>>());
blend_tree_node->calculate_sync_track(Vector<Ref<BLTAnimationNode>>());
blend_tree_node->update_time(0.825);
blend_tree_node->evaluate(graph_context, LocalVector<AnimationData *>(), *graph_output);
}
}
TEST_CASE_FIXTURE(BlendTreeFixture, "[SceneTree][Blendalot][BlendTreeGraph][EmbeddedBlendTree] BlendTree with an embedded BlendTree subgraph") {
// Embedded BlendTree
Ref<BLTAnimationNodeBlendTree> embedded_blend_tree;
embedded_blend_tree.instantiate();
// TestAnimationB
Ref<BLTAnimationNodeSampler> 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<BLTAnimationNodeBlendTree> blend_tree;
blend_tree.instantiate();
// Blend2
Ref<BLTAnimationNodeBlend2> blend2;
blend2.instantiate();
blend2->set_name("Blend2");
blend_tree->add_node(blend2);
// TestAnimationA
Ref<BLTAnimationNodeSampler> 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<Ref<BLTAnimationNode>>());
blend_tree->calculate_sync_track(Vector<Ref<BLTAnimationNode>>());
blend_tree->update_time(0.1);
blend_tree->evaluate(graph_context, LocalVector<AnimationData *>(), *graph_output);
// Check values
AnimationData::TransformTrackValue *hip_transform_value = graph_output->get_value<AnimationData::TransformTrackValue>(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<Ref<BLTAnimationNode>>());
blend_tree->calculate_sync_track(Vector<Ref<BLTAnimationNode>>());
blend_tree->update_time(0.825);
blend_tree->evaluate(graph_context, LocalVector<AnimationData *>(), *graph_output);
// Check values
AnimationData::TransformTrackValue *hip_transform_value = graph_output->get_value<AnimationData::TransformTrackValue>(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

View File

@ -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
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<float>(blended.num_intervals / track_a.num_intervals);
float track_b_repeats = static_cast<float>(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