Added demo, otherwise WIP.

Also needs AnimationMixer to be modified such that SyncedAnimationGraph is a friend class of AnimationMixer.
This commit is contained in:
Martin Felis 2025-11-21 12:28:54 +01:00
parent 9a9adfbfb4
commit c642fa2a04
14 changed files with 450 additions and 26 deletions

29
.gitignore vendored
View File

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

17
README.md Normal file
View File

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

4
demo/.editorconfig Normal file
View File

@ -0,0 +1,4 @@
root = true
[*]
charset = utf-8

20
demo/.gitignore vendored Normal file
View File

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

BIN
demo/animation_library.res Normal file

Binary file not shown.

BIN
demo/assets/MixamoAmy.glb Normal file

Binary file not shown.

View File

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

1
demo/icon.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

After

Width:  |  Height:  |  Size: 995 B

43
demo/icon.svg.import Normal file
View File

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

29
demo/main.tscn Normal file
View File

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

19
demo/project.godot Normal file
View File

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

View File

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

View File

@ -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<AnimationNodeBlendTree> blend_tree = root_animation_node;
// if (!blend_tree.is_valid()) {
// print_line("Cannot process animation graph: root not AnimationNodeBlendTree");
// return;
// }
//
// LocalVector<StringName> 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<Skeleton3D>(get_node_or_null(skeleton_path));
if (!skeleton) {
return;
}
AnimationTree *animation_tree = Object::cast_to<AnimationTree>(get_node_or_null(animation_tree_path));
if (!animation_tree) {
return;
}
Ref<Animation> animation = animation_tree->get_animation("Walk-InPlace");
if (!animation.is_valid()) {
return;
}
// LocalVector<AnimationMixer::TrackCache *> &track_num_to_track_cache = animation_track_num_to_track_cache[a];
const Vector<Animation::Track *> 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<AnimationNodeBlendTree> blend_tree = root_animation_node;
// if (!blend_tree.is_valid()) {
// print_line("Cannot process animation graph: root not AnimationNodeBlendTree");
// return;
// }
//
// LocalVector<StringName> 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;

View File

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