Compare commits

..

5 Commits

Author SHA1 Message Date
Martin Felis
ecf3b0fef2 Substantial performance improvements by refactoring AnimationData allocations.
AnimationData is now a buffer and a hashmap with offsets of the TrackValues. During graph initialization all used TrackValues get registered and their offsets computed.

AnimationData should now be allocated by the AnimationDataAllocator. It takes care of pooling already allocated AnimationDatas and also has a buffer block that contains the default values making it fast to allocate a new AnimationData with the initial pose / default values.
2026-01-16 15:27:33 +01:00
Martin Felis
0e38a2ef65 Minor performance improvements. 2026-01-16 09:54:59 +01:00
Martin Felis
a5577eceea Added profiling statements to SyncedAnimationGraph. 2026-01-16 09:53:18 +01:00
Martin Felis
338a77d5e2 Renaming to Blendalot AnimGraph. 2026-01-13 21:08:50 +01:00
Martin Felis
2b7cf5bc66 Improved the demo. 2026-01-12 22:23:24 +01:00
16 changed files with 403 additions and 183 deletions

View File

@ -1,9 +1,9 @@
# Blendalot - A Magical Animation System for Godot
**Status**: This is a very much work in progress repository. Very rough drafts of the design and API can be found in the
doc folder.
Blendalot is an experimental animation system for Godot that is currently in development.
Blendalot is an experimental animation system for Godot that is currently in development. One of it's core features is a
very flexible animation syncing mechanism that allows smooth transitions between related motions (e.g. walking, running,
limping , ...). This is done by using SyncTracks as described by Bobby Anguelov
here: https://www.youtube.com/watch?v=Jkv0pbp0ckQ&t=7998s.
Stay tuned for more...

View File

