You're breathtaking!

This commit is contained in:
Wojtek Figat
2020-12-07 23:40:54 +01:00
commit 6fb9eee74c
5143 changed files with 1153594 additions and 0 deletions

View File

@@ -0,0 +1,456 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "AnimGraph.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Collections/Sorting.h"
#include "Engine/Content/Assets/SkeletonMask.h"
#include "Engine/Content/Assets/AnimationGraphFunction.h"
#include "Engine/Content/Content.h"
#include "Engine/Utilities/Delaunay2D.h"
#include "Engine/Serialization/MemoryReadStream.h"
void AnimGraphBase::ClearCache()
{
// Clear sub-graphs
for (int32 i = 0; i < SubGraphs.Count(); i++)
{
SubGraphs[i]->ClearCache();
}
// Clear cache
for (int32 i = 0; i < Nodes.Count(); i++)
{
auto& node = Nodes[i];
for (int32 j = 0; j < node.Boxes.Count(); j++)
{
node.Boxes[j].InvalidateCache();
}
}
}
AnimSubGraph* AnimGraphBase::LoadSubGraph(const void* data, int32 dataLength, const Char* name)
{
if (data == nullptr || dataLength == 0)
{
// No graph
return nullptr;
}
name = name ? name : TEXT("?");
// Allocate graph
// TODO: use shared allocations for graphs? eg. block allocator in AnimGraph for better performance
auto subGraph = New<AnimSubGraph>(_graph);
// Load graph
MemoryReadStream stream((byte*)data, dataLength);
if (subGraph->Load(&stream, false))
{
// Load failed
LOG(Warning, "Failed to load sub graph {0}.", name);
return nullptr;
}
// Done
BucketsCountTotal += subGraph->BucketsCountTotal;
SubGraphs.Add(subGraph);
return subGraph;
}
bool AnimGraphBase::Load(ReadStream* stream, bool loadMeta)
{
ASSERT(_graph);
_rootNode = nullptr;
BucketsCountSelf = 0;
BucketsCountTotal = 0;
BucketsStart = _graph->_bucketsCounter;
// Base
if (VisjectGraph::Load(stream, loadMeta))
return true;
BucketsCountTotal += BucketsCountSelf;
return false;
}
void AnimGraphBase::Clear()
{
// Release memory
SubGraphs.ClearDelete();
StateTransitions.Resize(0);
// Base
GraphType::Clear();
}
#if USE_EDITOR
void AnimGraphBase::GetReferences(Array<Guid>& output) const
{
GraphType::GetReferences(output);
// Collect references from nested graph (assets used in state machines)
for (const auto* subGraph : SubGraphs)
{
subGraph->GetReferences(output);
}
}
#endif
void AnimationBucketInit(AnimGraphInstanceData::Bucket& bucket)
{
bucket.Animation.TimePosition = 0.0f;
bucket.Animation.LastUpdateFrame = 0;
}
void MultiBlendBucketInit(AnimGraphInstanceData::Bucket& bucket)
{
bucket.MultiBlend.TimePosition = 0.0f;
bucket.MultiBlend.LastUpdateFrame = 0;
}
void BlendPoseBucketInit(AnimGraphInstanceData::Bucket& bucket)
{
bucket.BlendPose.TransitionPosition = 0.0f;
bucket.BlendPose.PreviousBlendPoseIndex = -1;
}
void StateMachineBucketInit(AnimGraphInstanceData::Bucket& bucket)
{
bucket.StateMachine.LastUpdateFrame = 0;
bucket.StateMachine.CurrentState = nullptr;
bucket.StateMachine.ActiveTransition = nullptr;
bucket.StateMachine.TransitionPosition = 0.0f;
}
bool SortMultiBlend1D(const byte& a, const byte& b, AnimGraphNode* n)
{
// Sort items by X location from the lowest to the highest
const auto aX = a == ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS ? MAX_float : n->Values[4 + a * 2].AsVector4().X;
const auto bX = b == ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS ? MAX_float : n->Values[4 + b * 2].AsVector4().X;
return aX < bX;
}
#define ADD_BUCKET(initializer) \
BucketsCountSelf++; \
n->BucketIndex = _graph->_bucketsCounter++; \
_graph->_bucketInitializerList.Add(initializer)
bool AnimGraphBase::onNodeLoaded(Node* n)
{
((AnimGraphNode*)n)->Graph = _graph;
// Check if this node needs a state container
switch (n->GroupID)
{
// Tools
case 7:
switch (n->TypeID)
{
// Time
case 5:
ADD_BUCKET(AnimationBucketInit);
break;
}
break;
// Animation
case 9:
switch (n->TypeID)
{
// Output
case 1:
_rootNode = n;
if (_rootNode->Values.Count() < 1)
{
_rootNode->Values.Resize(1);
_rootNode->Values[0] = (int32)RootMotionMode::NoExtraction;
}
break;
// Animation
case 2:
ADD_BUCKET(AnimationBucketInit);
n->Assets[0] = (Asset*)Content::LoadAsync<Animation>((Guid)n->Values[0]);
break;
// Blend with Mask
case 11:
n->Assets[0] = (Asset*)Content::LoadAsync<SkeletonMask>((Guid)n->Values[1]);
break;
// Multi Blend 1D
case 12:
{
ADD_BUCKET(MultiBlendBucketInit);
n->Data.MultiBlend1D.Length = -1;
const Vector4 range = n->Values[0].AsVector4();
for (int32 i = 0; i < ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS; i++)
{
auto data0 = n->Values[i * 2 + 4].AsVector4();
data0.X = Math::Clamp(data0.X, range.X, range.Y);
n->Assets[i] = Content::LoadAsync<Animation>((Guid)n->Values[i * 2 + 5]);
n->Data.MultiBlend1D.IndicesSorted[i] = n->Assets[i] ? i : ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS;
}
Sorting::SortArray(n->Data.MultiBlend1D.IndicesSorted, ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS, &SortMultiBlend1D, n);
break;
}
// Multi Blend 2D
case 13:
{
ADD_BUCKET(MultiBlendBucketInit);
n->Data.MultiBlend2D.Length = -1;
// Get blend points locations
Array<Vector2, FixedAllocation<ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS + 3>> vertices;
byte vertexIndexToAnimIndex[ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS];
const Vector4 range = n->Values[0].AsVector4();
for (int32 i = 0; i < ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS; i++)
{
auto data0 = n->Values[i * 2 + 4].AsVector4();
data0.X = Math::Clamp(data0.X, range.X, range.Y);
data0.Y = Math::Clamp(data0.Y, range.Z, range.W);
n->Assets[i] = (Asset*)Content::LoadAsync<Animation>((Guid)n->Values[i * 2 + 5]);
if (n->Assets[i])
{
const int32 vertexIndex = vertices.Count();
vertexIndexToAnimIndex[vertexIndex] = i;
vertices.Add(Vector2(n->Values[i * 2 + 4].AsVector4()));
}
else
{
vertexIndexToAnimIndex[i] = -1;
}
}
// Triangulate
Array<Delaunay2D::Triangle, FixedAllocation<ANIM_GRAPH_MULTI_BLEND_2D_MAX_TRIS>> triangles;
Delaunay2D::Triangulate(vertices, triangles);
// Store triangles vertices indices (map the back to the anim node slots)
for (int32 i = 0; i < triangles.Count(); i++)
{
n->Data.MultiBlend2D.TrianglesP0[i] = vertexIndexToAnimIndex[triangles[i].Indices[0]];
n->Data.MultiBlend2D.TrianglesP1[i] = vertexIndexToAnimIndex[triangles[i].Indices[1]];
n->Data.MultiBlend2D.TrianglesP2[i] = vertexIndexToAnimIndex[triangles[i].Indices[2]];
}
if (triangles.Count() < ANIM_GRAPH_MULTI_BLEND_2D_MAX_TRIS)
n->Data.MultiBlend2D.TrianglesP0[triangles.Count()] = ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS;
break;
}
// Blend Pose
case 14:
{
ADD_BUCKET(BlendPoseBucketInit);
break;
}
// State Machine
case 18:
{
ADD_BUCKET(StateMachineBucketInit);
// Load the graph
auto& data = n->Data.StateMachine;
const Value& name = n->Values[0];
Value& surfaceData = n->Values[1];
data.Graph = LoadSubGraph(surfaceData.AsBlob.Data, surfaceData.AsBlob.Length, (const Char*)name.AsBlob.Data);
// Release data to don't use that memory
surfaceData = Value::Null;
break;
}
// Entry
case 19:
{
const auto entryTargetId = (int32)n->Values[0];
const auto entryTarget = GetNode(entryTargetId);
if (entryTarget == nullptr)
{
LOG(Warning, "Missing Entry node in animation state machine graph.");
}
_rootNode = entryTarget;
break;
}
// State
case 20:
{
// Load the graph
auto& data = n->Data.State;
const Value& name = n->Values[0];
Value& surfaceData = n->Values[1];
data.Graph = LoadSubGraph(surfaceData.AsBlob.Data, surfaceData.AsBlob.Length, (const Char*)name.AsBlob.Data);
// Initialize transitions
Value& transitionsData = n->Values[2];
int32 validTransitions = 0;
if (transitionsData.Type == VariantType::Blob && transitionsData.AsBlob.Length)
{
MemoryReadStream stream((byte*)transitionsData.AsBlob.Data, transitionsData.AsBlob.Length);
int32 version;
stream.ReadInt32(&version);
if (version != 1)
{
LOG(Warning, "Invalid version of the Anim Graph state transitions data.");
return true;
}
int32 transitionsCount;
stream.ReadInt32(&transitionsCount);
StateTransitions.EnsureCapacity(StateTransitions.Count() + transitionsCount);
AnimGraphStateTransition transition;
for (int32 i = 0; i < transitionsCount; i++)
{
struct Data
{
int32 Destination;
int32 Flags;
int32 Order;
float BlendDuration;
int32 BlendMode;
int32 Unused0;
int32 Unused1;
int32 Unused2;
};
Data transitionData;
stream.ReadBytes(&transitionData, sizeof(transitionData));
transition.Flags = (AnimGraphStateTransition::FlagTypes)transitionData.Flags;
transition.BlendDuration = transitionData.BlendDuration;
transition.BlendMode = (AlphaBlendMode)transitionData.BlendMode;
transition.Destination = GetNode(transitionData.Destination);
transition.RuleGraph = nullptr;
int32 ruleSize;
stream.ReadInt32(&ruleSize);
const auto ruleBytes = (byte*)stream.Read(ruleSize);
if (static_cast<int32>(transition.Flags & AnimGraphStateTransition::FlagTypes::Enabled) == 0)
{
// Skip disabled transitions
continue;
}
if (ruleSize != 0)
{
transition.RuleGraph = LoadSubGraph(ruleBytes, ruleSize, TEXT("Rule"));
if (transition.RuleGraph && transition.RuleGraph->GetRootNode() == nullptr)
{
LOG(Warning, "Missing root node for the state machine transition rule graph.");
continue;
}
}
if (transition.Destination == nullptr)
{
LOG(Warning, "Missing target node for the state machine transition.");
continue;
}
if (validTransitions == ANIM_GRAPH_MAX_STATE_TRANSITIONS)
{
LOG(Warning, "State uses too many transitions.");
continue;
}
data.Transitions[validTransitions++] = (uint16)StateTransitions.Count();
StateTransitions.Add(transition);
}
}
if (validTransitions != ANIM_GRAPH_MAX_STATE_TRANSITIONS)
data.Transitions[validTransitions] = AnimGraphNode::StateData::InvalidTransitionIndex;
// Release data to don't use that memory
surfaceData = Value::Null;
transitionsData = Value::Null;
break;
}
// State Output
case 21:
{
_rootNode = n;
break;
}
// Rule Output
case 22:
{
_rootNode = n;
break;
}
// Animation Graph Function
case 24:
{
auto& data = n->Data.AnimationGraphFunction;
// Load function asset
const auto function = Content::LoadAsync<AnimationGraphFunction>((Guid)n->Values[0]);
if (!function || function->WaitForLoaded())
{
data.Graph = nullptr;
break;
}
n->Assets[0] = function;
// Load the graph
auto graphData = function->LoadSurface();
data.Graph = LoadSubGraph(graphData.Get(), graphData.Length(), TEXT("Animation Graph Function"));
break;
}
// Transform Node (local/model space)
// Get Node Transform (local/model space)
// IK Aim, Two Bone IK
case 25:
case 26:
case 28:
case 29:
case 30:
case 31:
{
auto& data = n->Data.TransformNode;
if (_graph->BaseModel && !_graph->BaseModel->WaitForLoaded())
data.NodeIndex = _graph->BaseModel->FindNode((StringView)n->Values[0]);
else
data.NodeIndex = -1;
break;
}
// Copy Node
case 27:
{
auto& data = n->Data.CopyNode;
if (_graph->BaseModel && !_graph->BaseModel->WaitForLoaded())
{
data.SrcNodeIndex = _graph->BaseModel->FindNode((StringView)n->Values[0]);
data.DstNodeIndex = _graph->BaseModel->FindNode((StringView)n->Values[1]);
}
else
{
data.SrcNodeIndex = -1;
data.DstNodeIndex = -1;
}
break;
}
}
break;
// Custom
case 13:
{
// Clear data
auto& data = n->Data.Custom;
data.Evaluate = nullptr;
data.Handle = 0;
// Register node
_graph->_customNodes.Add(n);
// Try init node for the first time
_graph->InitCustomNode(n);
break;
}
}
return VisjectGraph::onNodeLoaded(n);
}
#undef ADD_BUCKET

