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