#pragma once #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 /** * @class AnimationData * Represents data that is transported via animation connections in the SyncedAnimationGraph. * * 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 { TYPE_VALUE, // Set a value in a property, can be interpolated. TYPE_POSITION_3D, // Position 3D track, can be compressed. TYPE_ROTATION_3D, // Rotation 3D track, can be compressed. TYPE_SCALE_3D, // Scale 3D track, can be compressed. TYPE_BLEND_SHAPE, // Blend Shape track, can be compressed. TYPE_METHOD, // Call any method on a specific node. TYPE_BEZIER, // Bezier curve. TYPE_AUDIO, TYPE_ANIMATION, }; struct TrackValue { TrackType type = TYPE_ANIMATION; virtual ~TrackValue() = default; virtual void blend(const TrackValue &to_value, const float lambda) { print_error(vformat("Blending of TrackValue of type %d with TrackValue of type %d not yet implemented.", type, to_value.type)); } virtual bool operator==(const TrackValue &other_value) const { print_error(vformat("Comparing TrackValue of type %d with TrackValue of type %d not yet implemented.", type, other_value.type)); return false; } bool operator!=(const TrackValue &other_value) const { return !(*this == other_value); } virtual TrackValue *clone() const { print_error(vformat("Cannot clone TrackValue of type %d: not yet implemented.", type)); return nullptr; } }; struct TransformTrackValue : public TrackValue { int bone_idx = -1; bool loc_used = false; bool rot_used = false; bool scale_used = false; Vector3 init_loc = Vector3(0, 0, 0); Quaternion init_rot = Quaternion(0, 0, 0, 1); Vector3 init_scale = Vector3(1, 1, 1); Vector3 loc; Quaternion rot; Vector3 scale; TransformTrackValue() { type = TYPE_POSITION_3D; } void blend(const TrackValue &to_value, const float lambda) override { const TransformTrackValue *to_value_casted = &static_cast(to_value); assert(bone_idx == to_value_casted->bone_idx); if (loc_used) { loc = (1. - lambda) * loc + lambda * to_value_casted->loc; } if (rot_used) { rot = rot.slerp(to_value_casted->rot, lambda); } if (scale_used) { scale = (1. - lambda) * scale + lambda * to_value_casted->scale; } } bool operator==(const TrackValue &other_value) const override { if (type != other_value.type) { return false; } const TransformTrackValue *other_value_casted = &static_cast(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; } }; AnimationData() = default; ~AnimationData() = default; AnimationData(const AnimationData &other) { value_buffer_offset = other.value_buffer_offset; buffer = other.buffer; } AnimationData(AnimationData &&other) noexcept : value_buffer_offset(std::exchange(other.value_buffer_offset, AHashMap())), buffer(std::exchange(other.buffer, LocalVector())) { } AnimationData &operator=(const AnimationData &other) { AnimationData temp(other); std::swap(value_buffer_offset, temp.value_buffer_offset); std::swap(buffer, temp.buffer); return *this; } AnimationData &operator=(AnimationData &&other) noexcept { std::swap(value_buffer_offset, other.value_buffer_offset); std::swap(buffer, other.buffer); return *this; } void allocate_track_value(const Animation::Track *animation_track, const Skeleton3D *skeleton_3d); void allocate_track_values(const Ref &animation, const Skeleton3D *skeleton_3d); template TrackValueType *get_value(const Animation::TypeHash &thash) { return reinterpret_cast(&buffer[value_buffer_offset[thash]]); } template const TrackValueType *get_value(const Animation::TypeHash &thash) const { return reinterpret_cast(&buffer[value_buffer_offset[thash]]); } bool has_same_tracks(const AnimationData &other) const { HashSet valid_track_hashes; for (const KeyValue &K : value_buffer_offset) { valid_track_hashes.insert(K.key); } for (const KeyValue &K : other.value_buffer_offset) { if (HashSet::Iterator entry = valid_track_hashes.find(K.key)) { valid_track_hashes.remove(entry); } else { return false; } } return valid_track_hashes.size() == 0; } 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 &K : value_buffer_offset) { TrackValue *track_value = get_value(K.key); const TrackValue *other_track_value = to_data.get_value(K.key); track_value->blend(*other_track_value, lambda); } } void sample_from_animation(const Ref &animation, const Skeleton3D *skeleton_3d, double p_time); AHashMap value_buffer_offset; LocalVector buffer; }; /** * @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 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, 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; }; /** * @class BLTAnimationNode * Base class for all nodes in an SyncedAnimationGraph including BlendTree nodes and StateMachine states. */ class BLTAnimationNode : public Resource { GDCLASS(BLTAnimationNode, Resource); friend class BLTAnimationGraph; protected: static void _bind_methods(); virtual void get_parameter_list(List *r_list) const; virtual Variant get_parameter_default_value(const StringName &p_parameter) const; virtual bool is_parameter_read_only(const StringName &p_parameter) const; virtual void set_parameter(const StringName &p_name, const Variant &p_value); virtual Variant get_parameter(const StringName &p_name) const; virtual void _tree_changed(); virtual void _animation_node_renamed(const ObjectID &p_oid, const String &p_old_name, const String &p_new_name); virtual void _animation_node_removed(const ObjectID &p_oid, const StringName &p_node); public: struct NodeTimeInfo { double delta = 0.0; double position = 0.0; double sync_position = 0.0; bool is_synced = false; Animation::LoopMode loop_mode = Animation::LOOP_NONE; SyncTrack sync_track; }; NodeTimeInfo node_time_info; bool active = false; StringName name; Vector2 position; virtual ~BLTAnimationNode() override = default; virtual bool initialize(GraphEvaluationContext &context) { node_time_info = {}; return true; } virtual void activate_inputs(const Vector> &input_nodes) { // By default, all inputs nodes are activated. for (const Ref &node : input_nodes) { node->active = true; node->node_time_info.is_synced = node_time_info.is_synced; } } virtual void calculate_sync_track(const Vector> &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; node_time_info.loop_mode = input_nodes[0]->node_time_info.loop_mode; } } virtual void update_time(double p_time) { if (node_time_info.is_synced) { node_time_info.sync_position = p_time; } else { node_time_info.delta = p_time; node_time_info.position += p_time; } } virtual void evaluate(GraphEvaluationContext &context, const LocalVector &input_datas, AnimationData &output_data) { // By default, use the AnimationData of the first input. if (input_datas.size() > 0) { output_data = *input_datas[0]; } } virtual void get_input_names(Vector &inputs) const {} int get_input_index(const StringName &port_name) const { Vector inputs; get_input_names(inputs); return inputs.find(port_name); } int get_input_count() const { Vector inputs; get_input_names(inputs); return inputs.size(); } // Creates a list of nodes nested within the current node. E.g. all nodes within a BlendTree node. virtual void get_child_nodes(List> *r_child_nodes) const {} }; class BLTAnimationNodeSampler : public BLTAnimationNode { GDCLASS(BLTAnimationNodeSampler, BLTAnimationNode); public: StringName animation_name; void set_animation(const StringName &p_name); StringName get_animation() const; private: Ref animation; bool initialize(GraphEvaluationContext &context) override; void update_time(double p_time) override; void evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) override; protected: static void _bind_methods(); }; class BLTAnimationNodeOutput : public BLTAnimationNode { GDCLASS(BLTAnimationNodeOutput, BLTAnimationNode); public: void get_input_names(Vector &inputs) const override { inputs.push_back("Input"); } }; class BLTAnimationNodeBlend2 : public BLTAnimationNode { GDCLASS(BLTAnimationNodeBlend2, BLTAnimationNode); public: float blend_weight = 0.0f; bool sync = true; void get_input_names(Vector &inputs) const override { inputs.push_back("Input0"); inputs.push_back("Input1"); } bool initialize(GraphEvaluationContext &context) override { bool result = BLTAnimationNode::initialize(context); if (sync) { // TODO: do we always want looping in this case or do we traverse the graph to check what's reasonable? node_time_info.loop_mode = Animation::LOOP_LINEAR; } return result; } void activate_inputs(const Vector> &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. 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(const Vector> &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); } } void update_time(double p_delta) override { BLTAnimationNode::update_time(p_delta); if (sync && !node_time_info.is_synced) { if (node_time_info.loop_mode != Animation::LOOP_NONE) { if (node_time_info.loop_mode == Animation::LOOP_LINEAR) { if (!Math::is_zero_approx(node_time_info.sync_track.duration)) { node_time_info.position = Math::fposmod(static_cast(node_time_info.position), node_time_info.sync_track.duration); node_time_info.sync_position = node_time_info.sync_track.calc_sync_from_abs_time(node_time_info.position); } else { assert(false && !"Loop mode ping-pong not yet supported"); } } } } } void evaluate(GraphEvaluationContext &context, const LocalVector &inputs, AnimationData &output) override; void set_use_sync(bool p_sync); bool is_using_sync() const; protected: static void _bind_methods(); void get_parameter_list(List *p_list) const override; Variant get_parameter_default_value(const StringName &p_parameter) const override; void set_parameter(const StringName &p_name, const Variant &p_value) override; Variant get_parameter(const StringName &p_name) const override; void _get_property_list(List *p_list) const; bool _get(const StringName &p_name, Variant &r_value) const; bool _set(const StringName &p_name, const Variant &p_value); private: StringName blend_weight_pname = PNAME("blend_amount"); StringName sync_pname = PNAME("sync"); }; struct BLTBlendTreeConnection { const Ref source_node = nullptr; const Ref target_node = nullptr; const StringName target_port_name = ""; }; class BLTAnimationNodeBlendTree : public BLTAnimationNode { GDCLASS(BLTAnimationNodeBlendTree, BLTAnimationNode); public: enum ConnectionError { CONNECTION_OK, CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED, CONNECTION_ERROR_NO_SOURCE_NODE, CONNECTION_ERROR_NO_TARGET_NODE, CONNECTION_ERROR_PARENT_EXISTS, CONNECTION_ERROR_TARGET_PORT_NOT_FOUND, CONNECTION_ERROR_TARGET_PORT_ALREADY_CONNECTED, CONNECTION_ERROR_CONNECTION_CREATES_LOOP, }; /** * @class BLTBlendTreeGraph * Helper class that is used to build runtime blend trees and also to validate connections. */ struct BLTBlendTreeGraph { struct NodeConnectionInfo { int parent_node_index = -1; HashSet input_subtree_node_indices; // Contains all nodes down to the tree leaves that influence this node. LocalVector connected_child_node_index_at_port; // Contains for each input port the index of the node that is connected to it. NodeConnectionInfo() = default; explicit NodeConnectionInfo(const BLTAnimationNode *node) { parent_node_index = -1; for (int i = 0; i < node->get_input_count(); i++) { connected_child_node_index_at_port.push_back(-1); } } void apply_node_mapping(const LocalVector &node_index_mapping) { // Map connected node indices for (unsigned int j = 0; j < connected_child_node_index_at_port.size(); j++) { int connected_node_index = connected_child_node_index_at_port[j]; connected_child_node_index_at_port[j] = node_index_mapping.find(connected_node_index); } // Map connected subtrees HashSet old_indices = input_subtree_node_indices; input_subtree_node_indices.clear(); for (int old_index : old_indices) { input_subtree_node_indices.insert(node_index_mapping.find(old_index)); } } void _print_subtree() const; }; Vector> nodes; // All added nodes LocalVector node_connection_info; LocalVector connections; BLTBlendTreeGraph(); Ref get_output_node(); int find_node_index(const Ref &node) const; int find_node_index_by_name(const StringName &name) const; void sort_nodes_and_references(); LocalVector get_sorted_node_indices(); void sort_nodes_recursive(int node_index, LocalVector &result); void add_index_and_update_subtrees_recursive(int node, int node_parent); ConnectionError is_connection_valid(const Ref &source_node, const Ref &target_node, StringName target_port_name) const; void add_node(const Ref &node); ConnectionError add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name); }; private: BLTBlendTreeGraph tree_graph; bool tree_initialized = false; void sort_nodes() { _node_runtime_data.clear(); tree_graph.sort_nodes_and_references(); } void setup_runtime_data() { // Add nodes and allocate runtime data for (int i = 0; i < tree_graph.nodes.size(); i++) { const Ref node = tree_graph.nodes[i]; NodeRuntimeData node_runtime_data; for (int ni = 0; ni < node->get_input_count(); ni++) { node_runtime_data.input_data.push_back(nullptr); } node_runtime_data.output_data = nullptr; _node_runtime_data.push_back(node_runtime_data); } // Populate runtime data (only now is this.nodes populated to retrieve the nodes) for (int i = 0; i < tree_graph.nodes.size(); i++) { Ref node = tree_graph.nodes[i]; NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; for (int port_index = 0; port_index < node->get_input_count(); port_index++) { const int connected_node_index = tree_graph.node_connection_info[i].connected_child_node_index_at_port[port_index]; node_runtime_data.input_nodes.push_back(tree_graph.nodes[connected_node_index]); } } } protected: static void _bind_methods(); void _get_property_list(List *p_list) const; bool _get(const StringName &p_name, Variant &r_value) const; bool _set(const StringName &p_name, const Variant &p_value); public: struct NodeRuntimeData { Vector> input_nodes; LocalVector input_data; AnimationData *output_data = nullptr; }; LocalVector _node_runtime_data; Ref get_output_node() const { return tree_graph.nodes[0]; } int find_node_index(const Ref &node) const { return tree_graph.find_node_index(node); } int find_node_index_by_name(const StringName &name) const { return tree_graph.find_node_index_by_name(name); } Ref get_node(int node_index) { if (node_index < 0 || node_index > tree_graph.nodes.size()) { return nullptr; } return tree_graph.nodes[node_index]; } void add_node(const Ref &node) { if (tree_initialized) { print_error("Cannot add node to BlendTree: BlendTree already initialized."); return; } tree_graph.add_node(node); } ConnectionError add_connection(const Ref &source_node, const Ref &target_node, const StringName &target_port_name) { if (tree_initialized) { print_error("Cannot add connection to BlendTree: BlendTree already initialized."); return CONNECTION_ERROR_GRAPH_ALREADY_INITIALIZED; } return tree_graph.add_connection(source_node, target_node, target_port_name); } // overrides from SyncedAnimationNode bool initialize(GraphEvaluationContext &context) override { sort_nodes(); setup_runtime_data(); for (const Ref &node : tree_graph.nodes) { if (!node->initialize(context)) { return false; } } tree_initialized = true; return true; } void activate_inputs(const Vector> &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 &node = tree_graph.nodes[i]; if (!node->active) { continue; } const NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; node->activate_inputs(node_runtime_data.input_nodes); } } void calculate_sync_track(const Vector> &input_nodes) override { GodotProfileZone("SyncedBlendTree::calculate_sync_track"); for (int i = tree_graph.nodes.size() - 1; i > 0; i--) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { continue; } const NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; node->calculate_sync_track(node_runtime_data.input_nodes); } } 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; for (int i = 1; i < tree_graph.nodes.size(); i++) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { continue; } const Ref &node_parent = tree_graph.nodes[tree_graph.node_connection_info[i].parent_node_index]; if (node->node_time_info.is_synced) { node->update_time(node_parent->node_time_info.sync_position); } else { node->update_time(node_parent->node_time_info.delta); } } } void evaluate(GraphEvaluationContext &context, const LocalVector &input_datas, AnimationData &output_data) override { ZoneScopedN("SyncedBlendTree::evaluate"); for (int i = tree_graph.nodes.size() - 1; i > 0; i--) { const Ref &node = tree_graph.nodes[i]; if (!node->active) { continue; } NodeRuntimeData &node_runtime_data = _node_runtime_data[i]; // Populate the inputs for (unsigned int j = 0; j < node_runtime_data.input_data.size(); j++) { int child_index = tree_graph.node_connection_info[i].connected_child_node_index_at_port[j]; node_runtime_data.input_data[j] = _node_runtime_data[child_index].output_data; } // Set output pointer if (i == 1) { node_runtime_data.output_data = &output_data; } else { 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) { context.animation_data_allocator.free(_node_runtime_data[child_index].output_data); } } } void get_child_nodes(List> *r_child_nodes) const override { for (const Ref &node : tree_graph.nodes) { r_child_nodes->push_back(node.ptr()); } } }; VARIANT_ENUM_CAST(BLTAnimationNodeBlendTree::ConnectionError)