Add **Any State to Anim Graph state machines**

This commit is contained in:
Wojtek Figat
2023-04-24 20:20:27 +02:00
parent 65e477cfd0
commit 7158cfb5bf
7 changed files with 507 additions and 375 deletions

View File

@@ -180,8 +180,6 @@ bool AnimGraphBase::onNodeLoaded(Node* n)
const Float4 range = n->Values[0].AsFloat4();
for (int32 i = 0; i < ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS; i++)
{
auto data0 = n->Values[i * 2 + 4].AsFloat4();
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;
}
@@ -200,9 +198,6 @@ bool AnimGraphBase::onNodeLoaded(Node* n)
const Float4 range = n->Values[0].AsFloat4();
for (int32 i = 0; i < ANIM_GRAPH_MULTI_BLEND_MAX_ANIMS; i++)
{
auto data0 = n->Values[i * 2 + 4].AsFloat4();
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])
{
@@ -283,91 +278,11 @@ bool AnimGraphBase::onNodeLoaded(Node* n)
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.Move(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;
// Initialize transitions
LoadStateTransitions(data, n->Values[2]);
break;
}
@@ -443,6 +358,10 @@ bool AnimGraphBase::onNodeLoaded(Node* n)
case 33:
ADD_BUCKET(InstanceDataBucketInit);
break;
// Any State
case 34:
LoadStateTransitions(n->Data.AnyState, n->Values[0]);
break;
}
break;
// Custom
@@ -465,4 +384,91 @@ bool AnimGraphBase::onNodeLoaded(Node* n)
return VisjectGraph::onNodeLoaded(n);
}
void AnimGraphBase::LoadStateTransitions(AnimGraphNode::StateBaseData& data, Value& transitionsData)
{
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;
}
int32 transitionsCount;
stream.ReadInt32(&transitionsCount);
StateTransitions.EnsureCapacity(StateTransitions.Count() + transitionsCount);
AnimGraphStateTransition transition;
for (int32 i = 0; i < transitionsCount; i++)
{
// Must match StateMachineTransition.Data in C#
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.Move(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
transitionsData = AnimGraphExecutor::Value::Null;
}
#undef ADD_BUCKET

View File

@@ -495,24 +495,31 @@ public:
AnimSubGraph* Graph;
};
struct StateData
struct StateBaseData
{
/// <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 StateData : StateBaseData
{
/// <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;
};
struct AnyStateData : StateBaseData
{
};
struct CustomData
{
/// <summary>
@@ -564,6 +571,7 @@ public:
MultiBlend2DData MultiBlend2D;
StateMachineData StateMachine;
StateData State;
AnyStateData AnyState;
CustomData Custom;
CurveData Curve;
AnimationGraphFunctionData AnimationGraphFunction;
@@ -681,6 +689,9 @@ public:
protected:
// [Graph]
bool onNodeLoaded(Node* n) override;
private:
void LoadStateTransitions(AnimGraphNode::StateBaseData& data, Value& transitionsData);
};
/// <summary>
@@ -895,4 +906,5 @@ private:
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);
void UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData);
};

View File