View File

@@ -0,0 +1,268 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "AnimGraph.h"
#include "Engine/Debug/DebugLog.h"
#include "Engine/Scripting/InternalCalls.h"
#include "Engine/Scripting/BinaryModule.h"
#include "Engine/Scripting/ManagedCLR/MDomain.h"
#include "Engine/Scripting/ManagedCLR/MMethod.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/ManagedCLR/MUtils.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/MException.h"
#include <ThirdParty/mono-2.0/mono/metadata/appdomain.h>
struct InternalInitData
{
MonoArray* Values;
MonoObject* BaseModel;
};
struct InternalContext
{
AnimGraph* Graph;
AnimGraphExecutor* GraphExecutor;
AnimGraph::Node* Node;
uint32 NodeId;
int32 BoxId;
float DeltaTime;
uint64 CurrentFrameIndex;
MonoObject* BaseModel;
MonoObject* Instance;
};
struct InternalImpulse
{
int32 NodesCount;
int32 Unused;
Transform* Nodes;
Vector3 RootMotionTranslation;
Quaternion RootMotionRotation;
float Position;
float Length;
};
static_assert(sizeof(InternalImpulse) == sizeof(AnimGraphImpulse), "Please update managed impulse type for Anim Graph to match the C++ backend data layout.");
namespace AnimGraphInternal
{
bool HasConnection(InternalContext* context, int32 boxId)
{
const auto box = context->Node->TryGetBox(boxId);
if (box == nullptr)
DebugLog::ThrowArgumentOutOfRange("boxId");
return box->HasConnection();
}
MonoObject* GetInputValue(InternalContext* context, int32 boxId)
{
const auto box = context->Node->TryGetBox(boxId);
if (box == nullptr)
DebugLog::ThrowArgumentOutOfRange("boxId");
if (!box->HasConnection())
DebugLog::ThrowArgument("boxId", "This box has no connection. Use HasConnection to check if can get input value.");
Variant value = Variant::Null;
context->GraphExecutor->GetInputValue(box, value);
// Cast value to prevent implicit value conversion issues and handling this on C# side
if (!(box->Type.Type == VariantType::Void && value.Type.Type == VariantType::Pointer))
value = Variant::Cast(value, box->Type);
return MUtils::BoxVariant(value);
}
AnimGraphImpulse* GetOutputImpulseData(InternalContext* context)
{
const auto nodes = context->Node->GetNodes(context->GraphExecutor);
context->GraphExecutor->InitNodes(nodes);
return nodes;
}
}
#if USE_EDITOR
Delegate<Asset*, ScriptingObject*, uint32, uint32> AnimGraphExecutor::DebugFlow;
#endif
void AnimGraphExecutor::initRuntime()
{
ADD_INTERNAL_CALL("FlaxEngine.AnimationGraph::Internal_HasConnection", &AnimGraphInternal::HasConnection);
ADD_INTERNAL_CALL("FlaxEngine.AnimationGraph::Internal_GetInputValue", &AnimGraphInternal::GetInputValue);
ADD_INTERNAL_CALL("FlaxEngine.AnimationGraph::Internal_GetOutputImpulseData", &AnimGraphInternal::GetOutputImpulseData);
}
void AnimGraphExecutor::ProcessGroupCustom(Box* boxBase, Node* nodeBase, Value& value)
{
auto box = (AnimGraphBox*)boxBase;
if (box->IsCacheValid())
{
// Return cache
value = box->Cache;
return;
}
auto node = (AnimGraphNode*)nodeBase;
auto& data = node->Data.Custom;
value = Value::Null;
// Skip invalid nodes
if (data.Evaluate == nullptr)
return;
// Prepare node context
InternalContext context;
context.Graph = &_graph;
context.GraphExecutor = this;
context.Node = node;
context.NodeId = node->ID;
context.BoxId = box->ID;
context.DeltaTime = _deltaTime;
context.CurrentFrameIndex = _currentFrameIndex;;
context.BaseModel = _graph.BaseModel->GetOrCreateManagedInstance();
context.Instance = _data->Object ? _data->Object->GetOrCreateManagedInstance() : nullptr;
// Peek managed object
const auto obj = mono_gchandle_get_target(data.Handle);
if (obj == nullptr)
{
LOG(Warning, "Custom node instance is null.");
return;
}
// Evaluate node
void* params[1];
params[0] = &context;
MonoObject* exception = nullptr;
MonoObject* result = data.Evaluate->Invoke(obj, params, &exception);
if (exception)
{
MException ex(exception);
ex.Log(LogType::Warning, TEXT("AnimGraph"));
return;
}
// Extract result
value = MUtils::UnboxVariant(result);
box->Cache = value;
}
void AnimGraph::ClearCustomNode(Node* node)
{
// Clear data
auto& data = node->Data.Custom;
data.Evaluate = nullptr;
if (data.Handle)
{
mono_gchandle_free(data.Handle);
data.Handle = 0;
}
}
bool AnimGraph::InitCustomNode(Node* node)
{
// Fetch the node logic controller type
if (node->Values.Count() < 2 || node->Values[0].Type.Type != ValueType::String)
{
LOG(Warning, "Invalid custom node data values.");
return false;
}
const StringView typeName(node->Values[0]);
const MString typeNameStd = typeName.ToStringAnsi();
MClass* type = Scripting::FindClass(typeNameStd);
if (type == nullptr)
{
LOG(Warning, "Invalid custom node type {0}.", typeName);
return false;
}
// Get methods
MMethod* load = type->GetMethod("Load", 1);
MMethod* evaluate = type->GetMethod("Evaluate", 1);
if (load == nullptr)
{
LOG(Warning, "Invalid custom node type {0}. Missng Load method.", typeName);
return false;
}
if (evaluate == nullptr)
{
LOG(Warning, "Invalid custom node type {0}. Missng Evaluate method.", typeName);
return false;
}
// Create node values managed array
if (mono_domain_get() == nullptr)
Scripting::GetScriptsDomain()->Dispatch();
const auto values = mono_array_new(mono_domain_get(), mono_get_object_class(), node->Values.Count());
for (int32 i = 0; i < node->Values.Count(); i++)
{
const auto v = MUtils::BoxVariant(node->Values[i]);
mono_array_set(values, MonoObject*, i, v);
}
// Allocate managed node object (create GC handle to prevent destruction)
const auto obj = type->CreateInstance();
const auto handleGC = mono_gchandle_new(obj, false);
// Initialize node
InternalInitData initData;
initData.Values = values;
initData.BaseModel = BaseModel.GetManagedInstance();
void* params[1];
params[0] = &initData;
MonoObject* exception = nullptr;
load->Invoke(obj, params, &exception);
if (exception)
{
mono_gchandle_free(handleGC);
MException ex(exception);
ex.Log(LogType::Warning, TEXT("AnimGraph"));
return false;
}
// Cache node data
auto& data = node->Data.Custom;
data.Evaluate = evaluate;
data.Handle = handleGC;
return false;
}
#if USE_EDITOR
void AnimGraph::OnScriptsReloading()
{
// Clear all cached custom nodes for nodes from game assemblies (plugins may keep data because they are persistent)
for (int32 i = 0; i < _customNodes.Count(); i++)
{
const auto evaluate = _customNodes[i]->Data.Custom.Evaluate;
if (evaluate && Scripting::IsTypeFromGameScripts(evaluate->GetParentClass()))
{
ClearCustomNode(_customNodes[i]);
}
}
}
void AnimGraph::OnScriptsReloaded()
{
// Cache all custom nodes with no type setup
for (int32 i = 0; i < _customNodes.Count(); i++)
{
if (_customNodes[i]->Data.Custom.Evaluate == nullptr)
{
InitCustomNode(_customNodes[i]);
}
}
}
#endif
void AnimGraph::OnScriptsLoaded()
{
// Cache all custom nodes with no type setup
for (int32 i = 0; i < _customNodes.Count(); i++)
{
if (_customNodes[i]->Data.Custom.Evaluate == nullptr)
{
InitCustomNode(_customNodes[i]);
}
}
}

