diff --git a/.gitignore b/.gitignore
index 77e2c56..55de37c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,31 @@
.*swp
__pycache__
+
+# Godot 4+ specific ignores
+demo/.godot/
+demo/android/
+
+demo/.nomedia
+
+# Godot-specific ignores
+demo/.import/
+demo/export.cfg
+demo/export_credentials.cfg
+demo/*.tmp
+
+# Imported translations (automatically generated from CSV files)
+demo/*.translation
+
+# Mono-specific ignores
+demo/.mono/
+demo/data_*/
+demo/mono_crash.*.json
+
+/demo/assets/MixamoAmy_Ch46_1001_Diffuse.png
+/demo/assets/MixamoAmy_Ch46_1001_Diffuse.png.import
+/demo/assets/MixamoAmy_Ch46_1001_Glossiness.png
+/demo/assets/MixamoAmy_Ch46_1001_Glossiness.png.import
+/demo/assets/MixamoAmy_Ch46_1001_Normal.png
+/demo/assets/MixamoAmy_Ch46_1001_Normal.png.import
+/demo/assets/MixamoAmy_Image.png
+/demo/assets/MixamoAmy_Image.png.import
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2b2c245
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# Synced Animation Graphs for Godot
+
+
+## Questions
+
+1. Given an animation "Walk" with a call-method track and given that it is used as an input to a Blend2 node: will the
+ method be called twice?
+1. a)
+
+## Open Issues
+
+1. Dynamic Track Caches
+
+When AnimationMixer performs blends it c
+
+AnimationMixer still has all sampled Animations and therefore their Tracks individually. However for the SAG
+ evaluation all operations are
diff --git a/demo/.editorconfig b/demo/.editorconfig
new file mode 100644
index 0000000..f28239b
--- /dev/null
+++ b/demo/.editorconfig
@@ -0,0 +1,4 @@
+root = true
+
+[*]
+charset = utf-8
diff --git a/demo/.gitignore b/demo/.gitignore
new file mode 100644
index 0000000..d9d1d19
--- /dev/null
+++ b/demo/.gitignore
@@ -0,0 +1,20 @@
+# Godot 4+ specific ignores
+.godot/
+/android/
+
+.nomedia
+
+# Godot-specific ignores
+.import/
+export.cfg
+export_credentials.cfg
+*.tmp
+
+# Imported translations (automatically generated from CSV files)
+*.translation
+
+# Mono-specific ignores
+.mono/
+data_*/
+mono_crash.*.json
+
diff --git a/demo/animation_library.res b/demo/animation_library.res
new file mode 100644
index 0000000..52580cd
Binary files /dev/null and b/demo/animation_library.res differ
diff --git a/demo/assets/MixamoAmy.glb b/demo/assets/MixamoAmy.glb
new file mode 100644
index 0000000..d77a957
Binary files /dev/null and b/demo/assets/MixamoAmy.glb differ
diff --git a/demo/assets/MixamoAmy.glb.import b/demo/assets/MixamoAmy.glb.import
new file mode 100644
index 0000000..4cb2a09
--- /dev/null
+++ b/demo/assets/MixamoAmy.glb.import
@@ -0,0 +1,42 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://d1xcqdqr1qeu6"
+path="res://.godot/imported/MixamoAmy.glb-5f8a7101bbea7c3cd32f3ebd9335cd1a.scn"
+
+[deps]
+
+source_file="res://assets/MixamoAmy.glb"
+dest_files=["res://.godot/imported/MixamoAmy.glb-5f8a7101bbea7c3cd32f3ebd9335cd1a.scn"]
+
+[params]
+
+nodes/root_type=""
+nodes/root_name=""
+nodes/root_script=null
+nodes/apply_root_scale=true
+nodes/root_scale=1.0
+nodes/import_as_skeleton_bones=false
+nodes/use_name_suffixes=true
+nodes/use_node_type_suffixes=true
+meshes/ensure_tangents=true
+meshes/generate_lods=true
+meshes/create_shadow_meshes=true
+meshes/light_baking=1
+meshes/lightmap_texel_size=0.2
+meshes/force_disable_compression=false
+skins/use_named_skins=true
+animation/import=true
+animation/fps=30
+animation/trimming=false
+animation/remove_immutable_tracks=true
+animation/import_rest_as_RESET=true
+import_script/path=""
+materials/extract=0
+materials/extract_format=0
+materials/extract_path=""
+_subresources={}
+gltf/naming_version=2
+gltf/embedded_image_handling=1
diff --git a/demo/icon.svg b/demo/icon.svg
new file mode 100644
index 0000000..c6bbb7d
--- /dev/null
+++ b/demo/icon.svg
@@ -0,0 +1 @@
+
diff --git a/demo/icon.svg.import b/demo/icon.svg.import
new file mode 100644
index 0000000..454e0c4
--- /dev/null
+++ b/demo/icon.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://4q335xxs2p"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/demo/main.tscn b/demo/main.tscn
new file mode 100644
index 0000000..44e348c
--- /dev/null
+++ b/demo/main.tscn
@@ -0,0 +1,29 @@
+[gd_scene load_steps=4 format=3 uid="uid://svj53e2xoio"]
+
+[ext_resource type="PackedScene" uid="uid://d1xcqdqr1qeu6" path="res://assets/MixamoAmy.glb" id="1_0xm2m"]
+[ext_resource type="AnimationNodeBlendTree" uid="uid://c7o0gt3li5p4g" path="res://walk_limp_blend_tree.tres" id="2_h2yge"]
+[ext_resource type="AnimationLibrary" uid="uid://dwubn740aqx51" path="res://animation_library.res" id="3_h2yge"]
+
+[node name="Node3D" type="Node3D"]
+
+[node name="MixamoAmy" parent="." instance=ExtResource("1_0xm2m")]
+
+[node name="AnimationTree" type="AnimationTree" parent="."]
+active = false
+root_node = NodePath("../MixamoAmy")
+tree_root = ExtResource("2_h2yge")
+anim_player = NodePath("../MixamoAmy/AnimationPlayer")
+parameters/Blend2/blend_amount = 0.44
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
+active = false
+root_node = NodePath("../MixamoAmy")
+libraries = {
+&"animation_library": ExtResource("3_h2yge")
+}
+
+[node name="SyncedAnimationGraph" type="SyncedAnimationGraph" parent="."]
+animation_tree = NodePath("../AnimationTree")
+skeleton = NodePath("../MixamoAmy/Armature/Skeleton3D")
+
+[editable path="MixamoAmy"]
diff --git a/demo/project.godot b/demo/project.godot
new file mode 100644
index 0000000..6571085
--- /dev/null
+++ b/demo/project.godot
@@ -0,0 +1,19 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="Synced Blend Tree Test"
+config/features=PackedStringArray("4.5", "Forward Plus")
+config/icon="res://icon.svg"
+
+[dotnet]
+
+project/assembly_name="Synced Blend Tree Test"
diff --git a/demo/walk_limp_blend_tree.tres b/demo/walk_limp_blend_tree.tres
new file mode 100644
index 0000000..7ce2652
--- /dev/null
+++ b/demo/walk_limp_blend_tree.tres
@@ -0,0 +1,19 @@
+[gd_resource type="AnimationNodeBlendTree" load_steps=4 format=3 uid="uid://c7o0gt3li5p4g"]
+
+[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_fpiwu"]
+animation = &"Limping-InPlace"
+
+[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_4sffn"]
+animation = &"Walk-InPlace"
+
+[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_w6plo"]
+
+[resource]
+nodes/output/position = Vector2(860, 160)
+nodes/Animation/node = SubResource("AnimationNodeAnimation_4sffn")
+nodes/Animation/position = Vector2(280, 100)
+"nodes/Animation 2/node" = SubResource("AnimationNodeAnimation_fpiwu")
+"nodes/Animation 2/position" = Vector2(280, 300)
+nodes/Blend2/node = SubResource("AnimationNodeBlend2_w6plo")
+nodes/Blend2/position = Vector2(640, 160)
+node_connections = [&"output", 0, &"Blend2", &"Blend2", 0, &"Animation", &"Blend2", 1, &"Animation 2"]
diff --git a/synced_animation_graph.cpp b/synced_animation_graph.cpp
index 1848f92..74ffc4d 100644
--- a/synced_animation_graph.cpp
+++ b/synced_animation_graph.cpp
@@ -1,35 +1,108 @@
#include "synced_animation_graph.h"
+#include "core/os/time.h"
+#include "scene/3d/skeleton_3d.h"
#include "scene/animation/animation_player.h"
+
void SyncedAnimationGraph::_bind_methods() {
print_line(vformat("binding methods"));
+ ClassDB::bind_method(D_METHOD("set_active", "active"), &SyncedAnimationGraph::set_active);
+ ClassDB::bind_method(D_METHOD("is_active"), &SyncedAnimationGraph::is_active);
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "active"), "set_active", "is_active");
+
+ ClassDB::bind_method(D_METHOD("set_callback_mode_process", "mode"), &SyncedAnimationGraph::set_callback_mode_process);
+ ClassDB::bind_method(D_METHOD("get_callback_mode_process"), &SyncedAnimationGraph::get_callback_mode_process);
+
+ ClassDB::bind_method(D_METHOD("set_callback_mode_method", "mode"), &SyncedAnimationGraph::set_callback_mode_method);
+ ClassDB::bind_method(D_METHOD("get_callback_mode_method"), &SyncedAnimationGraph::get_callback_mode_method);
+
ClassDB::bind_method(D_METHOD("set_animation_tree", "animation_tree"), &SyncedAnimationGraph::set_animation_tree);
ClassDB::bind_method(D_METHOD("get_animation_tree"), &SyncedAnimationGraph::get_animation_tree);
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "animation_tree", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "AnimationTree"), "set_animation_tree", "get_animation_tree");
+ ClassDB::bind_method(D_METHOD("set_skeleton", "skeleton"), &SyncedAnimationGraph::set_skeleton);
+ ClassDB::bind_method(D_METHOD("get_skeleton"), &SyncedAnimationGraph::get_skeleton);
+ ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "skeleton", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Skeleton3D"), "set_skeleton", "get_skeleton");
+
ADD_SIGNAL(MethodInfo(SNAME("animation_tree_changed")));
}
void SyncedAnimationGraph::_notification(int p_what) {
switch (p_what) {
- case NOTIFICATION_READY: {
+ case Node::NOTIFICATION_READY: {
_set_process(true);
} break;
- case NOTIFICATION_INTERNAL_PROCESS: {
- _process_animation(get_process_delta_time());
+ case Node::NOTIFICATION_INTERNAL_PROCESS: {
+ if (active) {
+ _process_graph(get_process_delta_time());
+ }
} break;
- case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
- _process_animation(get_physics_process_delta_time());
+ case Node::NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
+ if (active) {
+ _process_graph(get_physics_process_delta_time());
+ }
} break;
}
}
+void SyncedAnimationGraph::set_active(bool p_active) {
+ if (active == p_active) {
+ return;
+ }
+
+ active = p_active;
+ _set_process(processing, true);
+}
+
+bool SyncedAnimationGraph::is_active() const {
+ return active;
+}
+
+void SyncedAnimationGraph::set_callback_mode_process(AnimationMixer::AnimationCallbackModeProcess p_mode) {
+ if (callback_mode_process == p_mode) {
+ return;
+ }
+
+ bool was_active = is_active();
+ if (was_active) {
+ set_active(false);
+ }
+
+ callback_mode_process = p_mode;
+
+ if (was_active) {
+ set_active(true);
+ }
+}
+
+AnimationMixer::AnimationCallbackModeProcess SyncedAnimationGraph::get_callback_mode_process() const {
+ return callback_mode_process;
+}
+
+void SyncedAnimationGraph::set_callback_mode_method(AnimationMixer::AnimationCallbackModeMethod p_mode) {
+ callback_mode_method = p_mode;
+ emit_signal(SNAME("mixer_updated"));
+}
+
+AnimationMixer::AnimationCallbackModeMethod SyncedAnimationGraph::get_callback_mode_method() const {
+ return callback_mode_method;
+}
+
+void SyncedAnimationGraph::set_callback_mode_discrete(AnimationMixer::AnimationCallbackModeDiscrete p_mode) {
+ callback_mode_discrete = p_mode;
+ emit_signal(SNAME("mixer_updated"));
+}
+
+AnimationMixer::AnimationCallbackModeDiscrete SyncedAnimationGraph::get_callback_mode_discrete() const {
+ return callback_mode_discrete;
+}
+
void SyncedAnimationGraph::set_animation_tree(const NodePath &p_path) {
- animation_tree = p_path;
+ animation_tree_path = p_path;
if (p_path.is_empty()) {
// set_root_node(SceneStringName(path_pp));
// while (animation_libraries.size()) {
@@ -40,31 +113,134 @@ void SyncedAnimationGraph::set_animation_tree(const NodePath &p_path) {
}
NodePath SyncedAnimationGraph::get_animation_tree() const {
- return animation_tree;
+ return animation_tree_path;
+}
+
+void SyncedAnimationGraph::set_skeleton(const NodePath &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);
+ // }
+ }
+ emit_signal(SNAME("skeleton_changed")); // Needs to unpin AnimationPlayerEditor.
+}
+
+NodePath SyncedAnimationGraph::get_skeleton() const {
+ return skeleton_path;
}
void SyncedAnimationGraph::_ready(const NodePath &p_path) {
print_line(vformat("synced animation graph ready!"));
}
-void SyncedAnimationGraph::_process_animation(double p_delta, bool p_update_only) {
- print_line(vformat("updating blend tree! %f", p_delta));
- // if (!root_animation_node.is_valid()) {
- // return;
- // }
- //
- // Ref blend_tree = root_animation_node;
- // if (!blend_tree.is_valid()) {
- // print_line("Cannot process animation graph: root not AnimationNodeBlendTree");
- // return;
- // }
- //
- // LocalVector node_names = blend_tree->get_node_list();
- // for (StringName node_name : node_names) {
- // print_line(vformat(" %s", node_name));
- // }
+void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) {
+ if (skeleton_path.is_empty()) {
+ return;
+ }
+
+ float current_time = Time::get_singleton()->get_unix_time_from_system();
+ Skeleton3D *skeleton = Object::cast_to(get_node_or_null(skeleton_path));
+ if (!skeleton) {
+ return;
+ }
+
+ AnimationTree *animation_tree = Object::cast_to(get_node_or_null(animation_tree_path));
+ if (!animation_tree) {
+ return;
+ }
+
+ Ref animation = animation_tree->get_animation("Walk-InPlace");
+ if (!animation.is_valid()) {
+ return;
+ }
+
+
+ // LocalVector &track_num_to_track_cache = animation_track_num_to_track_cache[a];
+ const Vector tracks = animation->get_tracks();
+ Animation::Track *const *tracks_ptr = tracks.ptr();
+ // real_t a_length = animation->get_length();
+ int count = tracks.size();
+ for (int i = 0; i < count; i++) {
+ const Animation::Track *animation_track = tracks_ptr[i];
+ if (!animation_track->enabled) {
+ continue;
+ }
+
+ Animation::TrackType ttype = animation_track->type;
+ switch (ttype) {
+ case Animation::TYPE_POSITION_3D: {
+ AnimationMixer::TrackCacheTransform *track_xform = memnew(AnimationMixer::TrackCacheTransform);
+ track_xform->type = Animation::TYPE_POSITION_3D;
+ track_xform->bone_idx = -1;
+ track_xform->skeleton_id = skeleton->get_instance_id();
+ NodePath path = animation->track_get_path(i);
+
+ double animation_time = Math::fposmod(current_time, animation->get_length());
+ if (path.get_subname_count() == 1) {
+ int bone_idx = skeleton->find_bone(path.get_subname(0));
+ if (bone_idx != -1) {
+ track_xform->bone_idx = bone_idx;
+ Vector3 pos;
+ animation->try_position_track_interpolate(i, animation_time, &pos);
+ skeleton->set_bone_pose_position(bone_idx, pos);
+ }
+ }
+ break;
+ }
+ case Animation::TYPE_ROTATION_3D: {
+ AnimationMixer::TrackCacheTransform *track_xform = memnew(AnimationMixer::TrackCacheTransform);
+ track_xform->type = Animation::TYPE_POSITION_3D;
+ track_xform->bone_idx = -1;
+ track_xform->skeleton_id = skeleton->get_instance_id();
+ NodePath path = animation->track_get_path(i);
+
+ double animation_time = Math::fposmod(current_time, animation->get_length());
+ if (path.get_subname_count() == 1) {
+ int bone_idx = skeleton->find_bone(path.get_subname(0));
+ if (bone_idx != -1) {
+ track_xform->bone_idx = bone_idx;
+ Quaternion rot;
+ animation->try_rotation_track_interpolate(i, animation_time, &rot);
+ skeleton->set_bone_pose_rotation(bone_idx, rot);
+ }
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+
+ }
+ }
+
+// skeleton->set_bone_pose_position(3, Vector3(sin(current_time) * 10., 0., 0.));
+ skeleton->force_update_all_bone_transforms();
+
+
+ // TrackCache *track = track_num_to_track_cache[i];
+ // if (track == nullptr) {
+ // continue; // No path, but avoid error spamming.
+ // }
}
+// if (!root_animation_node.is_valid()) {
+// return;
+// }
+//
+// Ref blend_tree = root_animation_node;
+// if (!blend_tree.is_valid()) {
+// print_line("Cannot process animation graph: root not AnimationNodeBlendTree");
+// return;
+// }
+//
+// LocalVector node_names = blend_tree->get_node_list();
+// for (StringName node_name : node_names) {
+// print_line(vformat(" %s", node_name));
+// }
+
+
void SyncedAnimationGraph::_set_process(bool p_process, bool p_force) {
if (processing == p_process && !p_force) {
return;
diff --git a/synced_animation_graph.h b/synced_animation_graph.h
index 2594268..27d9748 100644
--- a/synced_animation_graph.h
+++ b/synced_animation_graph.h
@@ -7,19 +7,44 @@ class SyncedAnimationGraph : public Node {
GDCLASS(SyncedAnimationGraph, Node);
private:
- NodePath animation_tree;
- bool processing = false;
+ NodePath animation_tree_path;
+ NodePath skeleton_path;
void set_animation_tree(const NodePath &p_path);
NodePath get_animation_tree() const;
+ void set_skeleton(const NodePath &p_path);
+ NodePath get_skeleton() const;
+
+ // AnimationMixer::TrackCache
+
protected:
void _notification(int p_what);
static void _bind_methods();
+ /* ---- General settings for animation ---- */
+ AnimationMixer::AnimationCallbackModeProcess callback_mode_process = AnimationMixer::ANIMATION_CALLBACK_MODE_PROCESS_IDLE;
+ AnimationMixer::AnimationCallbackModeMethod callback_mode_method = AnimationMixer::ANIMATION_CALLBACK_MODE_METHOD_DEFERRED;
+ AnimationMixer::AnimationCallbackModeDiscrete callback_mode_discrete = AnimationMixer::ANIMATION_CALLBACK_MODE_DISCRETE_RECESSIVE;
+
+ bool processing = false;
+ bool active = true;
+
public:
void _ready(const NodePath &p_path);
- void _process_animation(double p_delta, bool p_update_only = false);
+ void _process_graph(double p_delta, bool p_update_only = false);
+
+ void set_active(bool p_active);
+ bool is_active() const;
+
+ void set_callback_mode_process(AnimationMixer::AnimationCallbackModeProcess p_mode);
+ AnimationMixer::AnimationCallbackModeProcess get_callback_mode_process() const;
+
+ void set_callback_mode_method(AnimationMixer::AnimationCallbackModeMethod p_mode);
+ AnimationMixer::AnimationCallbackModeMethod get_callback_mode_method() const;
+
+ void set_callback_mode_discrete(AnimationMixer::AnimationCallbackModeDiscrete p_mode);
+ AnimationMixer::AnimationCallbackModeDiscrete get_callback_mode_discrete() const;
SyncedAnimationGraph();