@@ -11,7 +11,7 @@
namespace
{
void BlendAdditiveWeightedRotation(Quaternion& base, Quaternion& additive, float weight)
FORCE_INLINE void BlendAdditiveWeightedRotation(Quaternion& base, Quaternion& additive, float weight)
{
// Pick a shortest path between rotation to fix blending artifacts
additive *= weight;
@@ -463,6 +463,79 @@ Variant AnimGraphExecutor::SampleState(AnimGraphNode* state)
return result;
}
void AnimGraphExecutor::UpdateStateTransitions(AnimGraphContext& context, const AnimGraphNode::StateMachineData& stateMachineData, AnimGraphInstanceData::StateMachineBucket& stateMachineBucket, const AnimGraphNode::StateBaseData& stateData)
{
int32 transitionIndex = 0;
while (transitionIndex < ANIM_GRAPH_MAX_STATE_TRANSITIONS && stateData.Transitions[transitionIndex] != AnimGraphNode::StateData::InvalidTransitionIndex)
{
const uint16 idx = stateData.Transitions[transitionIndex];
ASSERT(idx < stateMachineData.Graph->StateTransitions.Count());
auto& transition = stateMachineData.Graph->StateTransitions[idx];
if (transition.Destination == stateMachineBucket.CurrentState)
{
// Ignore transition to the current state
transitionIndex++;
continue;
}
const bool useDefaultRule = EnumHasAnyFlags(transition.Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule);
if (transition.RuleGraph && !useDefaultRule)
{
// Execute transition rule
auto rootNode = transition.RuleGraph->GetRootNode();
ASSERT(rootNode);
if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
{
transitionIndex++;
continue;
}
}
// Evaluate source state transition data (position, length, etc.)
const Value sourceStatePtr = SampleState(stateMachineBucket.CurrentState);
auto& transitionData = context.TransitionData; // Note: this could support nested transitions but who uses state machine inside transition rule?
if (ANIM_GRAPH_IS_VALID_PTR(sourceStatePtr))
{
// Use source state as data provider
const auto sourceState = (AnimGraphImpulse*)sourceStatePtr.AsPointer;
auto sourceLength = Math::Max(sourceState->Length, 0.0f);
transitionData.Position = Math::Clamp(sourceState->Position, 0.0f, sourceLength);
transitionData.Length = sourceLength;
}
else
{
// Reset
transitionData.Position = 0;
transitionData.Length = ZeroTolerance;
}
// Check if can trigger the transition
bool canEnter = false;
if (useDefaultRule)
{
// Start transition when the current state animation is about to end (split blend duration evenly into two states)
const auto transitionDurationHalf = transition.BlendDuration * 0.5f + ZeroTolerance;
const auto endPos = transitionData.Length - transitionDurationHalf;
canEnter = transitionData.Position >= endPos;
}
else if (transition.RuleGraph)
canEnter = true;
if (canEnter)
{
// Start transition
stateMachineBucket.ActiveTransition = &transition;
stateMachineBucket.TransitionPosition = 0.0f;
break;
}
// Skip after Solo transition
// TODO: don't load transitions after first enabled Solo transition and remove this check here
if (EnumHasAnyFlags(transition.Flags, AnimGraphStateTransition::FlagTypes::Solo))
break;
transitionIndex++;
}
}
void ComputeMultiBlendLength(float& length, AnimGraphNode* node)
{
ANIM_GRAPH_PROFILE_EVENT("Setup Mutli Blend Length");
@@ -1502,77 +1575,21 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
}
}
ASSERT(bucket.CurrentState && bucket.CurrentState->GroupID == 9 && bucket.CurrentState->TypeID == 20);
ASSERT(bucket.CurrentState && bucket.CurrentState->Type == GRAPH_NODE_MAKE_TYPE(9, 20));
// Update transitions
// Note: this logic assumes that all transitions are sorted by Order property and Enabled
// Note: this logic assumes that all transitions are sorted by Order property and Enabled (by Editor when saving Anim Graph asset)
while (!bucket.ActiveTransition && transitionsLeft-- > 0)
{
// Check if can change the current state
const auto& stateData = bucket.CurrentState->Data.State;
int32 transitionIndex = 0;
while (stateData.Transitions[transitionIndex] != AnimGraphNode::StateData::InvalidTransitionIndex
&& transitionIndex < ANIM_GRAPH_MAX_STATE_TRANSITIONS)
// State transitions
UpdateStateTransitions(context, data, bucket, bucket.CurrentState->Data.State);
// Any state transitions
// TODO: cache Any state nodes inside State Machine to optimize the loop below
for (const AnimGraphNode& anyStateNode : data.Graph->Nodes)
{
const uint16 idx = stateData.Transitions[transitionIndex];
ASSERT(idx < data.Graph->StateTransitions.Count());
auto& transition = data.Graph->StateTransitions[idx];
const bool useDefaultRule = EnumHasAnyFlags(transition.Flags, AnimGraphStateTransition::FlagTypes::UseDefaultRule);
if (transition.RuleGraph && !useDefaultRule)
{
// Execute transition rule
auto rootNode = transition.RuleGraph->GetRootNode();
ASSERT(rootNode);
if (!(bool)eatBox((Node*)rootNode, &rootNode->Boxes[0]))
{
transitionIndex++;
continue;
}
}
// Evaluate source state transition data (position, length, etc.)
const Value sourceStatePtr = SampleState(bucket.CurrentState);
auto& transitionData = context.TransitionData; // Note: this could support nested transitions but who uses state machine inside transition rule?
if (ANIM_GRAPH_IS_VALID_PTR(sourceStatePtr))
{
// Use source state as data provider
const auto sourceState = (AnimGraphImpulse*)sourceStatePtr.AsPointer;
auto sourceLength = Math::Max(sourceState->Length, 0.0f);
transitionData.Position = Math::Clamp(sourceState->Position, 0.0f, sourceLength);
transitionData.Length = sourceLength;
}
else
{
// Reset
transitionData.Position = 0;
transitionData.Length = ZeroTolerance;
}
// Check if can trigger the transition
bool canEnter = false;
if (useDefaultRule)
{
// Start transition when the current state animation is about to end (split blend duration evenly into two states)
const auto transitionDurationHalf = transition.BlendDuration * 0.5f + ZeroTolerance;
const auto endPos = transitionData.Length - transitionDurationHalf;
canEnter = transitionData.Position >= endPos;
}
else if (transition.RuleGraph)
canEnter = true;
if (canEnter)
{
// Start transition
bucket.ActiveTransition = &transition;
bucket.TransitionPosition = 0.0f;
break;
}
// Skip after Solo transition
// TODO: don't load transitions after first enabled Solo transition and remove this check here
if (EnumHasAnyFlags(transition.Flags, AnimGraphStateTransition::FlagTypes::Solo))
break;
transitionIndex++;
if (anyStateNode.Type == GRAPH_NODE_MAKE_TYPE(9, 34))
UpdateStateTransitions(context, data, bucket, anyStateNode.Data.AnyState);
}
// Check for instant transitions
@@ -1608,13 +1625,10 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
}
// Entry
case 19:
{
// Not used
CRASH;
break;
}
// State
case 20:
// Any State
case 34:
{
// Not used
CRASH;
@@ -1622,8 +1636,6 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
}
// State Output
case 21:
value = box->HasConnection() ? eatBox(nodeBase, box->FirstConnection()) : Value::Null;
break;
// Rule Output
case 22:
value = box->HasConnection() ? eatBox(nodeBase, box->FirstConnection()) : Value::Null;