View File

@@ -0,0 +1,322 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "AnimGraph.h"
#include "Engine/Engine/Time.h"
#include "Engine/Scripting/Scripting.h"
RootMotionData RootMotionData::Identity = { Vector3(0.0f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f) };
RootMotionData& RootMotionData::operator+=(const RootMotionData& b)
{
Translation += b.Translation;
Rotation *= b.Rotation;
return *this;
}
RootMotionData& RootMotionData::operator+=(const Transform& b)
{
Translation += b.Translation;
Rotation *= b.Orientation;
return *this;
}
RootMotionData& RootMotionData::operator-=(const Transform& b)
{
Translation -= b.Translation;
Quaternion invRotation = Rotation;
invRotation.Invert();
Quaternion::Multiply(invRotation, b.Orientation, Rotation);
return *this;
}
RootMotionData RootMotionData::operator+(const RootMotionData& b) const
{
RootMotionData result;
result.Translation = Translation + b.Translation;
result.Rotation = Rotation * b.Rotation;
return result;
}
RootMotionData RootMotionData::operator-(const RootMotionData& b) const
{
RootMotionData result;
result.Rotation = Rotation;
result.Rotation.Invert();
Vector3::Transform(b.Translation - Translation, result.Rotation, result.Translation);
Quaternion::Multiply(result.Rotation, b.Rotation, result.Rotation);
return result;
}
Transform AnimGraphImpulse::GetNodeModelTransformation(SkeletonData& skeleton, int32 nodeIndex) const
{
const int32 parentIndex = skeleton.Nodes[nodeIndex].ParentIndex;
if (parentIndex == -1)
{
return Nodes[nodeIndex];
}
const Transform parentTransform = GetNodeModelTransformation(skeleton, parentIndex);
return parentTransform.LocalToWorld(Nodes[nodeIndex]);
}
void AnimGraphImpulse::SetNodeModelTransformation(SkeletonData& skeleton, int32 nodeIndex, const Transform& value)
{
const int32 parentIndex = skeleton.Nodes[nodeIndex].ParentIndex;
if (parentIndex == -1)
{
Nodes[nodeIndex] = value;
return;
}
const Transform parentTransform = GetNodeModelTransformation(skeleton, parentIndex);
parentTransform.WorldToLocal(value, Nodes[nodeIndex]);
}
AnimGraphImpulse* AnimGraphNode::GetNodes(AnimGraphExecutor* executor)
{
// Ensure to have memory
const int32 count = executor->_skeletonNodesCount;
if (Nodes.Nodes.Count() != count)
{
Nodes.Nodes.Resize(count, false);
}
return &Nodes;
}
bool AnimGraph::Load(ReadStream* stream, bool loadMeta)
{
Version++;
_bucketsCounter = 0;
_customNodes.Clear();
// Base
if (AnimGraphBase::Load(stream, loadMeta))
return true;
if (!_isFunction)
{
// Check if has a proper parameters setup
if (Parameters.IsEmpty())
{
LOG(Warning, "Missing Animation Graph parameters.");
return true;
}
if (_rootNode == nullptr)
{
LOG(Warning, "Missing Animation Graph output node.");
return true;
}
if (BaseModel == nullptr)
{
LOG(Warning, "Missing Base Model asset for the Animation Graph. Animation won't be played.");
}
}
// Register for scripts reloading events (only if using any custom nodes)
// Handle load event always because anim graph asset may be loaded before game scripts
if (_customNodes.HasItems())
{
#if USE_EDITOR
Scripting::ScriptsReloading.Bind<AnimGraph, &AnimGraph::OnScriptsReloading>(this);
Scripting::ScriptsReloaded.Bind<AnimGraph, &AnimGraph::OnScriptsReloaded>(this);
#endif
Scripting::ScriptsLoaded.Bind<AnimGraph, &AnimGraph::OnScriptsLoaded>(this);
}
return false;
}
bool AnimGraph::onParamCreated(Parameter* p)
{
if (p->Identifier == ANIM_GRAPH_PARAM_BASE_MODEL_ID)
{
// Perform validation
if ((p->Type.Type != VariantType::Asset && p->Type.Type != VariantType::Null) || p->IsPublic)
{
LOG(Warning, "Invalid Base Model parameter from the Animation Graph.");
return true;
}
// Peek the value
BaseModel = (Guid)p->Value;
}
return Graph::onParamCreated(p);
}
AnimGraphExecutor::AnimGraphExecutor(AnimGraph& graph)
: _graph(graph)
{
_perGroupProcessCall[6] = (ProcessBoxHandler)&AnimGraphExecutor::ProcessGroupParameters;
_perGroupProcessCall[7] = (ProcessBoxHandler)&AnimGraphExecutor::ProcessGroupTools;
_perGroupProcessCall[9] = (ProcessBoxHandler)&AnimGraphExecutor::ProcessGroupAnimation;
_perGroupProcessCall[13] = (ProcessBoxHandler)&AnimGraphExecutor::ProcessGroupCustom;
_perGroupProcessCall[16] = (ProcessBoxHandler)&AnimGraphExecutor::ProcessGroupFunction;
}
const Matrix* AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
{
ASSERT(data.Parameters.Count() == _graph.Parameters.Count());
// Initialize
auto& skeleton = _graph.BaseModel->Skeleton;
{
ANIM_GRAPH_PROFILE_EVENT("Init");
// Prepare graph data for the evaluation
_skeletonBonesCount = skeleton.Bones.Count();
_skeletonNodesCount = skeleton.Nodes.Count();
_graphStack.Clear();
_graphStack.Push((Graph*)&_graph);
_data = &data;
_deltaTime = dt;
_rootMotionMode = (RootMotionMode)(int32)_graph._rootNode->Values[0];
_currentFrameIndex = ++data.CurrentFrame;
_callStack.Clear();
_functions.Clear();
_graph.ClearCache();
// Prepare instance data
if (data.Version != _graph.Version)
{
data.ClearState();
data.Version = _graph.Version;
}
if (data.State.Count() != _graph.BucketsCountTotal)
{
// Prepare memory for buckets state information
data.State.Resize(_graph.BucketsCountTotal, false);
// Initialize buckets
ResetBuckets(&_graph);
}
// Init empty nodes data
_emptyNodes.RootMotion = RootMotionData::Identity;
_emptyNodes.Position = 0.0f;
_emptyNodes.Length = 0.0f;
_emptyNodes.Nodes.Resize(_skeletonNodesCount, false);
for (int32 i = 0; i < _skeletonNodesCount; i++)
{
auto& node = skeleton.Nodes[i];
_emptyNodes.Nodes[i] = node.LocalTransform;
}
}
// Update the animation graph and gather skeleton nodes transformations in nodes local space
AnimGraphImpulse* animResult;
{
ANIM_GRAPH_PROFILE_EVENT("Evaluate");
auto result = eatBox((Node*)_graph._rootNode, &_graph._rootNode->Boxes[0]);
if (result.Type.Type != VariantType::Pointer)
{
result = VariantType::Null;
LOG(Warning, "Invalid animation update result");
}
animResult = (AnimGraphImpulse*)result.AsPointer;
if (animResult == nullptr)
animResult = GetEmptyNodes();
}
Transform* nodesTransformations = animResult->Nodes.Get();
// Calculate the global poses for the skeleton nodes
{
ANIM_GRAPH_PROFILE_EVENT("Global Pose");
_data->NodesPose.Resize(_skeletonNodesCount, false);
// Note: this assumes that nodes are sorted (parents first)
for (int32 nodeIndex = 0; nodeIndex < _skeletonNodesCount; nodeIndex++)
{
const int32 parentIndex = skeleton.Nodes[nodeIndex].ParentIndex;
if (parentIndex != -1)
{
nodesTransformations[nodeIndex] = nodesTransformations[parentIndex].LocalToWorld(nodesTransformations[nodeIndex]);
}
nodesTransformations[nodeIndex].GetWorld(_data->NodesPose[nodeIndex]);
}
}
// Process the root node transformation and the motion
{
_data->RootTransform = nodesTransformations[0];
_data->RootMotion = animResult->RootMotion;
}
// Calculate the final bones transformations
{
ANIM_GRAPH_PROFILE_EVENT("Final Pose");
_bonesTransformations.Resize(_skeletonBonesCount, false);
for (int32 boneIndex = 0; boneIndex < _skeletonBonesCount; boneIndex++)
{
auto& bone = skeleton.Bones[boneIndex];
_bonesTransformations[boneIndex] = bone.OffsetMatrix * _data->NodesPose[bone.NodeIndex];
}
}
// Cleanup
_data = nullptr;
return _bonesTransformations.Get();
}
void AnimGraphExecutor::ResetBuckets(AnimGraphBase* graph)
{
if (graph == nullptr)
return;
ASSERT(_data);
for (int32 i = 0; i < graph->BucketsCountTotal; i++)
{
const int32 bucketIndex = graph->BucketsStart + i;
_graph._bucketInitializerList[bucketIndex](_data->State[bucketIndex]);
}
}
VisjectExecutor::Value AnimGraphExecutor::eatBox(Node* caller, Box* box)
{
// Check if graph is looped or is too deep
if (_callStack.Count() >= ANIM_GRAPH_MAX_CALL_STACK)
{
OnError(caller, box, TEXT("Graph is looped or too deep!"));
return Value::Zero;
}
#if !BUILD_RELEASE
if (box == nullptr)
{
OnError(caller, box, TEXT("Null graph box!"));
return Value::Zero;
}
#endif
// Add to the calling stack
_callStack.Add(caller);
#if USE_EDITOR
DebugFlow(_graph._owner, _data->Object, box->GetParent<Node>()->ID, box->ID);
#endif
// Call per group custom processing event
Value value;
const auto parentNode = box->GetParent<Node>();
const ProcessBoxHandler func = _perGroupProcessCall[parentNode->GroupID];
(this->*func)(box, parentNode, value);
// Remove from the calling stack
_callStack.RemoveLast();
return value;
}
VisjectExecutor::Graph* AnimGraphExecutor::GetCurrentGraph() const
{
return _graphStack.Peek();
}