@ -0,0 +1,19 @@
[gd_resource type="AnimationNodeBlendTree" load_steps=4 format=3 uid="uid://dqy0dgwsm8t46"]
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_h2yge"]
animation = &"Limping-InPlace"
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_1bvp3"]
animation = &"Walk-InPlace"
[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_lquwl"]
[resource]
nodes/output/position = Vector2(540, 140)
nodes/Animation/node = SubResource("AnimationNodeAnimation_1bvp3")
nodes/Animation/position = Vector2(120, 80)
"nodes/Animation 2/node" = SubResource("AnimationNodeAnimation_h2yge")
"nodes/Animation 2/position" = Vector2(80, 320)
nodes/Blend2/node = SubResource("AnimationNodeBlend2_lquwl")
nodes/Blend2/position = Vector2(360, 180)
node_connections = [&"output", 0, &"Blend2", &"Blend2", 0, &"Animation", &"Blend2", 1, &"Animation 2"]

View File

@ -0,0 +1,19 @@
[gd_resource type="AnimationNodeBlendTree" load_steps=4 format=3 uid="uid://vsf71o82lkld"]
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_h2yge"]
animation = &"Run-InPlace"
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_1bvp3"]
animation = &"Walk-InPlace"
[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_lquwl"]
[resource]
nodes/output/position = Vector2(540, 140)
nodes/Animation/node = SubResource("AnimationNodeAnimation_1bvp3")
nodes/Animation/position = Vector2(120, 80)
"nodes/Animation 2/node" = SubResource("AnimationNodeAnimation_h2yge")
"nodes/Animation 2/position" = Vector2(80, 320)
nodes/Blend2/node = SubResource("AnimationNodeBlend2_lquwl")
nodes/Blend2/position = Vector2(360, 180)
node_connections = [&"output", 0, &"Blend2", &"Blend2", 0, &"Animation", &"Blend2", 1, &"Animation 2"]

View File

@ -1,19 +1,26 @@
extends Node3D
@onready var synced_animation_graph: SyncedAnimationGraph = %SyncedAnimationGraph
@onready var animation_tree: AnimationTree = %AnimationTree
@onready var mixamo_amy_walk_limp: Node3D = %MixamoAmyWalkLimp
@onready var mixamo_amy_walk_limp_synced: Node3D = %MixamoAmyWalkLimpSynced
@onready var mixamo_amy_walk_run: Node3D = %MixamoAmyWalkRun
@onready var mixamo_amy_walk_run_synced: Node3D = %MixamoAmyWalkRunSynced
@onready var blend_weight_slider: HSlider = %BlendWeightSlider
@onready var blend_weight_label: Label = %BlendWeightLabel
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
blend_weight_slider.value = 0.0
blend_weight_slider.value = 0.5
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
pass
func _on_blend_weight_slider_value_changed(value: float) -> void:
animation_tree.set("parameters/Blend2/blend_amount", value)
synced_animation_graph.set("parameters/AnimationBlend2Node/blend_amount", value)
mixamo_amy_walk_limp.get_node("AnimationTree").set("parameters/Blend2/blend_amount", value)
mixamo_amy_walk_limp_synced.get_node("SyncedAnimationGraph").set("parameters/AnimationBlend2Node/blend_amount", value)
mixamo_amy_walk_run.get_node("AnimationTree").set("parameters/Blend2/blend_amount", value)
mixamo_amy_walk_run_synced.get_node("SyncedAnimationGraph").set("parameters/AnimationBlend2Node/blend_amount", value)
blend_weight_label.text = str(value)

View File

@ -3,7 +3,14 @@
[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="SyncedBlendTree" uid="uid://bijslmj4wd7ap" path="res://synced_blend_tree_node_limping.tres" id="4_1bvp3"]
[ext_resource type="AnimationNodeBlendTree" uid="uid://dqy0dgwsm8t46" path="res://animation_tree_walk_limp.tres" id="3_272bh"]
[ext_resource type="SyncedBlendTree" uid="uid://2qfwr1xkiw0s" path="res://synced_blend_tree_walk_limp.tres" id="4_lquwl"]
[ext_resource type="SyncedBlendTree" 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"]
[sub_resource type="Theme" id="Theme_272bh"]
default_font_size = 30
Label/fonts/DefaultFont = null
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_h2yge"]
albedo_color = Color(0.427493, 0.42749307, 0.42749307, 1)
@ -25,24 +32,6 @@ sky = SubResource("Sky_1bvp3")
tonemap_mode = 2
glow_enabled = true
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_h2yge"]
animation = &"Limping-InPlace"
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_1bvp3"]
animation = &"Walk-InPlace"
[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_lquwl"]
[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_7mycd"]
nodes/output/position = Vector2(540, 140)
nodes/Animation/node = SubResource("AnimationNodeAnimation_1bvp3")
nodes/Animation/position = Vector2(120, 80)
"nodes/Animation 2/node" = SubResource("AnimationNodeAnimation_h2yge")
"nodes/Animation 2/position" = Vector2(80, 320)
nodes/Blend2/node = SubResource("AnimationNodeBlend2_lquwl")
nodes/Blend2/position = Vector2(360, 180)
node_connections = [&"output", 0, &"Blend2", &"Blend2", 0, &"Animation", &"Blend2", 1, &"Animation 2"]
[node name="Main" type="Node3D"]
script = ExtResource("1_1bvp3")
@ -61,6 +50,7 @@ grow_horizontal = 2
grow_vertical = 0
size_flags_horizontal = 4
size_flags_vertical = 8
theme = SubResource("Theme_272bh")
[node name="HBoxContainer" type="HBoxContainer" parent="UI/MarginContainer"]
layout_mode = 2
@ -80,8 +70,36 @@ step = 0.001
[node name="BlendWeightLabel" type="Label" parent="UI/MarginContainer/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
text = "0.0"
horizontal_alignment = 2
[node name="MarginContainer2" type="MarginContainer" parent="UI"]
anchors_preset = 10
anchor_right = 1.0
offset_bottom = 23.0
grow_horizontal = 2
theme_override_constants/margin_top = 80
[node name="HBoxContainer2" type="HBoxContainer" parent="UI/MarginContainer2"]
layout_mode = 2
size_flags_vertical = 0
theme_override_constants/separation = 400
alignment = 1
[node name="Label" type="Label" parent="UI/MarginContainer2/HBoxContainer2"]
layout_mode = 2
theme_override_font_sizes/font_size = 32
text = "Unsynced"
[node name="EmptyLabel" type="Label" parent="UI/MarginContainer2/HBoxContainer2"]
layout_mode = 2
[node name="Label3" type="Label" parent="UI/MarginContainer2/HBoxContainer2"]
layout_mode = 2
theme_override_font_sizes/font_size = 32
text = "Synced"
[node name="Level" type="Node3D" parent="."]
@ -97,36 +115,62 @@ shadow_enabled = true
environment = SubResource("Environment_lquwl")
[node name="Camera3D" type="Camera3D" parent="Level"]
transform = Transform3D(1, 0, 0, 0, 0.9897887, 0.14254257, 0, -0.14254257, 0.9897887, 0, 0.89188766, 1.4517534)
transform = Transform3D(1, 0, 0, 0, 0.95413065, 0.29939055, 0, -0.29939055, 0.95413065, 0, 1.649, 3.197)
fov = 36.8
[node name="Characters" type="Node3D" parent="."]
[node name="MixamoAmy" parent="Characters" instance=ExtResource("1_0xm2m")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.5, 0, 0)
[node name="AnimationTree" type="AnimationTree" parent="Characters/MixamoAmy"]
[node name="MixamoAmyWalkLimp" parent="Characters" instance=ExtResource("1_0xm2m")]
unique_name_in_owner = true
root_node = NodePath("%AnimationTree/..")
tree_root = SubResource("AnimationNodeBlendTree_7mycd")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.4, 0, 0)
[node name="AnimationTree" type="AnimationTree" parent="Characters/MixamoAmyWalkLimp"]
tree_root = ExtResource("3_272bh")
anim_player = NodePath("../AnimationPlayer")
parameters/Blend2/blend_amount = 0.5
parameters/Blend2/blend_amount = 0.0
[node name="MixamoAmySynced" parent="Characters" instance=ExtResource("1_0xm2m")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5, 0, 0)
[node name="MixamoAmyWalkRun" parent="Characters" instance=ExtResource("1_0xm2m")]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.6, 0, 0)
[node name="AnimationPlayer2" type="AnimationPlayer" parent="Characters/MixamoAmySynced"]
[node name="AnimationTree" type="AnimationTree" parent="Characters/MixamoAmyWalkRun"]
tree_root = ExtResource("6_5vw27")
anim_player = NodePath("../AnimationPlayer")
parameters/Blend2/blend_amount = 0.0
[node name="MixamoAmyWalkLimpSynced" parent="Characters" instance=ExtResource("1_0xm2m")]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6, 0, 0)
[node name="AnimationPlayer2" type="AnimationPlayer" parent="Characters/MixamoAmyWalkLimpSynced"]
libraries = {
&"animation_library": ExtResource("3_1bvp3")
}
[node name="SyncedAnimationGraph" type="SyncedAnimationGraph" parent="Characters/MixamoAmySynced"]
unique_name_in_owner = true
[node name="SyncedAnimationGraph" type="SyncedAnimationGraph" parent="Characters/MixamoAmyWalkLimpSynced"]
animation_player = NodePath("../AnimationPlayer2")
tree_root = ExtResource("4_1bvp3")
tree_root = ExtResource("4_lquwl")
skeleton = NodePath("../Armature/Skeleton3D")
parameters/AnimationBlend2Node/blend_amount = 0.5
parameters/AnimationBlend2Node/blend_amount = 0.0
[node name="MixamoAmyWalkRunSynced" parent="Characters" 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="AnimationPlayer2" type="AnimationPlayer" parent="Characters/MixamoAmyWalkRunSynced"]
libraries = {
&"animation_library": ExtResource("3_1bvp3")
}
[node name="SyncedAnimationGraph" type="SyncedAnimationGraph" parent="Characters/MixamoAmyWalkRunSynced"]
animation_player = NodePath("../AnimationPlayer2")
tree_root = ExtResource("5_7mycd")
skeleton = NodePath("../Armature/Skeleton3D")
parameters/AnimationBlend2Node/blend_amount = 0.0
[connection signal="value_changed" from="UI/MarginContainer/HBoxContainer/BlendWeightSlider" to="." method="_on_blend_weight_slider_value_changed"]
[editable path="Characters/MixamoAmy"]
[editable path="Characters/MixamoAmySynced"]
[editable path="Characters/MixamoAmyWalkLimp"]
[editable path="Characters/MixamoAmyWalkRun"]
[editable path="Characters/MixamoAmyWalkLimpSynced"]
[editable path="Characters/MixamoAmyWalkRunSynced"]

View File

@ -15,6 +15,11 @@ run/main_scene="uid://svj53e2xoio"
config/features=PackedStringArray("4.5", "Forward Plus")
config/icon="res://icon.svg"
[display]
window/size/viewport_width=1900
window/size/viewport_height=1024
[dotnet]
project/assembly_name="Synced Blend Tree Test"

View File

@ -1,12 +1,15 @@
[gd_resource type="SyncedBlendTree" load_steps=4 format=3 uid="uid://de41u8rkjnjyk"]
[gd_resource type="SyncedBlendTree" load_steps=4 format=3]
[sub_resource type="AnimationSamplerNode" id="AnimationSamplerNode_bvt3d"]
animation = &"animation_library/Run-InPlace"
animation = &"animation_library/TestAnimationB"
[sub_resource type="AnimationSamplerNode" id="AnimationSamplerNode_sntl5"]
animation = &"animation_library/Walk-InPlace"
animation = &"animation_library/TestAnimationA"
[sub_resource type="AnimationBlend2Node" id="AnimationBlend2Node_n4m28"]
sync = false
blend_amount = 0.5
sync = false
[resource]
nodes/Blend2/node = SubResource("AnimationBlend2Node_n4m28")

View File

@ -0,0 +1,18 @@
[gd_resource type="SyncedBlendTree" load_steps=4 format=3 uid="uid://2qfwr1xkiw0s"]
[sub_resource type="AnimationBlend2Node" id="AnimationBlend2Node_bvt3d"]
[sub_resource type="AnimationSamplerNode" id="AnimationSamplerNode_sntl5"]
animation = &"animation_library/Limping-InPlace"
[sub_resource type="AnimationSamplerNode" id="AnimationSamplerNode_n4m28"]
animation = &"animation_library/Walk-InPlace"
[resource]
nodes/AnimationBlend2Node/node = SubResource("AnimationBlend2Node_bvt3d")
nodes/AnimationBlend2Node/position = Vector2(0, 0)
"nodes/AnimationSamplerNode 1/node" = SubResource("AnimationSamplerNode_sntl5")
"nodes/AnimationSamplerNode 1/position" = Vector2(0, 0)
nodes/AnimationSamplerNode/node = SubResource("AnimationSamplerNode_n4m28")
nodes/AnimationSamplerNode/position = Vector2(0, 0)
node_connections = [&"AnimationBlend2Node", 0, &"AnimationSamplerNode", &"AnimationBlend2Node", 1, &"AnimationSamplerNode 1", &"Output", 0, &"AnimationBlend2Node"]

View File

@ -0,0 +1,18 @@
[gd_resource type="SyncedBlendTree" load_steps=4 format=3 uid="uid://qsk64ax2o47f"]
[sub_resource type="AnimationBlend2Node" id="AnimationBlend2Node_bvt3d"]
[sub_resource type="AnimationSamplerNode" id="AnimationSamplerNode_sntl5"]
animation = &"animation_library/Run-InPlace"
[sub_resource type="AnimationSamplerNode" id="AnimationSamplerNode_n4m28"]
animation = &"animation_library/Walk-InPlace"
[resource]
nodes/AnimationBlend2Node/node = SubResource("AnimationBlend2Node_bvt3d")
nodes/AnimationBlend2Node/position = Vector2(0, 0)
"nodes/AnimationSamplerNode 1/node" = SubResource("AnimationSamplerNode_sntl5")
"nodes/AnimationSamplerNode 1/position" = Vector2(0, 0)
nodes/AnimationSamplerNode/node = SubResource("AnimationSamplerNode_n4m28")
nodes/AnimationSamplerNode/position = Vector2(0, 0)
node_connections = [&"AnimationBlend2Node", 0, &"AnimationSamplerNode", &"AnimationBlend2Node", 1, &"AnimationSamplerNode 1", &"Output", 0, &"AnimationBlend2Node"]

View File

@ -3,7 +3,7 @@
#include "core/object/class_db.h"
#include "synced_animation_graph.h"
void initialize_synced_blend_tree_module(ModuleInitializationLevel p_level) {
void initialize_blendalot_animgraph_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
@ -14,7 +14,7 @@ void initialize_synced_blend_tree_module(ModuleInitializationLevel p_level) {
ClassDB::register_class<AnimationBlend2Node>();
}
void uninitialize_synced_blend_tree_module(ModuleInitializationLevel p_level) {
void uninitialize_blendalot_animgraph_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}

View File

@ -1,4 +1,4 @@
#include "modules/register_module_types.h"
void initialize_synced_blend_tree_module(ModuleInitializationLevel p_level);
void uninitialize_synced_blend_tree_module(ModuleInitializationLevel p_level);
void initialize_blendalot_animgraph_module(ModuleInitializationLevel p_level);
void uninitialize_blendalot_animgraph_module(ModuleInitializationLevel p_level);

View File

@ -1,6 +1,7 @@
#include "synced_animation_graph.h"
#include "core/os/time.h"
#include "core/profiling/profiling.h"
#include "scene/3d/skeleton_3d.h"
#include "scene/animation/animation_player.h"
@ -141,6 +142,8 @@ void SyncedAnimationGraph::_tree_changed() {
}
void SyncedAnimationGraph::_notification(int p_what) {
GodotProfileZone("SyncedAnimationGraph::_notification");
switch (p_what) {
case Node::NOTIFICATION_READY: {
_setup_evaluation_context();
@ -292,41 +295,41 @@ void SyncedAnimationGraph::_process_graph(double p_delta, bool p_update_only) {
return;
}
GodotProfileZone("SyncedAnimationGraph::_process_graph");
_update_properties();
AnimationData *graph_output = graph_context.animation_data_allocator.allocate();
root_animation_node->activate_inputs(Vector<Ref<SyncedAnimationNode>>());
root_animation_node->calculate_sync_track(Vector<Ref<SyncedAnimationNode>>());
root_animation_node->update_time(p_delta);
root_animation_node->evaluate(graph_context, LocalVector<AnimationData *>(), graph_output);
root_animation_node->evaluate(graph_context, LocalVector<AnimationData *>(), *graph_output);
_apply_animation_data(graph_output);
_apply_animation_data(*graph_output);
graph_context.animation_data_allocator.free(graph_output);
}
void SyncedAnimationGraph::_apply_animation_data(const AnimationData &output_data) const {
for (const KeyValue<Animation::TypeHash, AnimationData::TrackValue *> &K : output_data.track_values) {
const AnimationData::TrackValue *track_value = K.value;
GodotProfileZone("SyncedAnimationGraph::_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);
switch (track_value->type) {
case AnimationData::TrackType::TYPE_POSITION_3D:
case AnimationData::TrackType::TYPE_ROTATION_3D: {
const AnimationData::TransformTrackValue *transform_track_value = static_cast<const AnimationData::TransformTrackValue *>(track_value);
int bone_idx = -1;
NodePath path = transform_track_value->track->path;
if (path.get_subname_count() == 1) {
bone_idx = graph_context.skeleton_3d->find_bone(path.get_subname(0));
if (bone_idx != -1) {
if (transform_track_value->loc_used) {
graph_context.skeleton_3d->set_bone_pose_position(transform_track_value->bone_idx, transform_track_value->loc);
}
if (transform_track_value->rot_used) {
graph_context.skeleton_3d->set_bone_pose_rotation(transform_track_value->bone_idx, transform_track_value->rot);
}
} else {
assert(false && "Not yet implemented!");
if (transform_track_value->bone_idx != -1) {
if (transform_track_value->loc_used) {
graph_context.skeleton_3d->set_bone_pose_position(transform_track_value->bone_idx, transform_track_value->loc);
}
if (transform_track_value->rot_used) {
graph_context.skeleton_3d->set_bone_pose_rotation(transform_track_value->bone_idx, transform_track_value->rot);
}
} else {
assert(false && "Not yet implemented!");
}
break;
@ -337,8 +340,6 @@ void SyncedAnimationGraph::_apply_animation_data(const AnimationData &output_dat
}
}
}
graph_context.skeleton_3d->force_update_all_bone_transforms();
}
void SyncedAnimationGraph::_set_process(bool p_process, bool p_force) {

View File

@ -3,8 +3,6 @@
#include "scene/animation/animation_player.h"
#include "synced_animation_node.h"
#include <cassert>
class Skeleton3D;
class SyncedAnimationGraph : public Node {
@ -16,7 +14,6 @@ private:
NodePath skeleton_path;
GraphEvaluationContext graph_context = {};
AnimationData graph_output;
mutable List<PropertyInfo> properties;
mutable AHashMap<StringName, Pair<Ref<SyncedAnimationNode>, StringName>> parameter_to_node_parameter_map;

View File

@ -135,14 +135,14 @@ bool SyncedBlendTree::_set(const StringName &p_name, const Variant &p_value) {
}
void AnimationData::sample_from_animation(const Ref<Animation> &animation, const Skeleton3D *skeleton_3d, double p_time) {
const Vector<Animation::Track *> tracks = animation->get_tracks();
GodotProfileZone("AnimationData::sample_from_animation");
const LocalVector<Animation::Track *> tracks = animation->get_tracks();
Animation::Track *const *tracks_ptr = tracks.ptr();
int count = tracks.size();
for (int i = 0; i < count; i++) {
TrackValue *track_value = nullptr;
const Animation::Track *animation_track = tracks_ptr[i];
const NodePath &track_node_path = animation_track->path;
if (!animation_track->enabled) {
continue;
}
@ -151,41 +151,29 @@ void AnimationData::sample_from_animation(const Ref<Animation> &animation, const
switch (ttype) {
case Animation::TYPE_POSITION_3D:
case Animation::TYPE_ROTATION_3D: {
TransformTrackValue *transform_track_value = nullptr;
if (track_values.has(animation_track->thash)) {
transform_track_value = static_cast<TransformTrackValue *>(track_values[animation_track->thash]);
} else {
transform_track_value = memnew(AnimationData::TransformTrackValue);
}
int bone_idx = -1;
TransformTrackValue *transform_track_value = get_value<TransformTrackValue>(animation_track->thash);
if (track_node_path.get_subname_count() == 1) {
bone_idx = skeleton_3d->find_bone(track_node_path.get_subname(0));
if (bone_idx != -1) {
transform_track_value->bone_idx = bone_idx;
switch (ttype) {
case Animation::TYPE_POSITION_3D: {
animation->try_position_track_interpolate(i, p_time, &transform_track_value->loc);
transform_track_value->loc_used = true;
break;
}
case Animation::TYPE_ROTATION_3D: {
animation->try_rotation_track_interpolate(i, p_time, &transform_track_value->rot);
transform_track_value->rot_used = true;
break;
}
default: {
assert(false && !"Not yet implemented");
break;
}
if (transform_track_value->bone_idx != -1) {
switch (ttype) {
case Animation::TYPE_POSITION_3D: {
animation->try_position_track_interpolate(i, p_time, &transform_track_value->loc);
transform_track_value->loc_used = true;
break;
}
case Animation::TYPE_ROTATION_3D: {
animation->try_rotation_track_interpolate(i, p_time, &transform_track_value->rot);
transform_track_value->rot_used = true;
break;
}
default: {
assert(false && !"Not yet implemented");
break;
}
}
} else {
// TODO
assert(false && !"Not yet implemented");
}
track_value = transform_track_value;
break;
}
default: {
@ -194,12 +182,63 @@ void AnimationData::sample_from_animation(const Ref<Animation> &animation, const
break;
}
}
track_value->track = tracks_ptr[i];
set_value(animation_track->thash, track_value);
}
}
void AnimationData::allocate_track_value(const Animation::Track *animation_track, const Skeleton3D *skeleton_3d) {
switch (animation_track->type) {
case Animation::TrackType::TYPE_ROTATION_3D:
case Animation::TrackType::TYPE_POSITION_3D: {
size_t value_offset = 0;
AnimationData::TransformTrackValue *transform_track_value = nullptr;
if (value_buffer_offset.has(animation_track->thash)) {
value_offset = value_buffer_offset[animation_track->thash];
transform_track_value = reinterpret_cast<AnimationData::TransformTrackValue *>(&buffer[value_offset]);
} else {
value_offset = buffer.size();
value_buffer_offset.insert(animation_track->thash, buffer.size());
buffer.resize(buffer.size() + sizeof(AnimationData::TransformTrackValue));
transform_track_value = new (reinterpret_cast<AnimationData::TransformTrackValue *>(&buffer[value_offset])) AnimationData::TransformTrackValue();
}
assert(transform_track_value != nullptr);
if (animation_track->path.get_subname_count() == 1) {
transform_track_value->bone_idx = skeleton_3d->find_bone(animation_track->path.get_subname(0));
}
if (animation_track->type == Animation::TrackType::TYPE_POSITION_3D) {
transform_track_value->loc_used = true;
} else if (animation_track->type == Animation::TrackType::TYPE_ROTATION_3D) {
transform_track_value->rot_used = true;
}
break;
}
default:
break;
}
}
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();
Animation::Track *const *tracks_ptr = tracks.ptr();
int count = tracks.size();
for (int i = 0; i < count; i++) {
const Animation::Track *animation_track = tracks_ptr[i];
if (!animation_track->enabled) {
continue;
}
allocate_track_value(animation_track, skeleton_3d);
}
}
void AnimationDataAllocator::register_track_values(const Ref<Animation> &animation, const Skeleton3D *skeleton_3d) {
default_data.allocate_track_values(animation, skeleton_3d);
}
bool AnimationSamplerNode::initialize(GraphEvaluationContext &context) {
SyncedAnimationNode::initialize(context);
@ -209,6 +248,8 @@ bool AnimationSamplerNode::initialize(GraphEvaluationContext &context) {
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
@ -250,13 +291,14 @@ void AnimationSamplerNode::update_time(double p_time) {
}
void AnimationSamplerNode::evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &inputs, AnimationData &output) {
GodotProfileZone("AnimationSamplerNode::evaluate");
assert(inputs.size() == 0);
if (node_time_info.is_synced) {
node_time_info.position = node_time_info.sync_track.calc_ratio_from_sync_time(node_time_info.sync_position) * animation->get_length();
}
output.clear();
output.sample_from_animation(animation, context.skeleton_3d, node_time_info.position);
}
@ -276,7 +318,9 @@ void AnimationSamplerNode::_bind_methods() {
}
void AnimationBlend2Node::evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &inputs, AnimationData &output) {
output = *inputs[0];
GodotProfileZone("AnimationBlend2Node::evaluate");
output = std::move(*inputs[0]);
output.blend(*inputs[1], blend_weight);
}

View File

@ -1,9 +1,10 @@
#pragma once
#include "scene/animation/animation_player.h"
#include "core/io/resource.h"
#include "core/profiling/profiling.h"
#include "scene/3d/skeleton_3d.h"
#include "scene/animation/animation_player.h"
#include "sync_track.h"
#include <cassert>
@ -12,7 +13,11 @@
* @class AnimationData
* Represents data that is transported via animation connections in the SyncedAnimationGraph.
*
* Essentially, it is a hash map for all Animation::Track values that can are sampled from an Animation.
* In general AnimationData objects should be obtained using the AnimationDataAllocator.
*
* The class consists of a buffer containing the data and a hashmap that resolves the
* Animation::TypeHash of an Animation::Track to the corresponding AnimationData::TrackValue
* block within the buffer.
*/
struct AnimationData {
enum TrackType : uint8_t {
@ -28,7 +33,6 @@ struct AnimationData {
};
struct TrackValue {
Animation::Track *track = nullptr;
TrackType type = TYPE_ANIMATION;
virtual ~TrackValue() = default;
@ -90,70 +94,51 @@ struct AnimationData {
const TransformTrackValue *other_value_casted = &static_cast<const TransformTrackValue &>(other_value);
return bone_idx == other_value_casted->bone_idx && loc == other_value_casted->loc && rot == other_value_casted->rot && scale == other_value_casted->scale;
}
TrackValue *clone() const override {
TransformTrackValue *result = memnew(TransformTrackValue);
result->track = track;
result->bone_idx = bone_idx;
result->loc_used = loc_used;
result->rot_used = rot_used;
result->scale_used = scale_used;
result->init_loc = init_loc;
result->init_rot = init_rot;
result->init_scale = init_scale;
result->loc = loc;
result->rot = rot;
result->scale = scale;
return result;
}
};
AnimationData() = default;
~AnimationData() {
_clear_values();
}
~AnimationData() = default;
AnimationData(const AnimationData &other) {
for (const KeyValue<Animation::TypeHash, TrackValue *> &K : other.track_values) {
track_values.insert(K.key, K.value->clone());
}
value_buffer_offset = other.value_buffer_offset;
buffer = other.buffer;
}
AnimationData(AnimationData &&other) noexcept :
track_values(std::exchange(other.track_values, AHashMap<Animation::TypeHash, TrackValue *, HashHasher>())) {
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) {
AnimationData temp(other);
std::swap(track_values, temp.track_values);
std::swap(value_buffer_offset, temp.value_buffer_offset);
std::swap(buffer, temp.buffer);
return *this;
}
AnimationData &operator=(AnimationData &&other) noexcept {
std::swap(track_values, other.track_values);
std::swap(value_buffer_offset, other.value_buffer_offset);
std::swap(buffer, other.buffer);
return *this;
}
void
set_value(const Animation::TypeHash &thash, TrackValue *value) {
if (!track_values.has(thash)) {
track_values.insert(thash, value);
} else {
track_values[thash] = value;
}
void allocate_track_value(const Animation::Track *animation_track, const Skeleton3D *skeleton_3d);
void allocate_track_values(const Ref<Animation> &animation, const Skeleton3D *skeleton_3d);
template <typename TrackValueType>
TrackValueType *get_value(const Animation::TypeHash &thash) {
return reinterpret_cast<TrackValueType *>(&buffer[value_buffer_offset[thash]]);
}
void clear() {
_clear_values();
template <typename TrackValueType>
const TrackValueType *get_value(const Animation::TypeHash &thash) const {
return reinterpret_cast<const TrackValueType *>(&buffer[value_buffer_offset[thash]]);
}
bool has_same_tracks(const AnimationData &other) const {
HashSet<Animation::TypeHash> valid_track_hashes;
for (const KeyValue<Animation::TypeHash, TrackValue *> &K : track_values) {
for (const KeyValue<Animation::TypeHash, size_t> &K : value_buffer_offset) {
valid_track_hashes.insert(K.key);
}
for (const KeyValue<Animation::TypeHash, TrackValue *> &K : other.track_values) {
for (const KeyValue<Animation::TypeHash, size_t> &K : other.value_buffer_offset) {
if (HashSet<Animation::TypeHash>::Iterator entry = valid_track_hashes.find(K.key)) {
valid_track_hashes.remove(entry);
} else {
@ -165,14 +150,16 @@ struct AnimationData {
}
void blend(const AnimationData &to_data, const float lambda) {
GodotProfileZone("AnimationData::blend");
if (!has_same_tracks(to_data)) {
print_error("Cannot blend AnimationData: tracks do not match.");
return;
}
for (const KeyValue<Animation::TypeHash, TrackValue *> &K : track_values) {
TrackValue *track_value = K.value;
TrackValue *other_track_value = to_data.track_values[K.key];
for (const KeyValue<Animation::TypeHash, size_t> &K : value_buffer_offset) {
TrackValue *track_value = get_value<TrackValue>(K.key);
const TrackValue *other_track_value = to_data.get_value<TrackValue>(K.key);
track_value->blend(*other_track_value, lambda);
}
@ -180,19 +167,63 @@ struct AnimationData {
void sample_from_animation(const Ref<Animation> &animation, const Skeleton3D *skeleton_3d, double p_time);
AHashMap<Animation::TypeHash, TrackValue *, HashHasher> track_values; // Animation::Track to TrackValue
AHashMap<Animation::TypeHash, size_t, HashHasher> value_buffer_offset;
LocalVector<uint8_t> buffer;
};
protected:
void _clear_values() {
for (KeyValue<Animation::TypeHash, TrackValue *> &K : track_values) {
memdelete(K.value);
/**
* @class AnimationDataAllocator
*
* Allows reusing of already allocated AnimationData objects. Stores the default values for all
* tracks. An allocated AnimationData object always has a resetted state where all TrackValues
* have the default value.
*
* During SyncedAnimationGraph initialization all nodes that generate values for AnimationData
* must register their tracks in the AnimationDataAllocator to ensure all allocated AnimationData
* have corresponding tracks.
*/
class AnimationDataAllocator {
AnimationData default_data;
List<AnimationData *> allocated_data;
public:
~AnimationDataAllocator() {
while (!allocated_data.is_empty()) {
memfree(allocated_data.front()->get());
allocated_data.pop_front();
}
}
/// @brief Registers all animation track values for the default_data value.
void register_track_values(const Ref<Animation> &animation, const Skeleton3D *skeleton_3d);
AnimationData *allocate() {
GodotProfileZone("AnimationDataAllocator::allocate_template");
if (!allocated_data.is_empty()) {
GodotProfileZone("AnimationDataAllocator::allocate_from_list");
AnimationData *result = allocated_data.front()->get();
allocated_data.pop_front();
// We copy the whole block as the assignment operator copies entries element wise.
memcpy(result->buffer.ptr(), default_data.buffer.ptr(), default_data.buffer.size());
return result;
}
AnimationData *result = memnew(AnimationData);
*result = default_data;
return result;
}
void free(AnimationData *data) {
allocated_data.push_front(data);
}
};
struct GraphEvaluationContext {
AnimationPlayer *animation_player = nullptr;
Skeleton3D *skeleton_3d = nullptr;
AnimationDataAllocator animation_data_allocator;
};
/**
@ -240,14 +271,14 @@ public:
return true;
}
virtual void activate_inputs(Vector<Ref<SyncedAnimationNode>> input_nodes) {
virtual void activate_inputs(const Vector<Ref<SyncedAnimationNode>> &input_nodes) {
// By default, all inputs nodes are activated.
for (const Ref<SyncedAnimationNode> &node : input_nodes) {
node->active = true;
node->node_time_info.is_synced = node_time_info.is_synced;
}
}
virtual void calculate_sync_track(Vector<Ref<SyncedAnimationNode>> input_nodes) {
virtual void calculate_sync_track(const Vector<Ref<SyncedAnimationNode>> &input_nodes) {
// By default, use the SyncTrack of the first input.
if (input_nodes.size() > 0) {
node_time_info.sync_track = input_nodes[0]->node_time_info.sync_track;
@ -337,16 +368,16 @@ public:
return result;
}
void activate_inputs(Vector<Ref<SyncedAnimationNode>> input_nodes) override {
for (const Ref<SyncedAnimationNode> &node : input_nodes) {
node->active = true;
void activate_inputs(const Vector<Ref<SyncedAnimationNode>> &input_nodes) override {
input_nodes[0]->active = true;
input_nodes[1]->active = true;
// If this Blend2 node is already synced then inputs are also synced. Otherwise, inputs are only set to synced if synced blending is active in this node.
node->node_time_info.is_synced = node_time_info.is_synced || sync;
}
// If this Blend2 node is already synced then inputs are also synced. Otherwise, inputs are only set to synced if synced blending is active in this node.
input_nodes[0]->node_time_info.is_synced = node_time_info.is_synced || sync;
input_nodes[1]->node_time_info.is_synced = node_time_info.is_synced || sync;
}
void calculate_sync_track(Vector<Ref<SyncedAnimationNode>> input_nodes) override {
void calculate_sync_track(const Vector<Ref<SyncedAnimationNode>> &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);
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);
@ -502,7 +533,7 @@ struct BlendTreeGraph {
LocalVector<int> sorted_node_indices = get_sorted_node_indices();
Vector<Ref<SyncedAnimationNode>> sorted_nodes;
Vector<NodeConnectionInfo> old_node_connection_info = node_connection_info;
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]);
@ -713,7 +744,9 @@ public:
return true;
}
void activate_inputs(Vector<Ref<SyncedAnimationNode>> input_nodes) override {
void activate_inputs(const Vector<Ref<SyncedAnimationNode>> &input_nodes) override {
GodotProfileZone("SyncedBlendTree::activate_inputs");
tree_graph.nodes[0]->active = true;
for (int i = 0; i < tree_graph.nodes.size(); i++) {
const Ref<SyncedAnimationNode> &node = tree_graph.nodes[i];
@ -727,7 +760,8 @@ public:
}
}
void calculate_sync_track(Vector<Ref<SyncedAnimationNode>> input_nodes) override {
void calculate_sync_track(const Vector<Ref<SyncedAnimationNode>> &input_nodes) override {
GodotProfileZone("SyncedBlendTree::calculate_sync_track");
for (int i = tree_graph.nodes.size() - 1; i > 0; i--) {
const Ref<SyncedAnimationNode> &node = tree_graph.nodes[i];
@ -742,6 +776,8 @@ public:
}
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;
@ -763,6 +799,8 @@ public:
}
void evaluate(GraphEvaluationContext &context, const LocalVector<AnimationData *> &input_datas, AnimationData &output_data) override {
ZoneScopedN("SyncedBlendTree::evaluate");
for (int i = tree_graph.nodes.size() - 1; i > 0; i--) {
const Ref<SyncedAnimationNode> &node = tree_graph.nodes[i];
@ -782,14 +820,14 @@ public:
if (i == 1) {
node_runtime_data.output_data = &output_data;
} else {
node_runtime_data.output_data = memnew(AnimationData);
node_runtime_data.output_data = context.animation_data_allocator.allocate();
}
node->evaluate(context, node_runtime_data.input_data, *node_runtime_data.output_data);
// All inputs have been consumed and can now be freed.
for (const int child_index : tree_graph.node_connection_info[i].connected_child_node_index_at_port) {
memfree(_node_runtime_data[child_index].output_data);
context.animation_data_allocator.free(_node_runtime_data[child_index].output_data);
}
}
}

View File

@ -222,20 +222,25 @@ TEST_CASE("[SyncedAnimationGraph] Test BlendTree construction") {
TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph] 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);
AnimationData data_t1;
data_t1.allocate_track_values(test_animation_a, skeleton_node);
data_t1.sample_from_animation(test_animation_a, skeleton_node, 1.0);
AnimationData data_t0_5;
data_t0_5.allocate_track_values(test_animation_a, skeleton_node);
data_t0_5.sample_from_animation(test_animation_a, skeleton_node, 0.5);
AnimationData data_blended = data_t0;
data_blended.blend(data_t1, 0.5);
REQUIRE(data_blended.has_same_tracks(data_t0_5));
for (const KeyValue<Animation::TypeHash, AnimationData::TrackValue *> &K : data_blended.track_values) {
CHECK(K.value->operator==(*data_t0_5.track_values.find(K.key)->value));
for (const KeyValue<Animation::TypeHash, size_t> &K : data_blended.value_buffer_offset) {
AnimationData::TrackValue *blended_value = data_blended.get_value<AnimationData::TrackValue>(K.key);
AnimationData::TrackValue *data_t0_5_value = data_t0_5.get_value<AnimationData::TrackValue>(K.key);
CHECK(*blended_value == *data_t0_5_value);
}
// And also check that values do not match
@ -243,8 +248,10 @@ TEST_CASE_FIXTURE(SyncedAnimationGraphFixture, "[SceneTree][SyncedAnimationGraph
data_blended.blend(data_t1, 0.3);
REQUIRE(data_blended.has_same_tracks(data_t0_5));
for (const KeyValue<Animation::TypeHash, AnimationData::TrackValue *> &K : data_blended.track_values) {
CHECK(K.value->operator!=(*data_t0_5.track_values.find(K.key)->value));
for (const KeyValue<Animation::TypeHash, size_t> &K : data_blended.value_buffer_offset) {
AnimationData::TrackValue *blended_value = data_blended.get_value<AnimationData::TrackValue>(K.key);
AnimationData::TrackValue *data_t0_5_value = data_t0_5.get_value<AnimationData::TrackValue>(K.key);
CHECK(*blended_value != *data_t0_5_value);
}
}