View File

@@ -0,0 +1,949 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Visject/VisjectGraph.h"
#include "Engine/Content/Assets/SkinnedModel.h"
#include "Engine/Content/Assets/Animation.h"
#include "Engine/Animations/AlphaBlend.h"
#include "../Config.h"
#define ANIM_GRAPH_PARAM_BASE_MODEL_ID Guid(1000, 0, 0, 0)
#define ANIM_GRAPH_IS_VALID_PTR(value) (value.Type.Type == VariantType::Pointer && value.AsPointer != nullptr)
#define ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS 14
#define ANIM_GRAPH_MULTI_BLEND_2D_MAX_TRIS 32
#define ANIM_GRAPH_MAX_STATE_TRANSITIONS 64
#define ANIM_GRAPH_MAX_CALL_STACK 100
class AnimGraph;
class AnimSubGraph;
class AnimGraphBase;
class AnimGraphNode;
class AnimGraphExecutor;
/// <summary>
/// The root motion data container. Supports displacement and rotation (no scale component).
/// </summary>
struct RootMotionData
{
static RootMotionData Identity;
Vector3 Translation;
Quaternion Rotation;
RootMotionData()
{
}
RootMotionData(const Vector3& translation, const Quaternion& rotation)
{
Translation = translation;
Rotation = rotation;
}
RootMotionData(const RootMotionData& other)
{
Translation = other.Translation;
Rotation = other.Rotation;
}
explicit RootMotionData(const Transform& other)
{
Translation = other.Translation;
Rotation = other.Orientation;
}
RootMotionData& operator+=(const RootMotionData& b);
RootMotionData& operator+=(const Transform& b);
RootMotionData& operator-=(const Transform& b);
RootMotionData operator+(const RootMotionData& b) const;
RootMotionData operator-(const RootMotionData& b) const;
static void Lerp(const RootMotionData& t1, const RootMotionData& t2, float amount, RootMotionData& result)
{
Vector3::Lerp(t1.Translation, t2.Translation, amount, result.Translation);
Quaternion::Slerp(t1.Rotation, t2.Rotation, amount, result.Rotation);
}
};
/// <summary>
/// The animation graph 'impulse' connections data container (the actual transfer is done via pointer as it gives better performance).
/// Container for skeleton nodes transformation hierarchy and any other required data.
/// Unified layout for both local and model transformation spaces.
/// </summary>
struct AnimGraphImpulse
{
/// <summary>
/// The skeleton nodes transformation hierarchy nodes. Size always matches the Anim Graph skeleton description.
/// </summary>
Array<Transform> Nodes;
/// <summary>
/// The root motion extracted from the animation to apply on animated object.
/// </summary>
RootMotionData RootMotion = RootMotionData::Identity;
/// <summary>
/// The animation time position (in seconds).
/// </summary>
float Position;
/// <summary>
/// The animation length (in seconds).
/// </summary>
float Length;
FORCE_INLINE Transform GetNodeLocalTransformation(SkeletonData& skeleton, int32 nodeIndex) const
{
return Nodes[nodeIndex];
}
FORCE_INLINE void SetNodeLocalTransformation(SkeletonData& skeleton, int32 nodeIndex, const Transform& value)
{
Nodes[nodeIndex] = value;
}
Transform GetNodeModelTransformation(SkeletonData& skeleton, int32 nodeIndex) const;
void SetNodeModelTransformation(SkeletonData& skeleton, int32 nodeIndex, const Transform& value);
};
/// <summary>
/// The bone transformation modes.
/// </summary>
enum class BoneTransformMode
{
/// <summary>
/// No transformation.
/// </summary>
None = 0,
/// <summary>
/// Applies the transformation.
/// </summary>
Add = 1,
/// <summary>
/// Replaces the transformation.
/// </summary>
Replace = 2,
};
/// <summary>
/// The animated model root motion mode.
/// </summary>
enum class RootMotionMode
{
/// <summary>
/// Don't extract nor apply the root motion.
/// </summary>
NoExtraction = 0,
/// <summary>
/// Ignore root motion (remove from root node transform).
/// </summary>
Ignore = 1,
/// <summary>
/// Enable root motion (remove from root node transform and apply to the target).
/// </summary>
Enable = 2,
};
/// <summary>
/// Data container for the animation graph state machine transition between two states.
/// </summary>
class AnimGraphStateTransition
{
public:
/// <summary>
/// The transition flag types.
/// </summary>
enum class FlagTypes
{
/// <summary>
/// The none.
/// </summary>
None = 0,
/// <summary>
/// The enabled flag.
/// </summary>
Enabled = 1,
/// <summary>
/// The solo flag.
/// </summary>
Solo = 2,
/// <summary>
/// The use default rule flag.
/// </summary>
UseDefaultRule = 4,
};
public:
/// <summary>
/// The destination state node.
/// </summary>
AnimGraphNode* Destination;
/// <summary>
/// The transition rule graph (optional).
/// </summary>
AnimSubGraph* RuleGraph;
/// <summary>
/// The flags.
/// </summary>
FlagTypes Flags;
/// <summary>
/// The blend mode.
/// </summary>
AlphaBlendMode BlendMode;
/// <summary>
/// The blend duration (in seconds).
/// </summary>
float BlendDuration;
};
DECLARE_ENUM_OPERATORS(AnimGraphStateTransition::FlagTypes);
/// <summary>
/// Animation graph parameter.
/// </summary>
/// <seealso cref="GraphParameter" />
API_CLASS() class AnimGraphParameter : public VisjectGraphParameter
{
DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(AnimGraphParameter, VisjectGraphParameter);
public:
AnimGraphParameter(const AnimGraphParameter& other)
: AnimGraphParameter()
{
#if !BUILD_RELEASE
CRASH; // Not used
#endif
}
AnimGraphParameter& operator=(const AnimGraphParameter& other)
{
#if !BUILD_RELEASE
CRASH; // Not used
#endif
return *this;
}
};
/// <summary>
/// The animation graph instance data storage. Required to update the animation graph.
/// </summary>
class AnimGraphInstanceData
{
public:
// ---- Quick documentation ----
// AnimGraphInstanceData holds a single animation graph instance playback data.
// It has parameters (the same layout as graph) that can be modified per instance (eg. by game scripts).
// It has also Buckets!
// Each bucket contains a state for a single graph node that requires state information.
// For example, animation playback node needs to store the current playback position.
// State machine nodes need some state index and transition info and so on.
// So when loading the graph we need to capture the buckets metadata (which nodes use them, how init bucket, etc.).
// Later we just sync buckets with the instance data.
// The key of this solution: reduce allocations and redundancy between graph asset and the users.
struct AnimationBucket
{
float TimePosition;
uint64 LastUpdateFrame;
};
struct MultiBlendBucket
{
float TimePosition;
uint64 LastUpdateFrame;
};
struct BlendPoseBucket
{
float TransitionPosition;
int32 PreviousBlendPoseIndex;
};
struct StateMachineBucket
{
uint64 LastUpdateFrame;
AnimGraphNode* CurrentState;
AnimGraphStateTransition* ActiveTransition;
float TransitionPosition;
};
/// <summary>
/// The single data storage bucket for the instanced animation graph node. Used to store the node state (playback position, state, transition data).
/// </summary>
struct Bucket
{
union
{
AnimationBucket Animation;
MultiBlendBucket MultiBlend;
BlendPoseBucket BlendPose;
StateMachineBucket StateMachine;
};
};
public:
/// <summary>
/// Initializes a new instance of the <see cref="AnimGraphInstanceData"/> class.
/// </summary>
/// <param name="object">The object that represents the instance data source.</param>
AnimGraphInstanceData(ScriptingObject* object)
: Object(object)
{
}
public:
/// <summary>
/// The instance data version number. Used to sync the Anim Graph data with the instance state. Handles Anim Graph reloads to enure data is valid.
/// </summary>
uint32 Version = 0;
/// <summary>
/// The last game time when animation was updated.
/// </summary>
float LastUpdateTime = -1;
/// <summary>
/// The current animation update frame index. Increment on every update.
/// </summary>
int64 CurrentFrame = 0;
/// <summary>
/// The root node transformation. Cached after the animation update.
/// </summary>
Transform RootTransform = Transform::Identity;
/// <summary>
/// The current root motion delta to apply on a target object.
/// </summary>
RootMotionData RootMotion = RootMotionData::Identity;
/// <summary>
/// The animation graph parameters collection (instanced, override the default values).
/// </summary>
Array<AnimGraphParameter> Parameters;
/// <summary>
/// The animation state data.
/// </summary>
Array<Bucket> State;
/// <summary>
/// The per-node final transformations in actor local-space.
/// </summary>
Array<Matrix> NodesPose;
/// <summary>
/// The object that represents the instance data source (used by Custom Nodes and debug flows).
/// </summary>
ScriptingObject* Object;
public:
/// <summary>
/// Clears this container data.
/// </summary>
void Clear()
{
Version = 0;
LastUpdateTime = -1;
CurrentFrame = 0;
RootTransform = Transform::Identity;
RootMotion = RootMotionData::Identity;
Parameters.Resize(0);
State.Resize(0);
NodesPose.Resize(0);
}
/// <summary>
/// Clears this container state data.
/// </summary>
void ClearState()
{
Version = 0;
LastUpdateTime = -1;
CurrentFrame = 0;
RootTransform = Transform::Identity;
RootMotion = RootMotionData::Identity;
State.Resize(0);
NodesPose.Resize(0);
}
/// <summary>
/// Invalidates the update timer.
/// </summary>
void Invalidate()
{
LastUpdateTime = -1;
CurrentFrame = 0;
}
};
/// <summary>
/// The anim graph transition data cached for nodes that read it to calculate if can enter transition.
/// </summary>
struct AnimGraphTransitionData
{
float Position;
float Length;
};
class AnimGraphBox : public VisjectGraphBox
{
public:
AnimGraphBox()
{
}
AnimGraphBox(AnimGraphNode* parent, byte id, const VariantType::Types type)
: VisjectGraphBox(parent, id, type)
{
}
AnimGraphBox(AnimGraphNode* parent, byte id, const VariantType& type)
: VisjectGraphBox(parent, id, type)
{
}
public:
bool IsCacheValid() const
{
return Cache.Type.Type != VariantType::Pointer || Cache.AsPointer != nullptr;
}
void InvalidateCache()
{
Cache = Variant::Null;
}
};
class AnimGraphNode : public VisjectGraphNode<AnimGraphBox>
{
public:
struct MultiBlend1DData
{
/// <summary>
/// The computed length of the mixes animations. Shared for all blend points to provide mor stabilization during looped playback.
/// </summary>
float Length;
/// <summary>
/// The indices of the animations to blend. Sorted from the lowest X to the highest X. Contains only valid used animations. Unused items are using index ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS which is invalid.
/// </summary>
byte IndicesSorted[ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS];
};
struct MultiBlend2DData
{
/// <summary>
/// The computed length of the mixes animations. Shared for all blend points to provide mor stabilization during looped playback.
/// </summary>
float Length;
/// <summary>
/// Cached triangles vertices (vertex 0). Contains list of indices for triangles to use for blending.
/// </summary>
byte TrianglesP0[ANIM_GRAPH_MULTI_BLEND_2D_MAX_TRIS];
/// <summary>
/// Cached triangles vertices (vertex 1). Contains list of indices for triangles to use for blending.
/// </summary>
byte TrianglesP1[ANIM_GRAPH_MULTI_BLEND_2D_MAX_TRIS];
/// <summary>
/// Cached triangles vertices (vertex 2). Contains list of indices for triangles to use for blending.
/// </summary>
byte TrianglesP2[ANIM_GRAPH_MULTI_BLEND_2D_MAX_TRIS];
};
struct StateMachineData
{
/// <summary>
/// The graph of the state machine. Contains all states and transitions between them. Its root node is the first state of the state machine pointed by the Entry node.
/// </summary>
AnimSubGraph* Graph;
};
struct StateData
{
/// <summary>
/// The invalid transition valid used in Transitions to indicate invalid transition linkage.
/// </summary>
const static uint16 InvalidTransitionIndex = MAX_uint16;
/// <summary>
/// The graph of the state. Contains the state animation evaluation graph. Its root node is the state output node with an input box for the state blend pose sampling.
/// </summary>
AnimSubGraph* Graph;
/// <summary>
/// The outgoing transitions from this state to the other states. Each array item contains index of the transition data from the state node graph transitions cache. Value InvalidTransitionIndex is used for last transition to indicate the transitions amount.
/// </summary>
uint16 Transitions[ANIM_GRAPH_MAX_STATE_TRANSITIONS];
};
struct CustomData
{
/// <summary>
/// The cached method to invoke on custom node evaluation.
/// </summary>
class MMethod* Evaluate;
/// <summary>
/// The GC handle to the managed instance of the node object.
/// </summary>
uint32 Handle;
};
struct CurveData
{
/// <summary>
/// The curve index.
/// </summary>
int32 CurveIndex;
};
struct AnimationGraphFunctionData
{
/// <summary>
/// The loaded sub-graph.
/// </summary>
AnimSubGraph* Graph;
};
struct TransformNodeData
{
int32 NodeIndex;
};
struct CopyNodeData
{
int32 SrcNodeIndex;
int32 DstNodeIndex;
};
/// <summary>
/// Custom cached data per node type. Compact to use as small amount of memory as possible.
/// </summary>
struct AdditionalData
{
union
{
MultiBlend1DData MultiBlend1D;
MultiBlend2DData MultiBlend2D;
StateMachineData StateMachine;
StateData State;
CustomData Custom;
CurveData Curve;
AnimationGraphFunctionData AnimationGraphFunction;
TransformNodeData TransformNode;
CopyNodeData CopyNode;
};
};
public:
/// <summary>
/// The animation graph.
/// </summary>
AnimGraph* Graph = nullptr;
/// <summary>
/// The index of the animation state bucket used by this node.
/// </summary>
int32 BucketIndex = -1;
// TODO: use shared allocator per AnimGraph to reduce dynamic memory allocation (also bones data would be closer in memory -> less cache misses)
/// <summary>
/// The node transformations (layout matches the linked to graph skinned model skeleton).
/// </summary>
AnimGraphImpulse Nodes;
/// <summary>
/// The custom data (depends on node type). Used to cache data for faster usage at runtime.
/// </summary>
AdditionalData Data;
public:
AnimGraphNode()
{
}
public:
/// <summary>
/// Gets the per-node node transformations cache (cached).
/// </summary>
/// <param name="executor">The Graph execution context.</param>
/// <returns>The modes data.</returns>
AnimGraphImpulse* GetNodes(AnimGraphExecutor* executor);
};
/// <summary>
/// The base class for Anim Graphs that supports nesting sub graphs.
/// </summary>
/// <seealso cref="VisjectGraph{AnimGraphNode, AnimGraphBox, AnimGraphParameter}" />
class AnimGraphBase : public VisjectGraph<AnimGraphNode, AnimGraphBox, AnimGraphParameter>
{
protected:
AnimGraph* _graph;
Node* _rootNode = nullptr;
AnimGraphBase(AnimGraph* graph)
: _graph(graph)
{
}
public:
/// <summary>
/// The sub graphs nested in this graph.
/// </summary>
Array<AnimSubGraph*, InlinedAllocation<32>> SubGraphs;
/// <summary>
/// The state transitions cached per-graph (that is a state machine).
/// </summary>
Array<AnimGraphStateTransition> StateTransitions;
/// <summary>
/// The zero-based index of the bucket used by this graph. Valid only if BucketsCountSelf is non zero.
/// </summary>
int32 BucketsStart;
/// <summary>
/// The amount of state buckets used by the this graph.
/// </summary>
int32 BucketsCountSelf;
/// <summary>
/// The amount of state buckets used by this graph including all sub-graphs.
/// </summary>
int32 BucketsCountTotal;
public:
/// <summary>
/// Finalizes an instance of the <see cref="AnimGraphBase"/> class.
/// </summary>
~AnimGraphBase()
{
SubGraphs.ClearDelete();
}
public:
/// <summary>
/// Gets the root node of the graph (cache don load).
/// </summary>
/// <returns>The root node.</returns>
FORCE_INLINE Node* GetRootNode() const
{
return _rootNode;
}
/// <summary>
/// Clear all cached values in the graph nodes and the sub-graphs data.
/// </summary>
void ClearCache();
/// <summary>
/// Loads the sub-graph.
/// </summary>
/// <param name="data">The surface data.</param>
/// <param name="dataLength">The length of the data (in bytes).</param>
/// <param name="name">The name of the sub-graph (optional). Used in error message logs.</param>
/// <returns>The loaded sub graph or null if there is no surface to load or if loading failed.</returns>
AnimSubGraph* LoadSubGraph(const void* data, int32 dataLength, const Char* name);
public:
// [Graph]
bool Load(ReadStream* stream, bool loadMeta) override;
void Clear() override;
#if USE_EDITOR
void GetReferences(Array<Guid>& output) const override;
#endif
protected:
// [Graph]
bool onNodeLoaded(Node* n) override;
};
/// <summary>
/// The sub-graph for the main Animation Graph. Used for Anim graphs nesting.
/// </summary>
/// <seealso cref="AnimGraphBase" />
class AnimSubGraph : public AnimGraphBase
{
friend AnimGraph;
friend AnimGraphBase;
friend AnimGraphBox;
friend AnimGraphNode;
friend AnimGraphParameter;
public:
/// <summary>
/// Initializes a new instance of the <see cref="AnimSubGraph" /> class.
/// </summary>
/// <param name="graph">The graph.</param>
AnimSubGraph(AnimGraph* graph)
: AnimGraphBase(graph)
{
}
};
/// <summary>
/// The Animation Graph is used to evaluate a final pose for the animated model for the current frame.
/// </summary>
class AnimGraph : public AnimGraphBase
{
friend AnimGraphBase;
friend AnimGraphBox;
friend AnimGraphNode;
friend AnimGraphParameter;
friend AnimGraphExecutor;
private:
typedef void (*InitBucketHandler)(AnimGraphInstanceData::Bucket&);
bool _isFunction;
int32 _bucketsCounter;
Array<InitBucketHandler> _bucketInitializerList;
Array<Node*> _customNodes;
Asset* _owner;
public:
/// <summary>
/// Initializes a new instance of the <see cref="AnimGraph"/> class.
/// </summary>
/// <param name="owner">The graph owning asset.</param>
/// <param name="isFunction">True if the graph is Animation Graph Function, otherwise false.</param>
AnimGraph(Asset* owner, bool isFunction = false)
: AnimGraphBase(this)
, _isFunction(isFunction)
, _bucketInitializerList(64)
, _owner(owner)
{
}
public:
/// <summary>
/// The Anim Graph data version number. Used to sync the Anim Graph data with the instances state. Handles Anim Graph reloads to enure data is valid.
/// </summary>
uint32 Version = 0;
/// <summary>
/// The base model asset used for the animation preview and the skeleton layout source.
/// </summary>
/// <remarks>
/// Use for read-only as it's serialized from the one of the Graph parameters (see ANIM_GRAPH_PARAM_BASE_MODEL_ID);
/// </remarks>
AssetReference<SkinnedModel> BaseModel;
public:
/// <summary>
/// Determines whether this graph is ready for the animation evaluation.
/// </summary>
/// <returns>True if is ready and can be used for the animation evaluation, otherwise false.</returns>
bool IsReady() const
{
return BaseModel && BaseModel->IsLoaded();
}
/// <summary>
/// Determines whether this graph can be used with the specified skeleton.
/// </summary>
/// <param name="other">The other skinned model to check.</param>
/// <returns>True if can perform the update, otherwise false.</returns>
bool CanUseWithSkeleton(SkinnedModel* other) const
{
// All data loaded and bones count the same
return IsReady() && other && other->IsLoaded() && other->Skeleton.Bones.Count() == BaseModel->Skeleton.Bones.Count();
}
private:
void ClearCustomNode(Node* node);
bool InitCustomNode(Node* node);
#if USE_EDITOR
void OnScriptsReloading();
void OnScriptsReloaded();
#endif
void OnScriptsLoaded();
public:
// [Graph]
bool Load(ReadStream* stream, bool loadMeta) override;
bool onParamCreated(Parameter* p) override;
};
/// <summary>
/// The Animation Graph executor runtime for animation pose evaluation.
/// </summary>
class AnimGraphExecutor : public VisjectExecutor
{
friend AnimGraphNode;
private:
AnimGraph& _graph;
float _deltaTime = 0.0f;
uint64 _currentFrameIndex = 0;
int32 _skeletonBonesCount = 0;
int32 _skeletonNodesCount = 0;
RootMotionMode _rootMotionMode = RootMotionMode::NoExtraction;
AnimGraphInstanceData* _data = nullptr;
AnimGraphImpulse _emptyNodes;
Array<Matrix> _bonesTransformations;
AnimGraphTransitionData _transitionData;
Array<Node*, FixedAllocation<ANIM_GRAPH_MAX_CALL_STACK>> _callStack;
Array<Graph*, FixedAllocation<32>> _graphStack;
Dictionary<Node*, Graph*> _functions;
public:
#if USE_EDITOR
// Custom event that is called every time the Anim Graph signal flows over the graph (including the data connections). Can be used to read nad visualize the animation blending logic.
static Delegate<Asset*, ScriptingObject*, uint32, uint32> DebugFlow;
#endif
/// <summary>
/// Initializes the managed runtime calls.
/// </summary>
static void initRuntime();
/// <summary>
/// Initializes a new instance of the <see cref="AnimGraphExecutor"/> class.
/// </summary>
/// <param name="graph">The graph to execute.</param>
explicit AnimGraphExecutor(AnimGraph& graph);
public:
/// <summary>
/// Updates the graph animation.
/// </summary>
/// <param name="data">The instance data.</param>
/// <param name="dt">The delta time (in seconds).</param>
/// <returns>The pointer to the final bones structure as a result of the animation evaluation.</returns>
const Matrix* Update(AnimGraphInstanceData& data, float dt);
void GetInputValue(Box* box, Value& result)
{
result = eatBox(box->GetParent<Node>(), box->FirstConnection());
}
/// <summary>
/// Gets the skeleton nodes transformations structure containing identity matrices.
/// </summary>
/// <returns>The data.</returns>
FORCE_INLINE const AnimGraphImpulse* GetEmptyNodes() const
{
return &_emptyNodes;
}
/// <summary>
/// Gets the skeleton nodes transformations structure containing identity matrices.
/// </summary>
/// <returns>The data.</returns>
FORCE_INLINE AnimGraphImpulse* GetEmptyNodes()
{
return &_emptyNodes;
}
FORCE_INLINE void InitNodes(AnimGraphImpulse* nodes) const
{
// Initialize with cached node transformations
Platform::MemoryCopy(nodes->Nodes.Get(), _emptyNodes.Nodes.Get(), sizeof(Transform) * _skeletonNodesCount);
nodes->RootMotion = _emptyNodes.RootMotion;
nodes->Position = _emptyNodes.Position;
nodes->Length = _emptyNodes.Length;
}
FORCE_INLINE void InitNode(AnimGraphImpulse* nodes, int32 index) const
{
// Initialize with cached node transformation
nodes->Nodes[index] = GetEmptyNodes()->Nodes[index];
}
FORCE_INLINE void CopyNodes(AnimGraphImpulse* dstNodes, AnimGraphImpulse* srcNodes) const
{
// Copy the node transformations
Platform::MemoryCopy(dstNodes->Nodes.Get(), srcNodes->Nodes.Get(), sizeof(Transform) * _skeletonNodesCount);
// Copy the animation playback state
dstNodes->Position = srcNodes->Position;
dstNodes->Length = srcNodes->Length;
}
FORCE_INLINE void CopyNodes(AnimGraphImpulse* dstNodes, const Value& value) const
{
ASSERT(ANIM_GRAPH_IS_VALID_PTR(value));
CopyNodes(dstNodes, static_cast<AnimGraphImpulse*>(value.AsPointer));
}
/// <summary>
/// Resets the state bucket.
/// </summary>
/// <param name="bucketIndex">The zero-based index of the bucket.</param>
FORCE_INLINE void ResetBucket(int32 bucketIndex)
{
auto& stateBucket = _data->State[bucketIndex];
_graph._bucketInitializerList[bucketIndex](stateBucket);
}
/// <summary>
/// Resets all the state bucket used by the given graph including sub-graphs (total). Can eb used to reset the animation state of the nested graph (including children).
/// </summary>
void ResetBuckets(AnimGraphBase* graph);
private:
Value eatBox(Node* caller, Box* box) override;
Graph* GetCurrentGraph() const override;
void ProcessGroupParameters(Box* box, Node* node, Value& value);
void ProcessGroupTools(Box* box, Node* nodeBase, Value& value);
void ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Value& value);
void ProcessGroupCustom(Box* boxBase, Node* nodeBase, Value& value);
void ProcessGroupFunction(Box* boxBase, Node* node, Value& value);
int32 GetRootNodeIndex(Animation* anim);
void UpdateRootMotion(const Animation::NodeToChannel* mapping, Animation* anim, float pos, float prevPos, Transform& rootNode, RootMotionData& rootMotion);
Variant SampleAnimation(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* anim, float speed);
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, float speedA, float speedB, float alpha);
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, Animation* animC, float speedA, float speedB, float speedC, float alphaA, float alphaB, float alphaC);
Variant Blend(AnimGraphNode* node, const Value& poseA, const Value& poseB, float alpha, AlphaBlendMode alphaMode);
Variant SampleState(AnimGraphNode* state);
};

File diff suppressed because it is too large Load Diff