Add **Spring Bone Physics** node to Anim Graph
This commit is contained in:
@@ -1092,6 +1092,40 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
Utils.GetEmptyArray<byte>(),
|
||||
},
|
||||
},
|
||||
new NodeArchetype
|
||||
{
|
||||
TypeID = 35,
|
||||
Title = "Spring Bone Physics",
|
||||
Description = "Simulates a soft physics-based trailing motion―like antennae, tails, or cloth strips―for a connected chain of skeletal bones.",
|
||||
Flags = NodeFlags.AnimGraph,
|
||||
Size = new Float2(250, 160),
|
||||
DefaultValues = new object[]
|
||||
{
|
||||
string.Empty, // End Node
|
||||
1, // Nodes
|
||||
1.0f, // Weight
|
||||
0.0f, // Stiffness
|
||||
0.1f, // Drag
|
||||
0.0f, // Stretch Limit
|
||||
1.0f, // Gravity Scale
|
||||
Vector3.Zero, // Force
|
||||
},
|
||||
Elements = new[]
|
||||
{
|
||||
NodeElementArchetype.Factory.Output(0, string.Empty, typeof(void), 0),
|
||||
NodeElementArchetype.Factory.Input(0, string.Empty, true, typeof(void), 1),
|
||||
NodeElementArchetype.Factory.Input(1, "Weight", true, typeof(float), 2, 2),
|
||||
NodeElementArchetype.Factory.Input(2, "Stiffness", true, typeof(float), 3, 3),
|
||||
NodeElementArchetype.Factory.Input(3, "Drag", true, typeof(float), 4, 4),
|
||||
NodeElementArchetype.Factory.Input(4, "Stretch Limit", true, typeof(float), 5, 5),
|
||||
NodeElementArchetype.Factory.Input(5, "Gravity Scale", true, typeof(float), 6, 6),
|
||||
NodeElementArchetype.Factory.Input(6, "Force", true, typeof(Vector3), 7, 7),
|
||||
NodeElementArchetype.Factory.SkeletonNodeNameSelect(60, Surface.Constants.LayoutOffsetY * 7, 140, 0),
|
||||
NodeElementArchetype.Factory.Integer(60, Surface.Constants.LayoutOffsetY * 8, 1, -1, 1, 32),
|
||||
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 7, "End Node:", tooltip:"The last (tip) node of the spring bones chain to simulate."),
|
||||
NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 8, "Nodes:", tooltip:"Amount of nodes in a chain to simulate, starting from the End Node going up in the hierarchy. Excluding root node the chain is attached to."),
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,12 @@ void InstanceDataBucketInit(AnimGraphInstanceData::Bucket& bucket)
|
||||
bucket.InstanceData.Init = true;
|
||||
}
|
||||
|
||||
void SpringBonePhysicsBucketInit(AnimGraphInstanceData::Bucket& bucket)
|
||||
{
|
||||
bucket.SpringBonePhysics.LastUpdateFrame = 0;
|
||||
bucket.SpringBonePhysics.StateDataStart = -1;
|
||||
}
|
||||
|
||||
bool SortMultiBlend1D(const ANIM_GRAPH_MULTI_BLEND_INDEX& a, const ANIM_GRAPH_MULTI_BLEND_INDEX& b, AnimGraphNode* n)
|
||||
{
|
||||
// Sort items by X location from the lowest to the highest
|
||||
@@ -313,18 +319,24 @@ bool AnimGraphBase::onNodeLoaded(Node* n)
|
||||
// Transform Node (local/model space)
|
||||
// Get Node Transform (local/model space)
|
||||
// IK Aim, Two Bone IK
|
||||
// Spring Bone Physics
|
||||
case 25:
|
||||
case 26:
|
||||
case 28:
|
||||
case 29:
|
||||
case 30:
|
||||
case 31:
|
||||
case 35:
|
||||
{
|
||||
auto& data = n->Data.TransformNode;
|
||||
if (_graph->BaseModel && !_graph->BaseModel->WaitForLoaded())
|
||||
data.NodeIndex = _graph->BaseModel->FindNode((StringView)n->Values[0]);
|
||||
else
|
||||
data.NodeIndex = -1;
|
||||
if (n->TypeID == 35)
|
||||
{
|
||||
ADD_BUCKET(SpringBonePhysicsBucketInit);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Copy Node
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "Engine/Animations/AnimEvent.h"
|
||||
#include "Engine/Content/Assets/SkinnedModel.h"
|
||||
#include "Engine/Graphics/Models/SkeletonData.h"
|
||||
#include "Engine/Level/Actor.h"
|
||||
#include "Engine/Scripting/Scripting.h"
|
||||
#include "Engine/Threading/Threading.h"
|
||||
|
||||
@@ -24,17 +25,33 @@ Transform AnimGraphImpulse::GetNodeModelTransformation(SkeletonData& skeleton, i
|
||||
return parentTransform.LocalToWorld(Nodes[nodeIndex]);
|
||||
}
|
||||
|
||||
void AnimGraphImpulse::SetNodeModelTransformation(SkeletonData& skeleton, int32 nodeIndex, const Transform& value)
|
||||
void AnimGraphImpulse::SetNodeModelTransformation(SkeletonData& skeleton, int32 nodeIndex, const Transform& value, float weight)
|
||||
{
|
||||
Transform transform = value;
|
||||
const int32 parentIndex = skeleton.Nodes[nodeIndex].ParentIndex;
|
||||
if (parentIndex == -1)
|
||||
if (parentIndex != -1)
|
||||
{
|
||||
Nodes[nodeIndex] = value;
|
||||
return;
|
||||
const Transform parentTransform = GetNodeModelTransformation(skeleton, parentIndex);
|
||||
parentTransform.WorldToLocal(transform, transform);
|
||||
}
|
||||
|
||||
const Transform parentTransform = GetNodeModelTransformation(skeleton, parentIndex);
|
||||
parentTransform.WorldToLocal(value, Nodes[nodeIndex]);
|
||||
if (weight >= 1.0f)
|
||||
Nodes[nodeIndex] = transform;
|
||||
else
|
||||
Transform::Lerp(Nodes[nodeIndex], transform, weight, Nodes[nodeIndex]);
|
||||
}
|
||||
|
||||
Transform AnimGraphImpulse::GetNodeWorldTransformation(const AnimGraphContext& context, SkeletonData& skeleton, int32 nodeIndex) const
|
||||
{
|
||||
Transform transform = GetNodeModelTransformation(skeleton, nodeIndex);
|
||||
return context.Data->GetObjectTransform().LocalToWorld(transform);
|
||||
}
|
||||
|
||||
void AnimGraphImpulse::SetNodeWorldTransformation(const AnimGraphContext& context, SkeletonData& skeleton, int32 nodeIndex, const Transform& value)
|
||||
{
|
||||
Transform transform;
|
||||
context.Data->GetObjectTransform().WorldToLocal(value, transform);
|
||||
SetNodeModelTransformation(skeleton, nodeIndex, transform);
|
||||
}
|
||||
|
||||
void AnimGraphInstanceData::Clear()
|
||||
@@ -56,6 +73,7 @@ void AnimGraphInstanceData::ClearState()
|
||||
RootTransform = Transform::Identity;
|
||||
RootMotion = Transform::Identity;
|
||||
State.Resize(0);
|
||||
DynamicState.Resize(0);
|
||||
NodesPose.Resize(0);
|
||||
TraceEvents.Clear();
|
||||
}
|
||||
@@ -91,6 +109,13 @@ void AnimGraphInstanceData::InvokeAnimEvents()
|
||||
}
|
||||
}
|
||||
|
||||
Transform AnimGraphInstanceData::GetObjectTransform() const
|
||||
{
|
||||
if (auto* actor = ScriptingObject::Cast<Actor>(Object))
|
||||
return actor->GetTransform();
|
||||
return Transform::Identity;
|
||||
}
|
||||
|
||||
AnimGraphInstanceData::OutgoingEvent AnimGraphInstanceData::ActiveEvent::End(AnimatedModel* actor) const
|
||||
{
|
||||
OutgoingEvent out;
|
||||
@@ -285,6 +310,7 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
|
||||
{
|
||||
// Prepare memory for buckets state information
|
||||
data.State.Resize(_graph.BucketsCountTotal, false);
|
||||
data.DynamicState.Clear();
|
||||
|
||||
// Initialize buckets
|
||||
ResetBuckets(context, &_graph);
|
||||
|
||||
@@ -26,6 +26,7 @@ class AnimGraphExecutor;
|
||||
class AnimatedModel;
|
||||
class AnimEvent;
|
||||
class AnimContinuousEvent;
|
||||
struct AnimGraphContext;
|
||||
class SkinnedModel;
|
||||
class SkeletonData;
|
||||
|
||||
@@ -60,14 +61,16 @@ struct FLAXENGINE_API AnimGraphImpulse
|
||||
{
|
||||
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);
|
||||
void SetNodeModelTransformation(SkeletonData& skeleton, int32 nodeIndex, const Transform& value, float weight = 1.0f);
|
||||
|
||||
Transform GetNodeWorldTransformation(const AnimGraphContext& context, SkeletonData& skeleton, int32 nodeIndex) const;
|
||||
void SetNodeWorldTransformation(const AnimGraphContext& context, SkeletonData& skeleton, int32 nodeIndex, const Transform& value);
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -270,6 +273,18 @@ public:
|
||||
float Data[4];
|
||||
};
|
||||
|
||||
struct SpringBonePhysicsBucket
|
||||
{
|
||||
uint64 LastUpdateFrame;
|
||||
int32 StateDataStart;
|
||||
};
|
||||
|
||||
struct SpringBonePhysicsDynamic
|
||||
{
|
||||
Vector3 CurrentPosition;
|
||||
Vector3 PreviousPosition;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The single data storage bucket for the instanced animation graph node. Used to store the node state (playback position, state, transition data).
|
||||
/// </summary>
|
||||
@@ -283,6 +298,7 @@ public:
|
||||
StateMachineBucket StateMachine;
|
||||
SlotBucket Slot;
|
||||
InstanceDataBucket InstanceData;
|
||||
SpringBonePhysicsBucket SpringBonePhysics;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -322,6 +338,11 @@ public:
|
||||
/// </summary>
|
||||
Array<Bucket> State;
|
||||
|
||||
/// <summary>
|
||||
/// The animation state extended data. Managed by the specific node types and shared for a whole instance. Can be resized at runtime.
|
||||
/// </summary>
|
||||
Array<byte> DynamicState;
|
||||
|
||||
/// <summary>
|
||||
/// The per-node final transformations in actor local-space.
|
||||
/// </summary>
|
||||
@@ -368,6 +389,12 @@ public:
|
||||
/// </summary>
|
||||
void InvokeAnimEvents();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the world-space transformation of the animated object.
|
||||
/// </summary>
|
||||
/// <returns>Transformation from model/local space to world space.</returns>
|
||||
Transform GetObjectTransform() const;
|
||||
|
||||
public:
|
||||
// Anim Graph logic tracing feature that allows to collect insights of animations sampling and skeleton poses operations.
|
||||
bool EnableTracing = false;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "Engine/Animations/AnimEvent.h"
|
||||
#include "Engine/Animations/InverseKinematics.h"
|
||||
#include "Engine/Level/Actors/AnimatedModel.h"
|
||||
#include "Engine/Physics/Physics.h"
|
||||
|
||||
struct AnimSampleData
|
||||
{
|
||||
@@ -2513,6 +2514,145 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
|
||||
value = *(Float4*)bucket.Data;
|
||||
break;
|
||||
}
|
||||
// Spring Bone Physics
|
||||
case 35:
|
||||
{
|
||||
// Get bucket
|
||||
auto& bucket = context.Data->State[node->BucketIndex].SpringBonePhysics;
|
||||
Transform objectTransform = context.Data->GetObjectTransform();
|
||||
|
||||
// Get input
|
||||
auto input = tryGetValue(node->GetBox(1), Value::Null);
|
||||
const auto endNodeIndex = node->Data.TransformNode.NodeIndex;
|
||||
float weight = (float)tryGetValue(node->GetBox(2), node->Values[2]);
|
||||
if (endNodeIndex < 0 || endNodeIndex >= _skeletonNodesCount || weight < ANIM_GRAPH_BLEND_THRESHOLD)
|
||||
{
|
||||
value = input;
|
||||
break;
|
||||
}
|
||||
const auto nodes = node->GetNodes(this, input);
|
||||
|
||||
// Get parameters
|
||||
weight = Math::Min(weight, 1.0f);
|
||||
float deltaTime = context.DeltaTime;
|
||||
int32 nodesCount = Math::Clamp((int32)node->Values[1] + 1, 1, _skeletonNodesCount);
|
||||
float stiffness = (float)tryGetValue(node->GetBox(3), node->Values[3]);
|
||||
float drag = (float)tryGetValue(node->GetBox(4), node->Values[4]);
|
||||
float stretchLimit = (float)tryGetValue(node->GetBox(5), node->Values[5]) + 1.0f;
|
||||
float gravityScale = (float)tryGetValue(node->GetBox(6), node->Values[6]);
|
||||
Vector3 force = (Vector3)tryGetValue(node->GetBox(7), node->Values[7]) + Physics::GetGravity() * gravityScale;
|
||||
|
||||
// Get world-space transforms of the nodes (from root to end)
|
||||
Array<int32, InlinedAllocation<8>> nodesIndices;
|
||||
Array<Transform, InlinedAllocation<8>> nodesRef;
|
||||
nodesIndices.Resize(nodesCount);
|
||||
nodesRef.Resize(nodesCount);
|
||||
auto& skeleton = _graph.BaseModel->Skeleton;
|
||||
for (int32 i = nodesCount - 1, nodeIndex = endNodeIndex; i >= 0; i--)
|
||||
{
|
||||
nodesIndices[i] = nodeIndex;
|
||||
nodeIndex = skeleton.Nodes[nodeIndex].ParentIndex;
|
||||
}
|
||||
nodesRef[0] = nodes->GetNodeWorldTransformation(context, skeleton, nodesIndices[0]); // Root comes from animation (incl. animated model movement)
|
||||
for (int32 i = 1; i < nodesCount; i++)
|
||||
nodesRef[i] = nodesRef[i - 1].LocalToWorld(nodes->Nodes[nodesIndices[i]]);
|
||||
|
||||
// Check if we reset the simulation (start from the reference pose)
|
||||
bool reset = bucket.LastUpdateFrame < context.CurrentFrameIndex - 1 || context.CurrentFrameIndex == 1;
|
||||
if (bucket.StateDataStart == -1)
|
||||
{
|
||||
// Allocate a dynamic state
|
||||
bucket.StateDataStart = context.Data->DynamicState.Count();
|
||||
context.Data->DynamicState.AddUninitialized(sizeof(AnimGraphInstanceData::SpringBonePhysicsDynamic) * nodesCount);
|
||||
reset = true;
|
||||
}
|
||||
auto* dynamic = (AnimGraphInstanceData::SpringBonePhysicsDynamic*)&context.Data->DynamicState[bucket.StateDataStart];
|
||||
if (reset)
|
||||
{
|
||||
// Initialize the simulation from the reference pose
|
||||
for (int32 i = 0; i < nodesCount; i++)
|
||||
dynamic[i].CurrentPosition = dynamic[i].PreviousPosition = nodesRef[i].Translation;
|
||||
}
|
||||
bucket.LastUpdateFrame = context.CurrentFrameIndex;
|
||||
|
||||
// Move each node by velocity and forces
|
||||
dynamic[0].PreviousPosition = dynamic[0].CurrentPosition;
|
||||
dynamic[0].CurrentPosition = nodesRef[0].Translation;
|
||||
for (int32 i = 1; i < nodesCount; i++)
|
||||
{
|
||||
Vector3 position = dynamic[i].CurrentPosition;
|
||||
Vector3 prevPosition = dynamic[i].PreviousPosition;
|
||||
Vector3 refPosition = nodesRef[i].Translation;
|
||||
|
||||
// Stiffness force
|
||||
Vector3 delta = (refPosition - position) * stiffness;
|
||||
|
||||
// Gravity and wind forces
|
||||
delta += force * (deltaTime * deltaTime);
|
||||
|
||||
// Verlet integration
|
||||
delta += (position - prevPosition) * (1 - drag);
|
||||
|
||||
dynamic[i].PreviousPosition = position;
|
||||
dynamic[i].CurrentPosition = position + delta;
|
||||
}
|
||||
|
||||
// Length constraints
|
||||
for (int32 i = 1; i < nodesCount; i++)
|
||||
{
|
||||
Vector3 offset = dynamic[i].CurrentPosition - dynamic[i - 1].CurrentPosition;
|
||||
Real dist = offset.Length();
|
||||
Real boneLength = Vector3::Distance(nodesRef[i].Translation, nodesRef[i - 1].Translation);
|
||||
Real maxBoneLength = boneLength * stretchLimit;
|
||||
if (dist > maxBoneLength && dist > ANIM_GRAPH_BLEND_THRESHOLD)
|
||||
{
|
||||
Real scale = ((dist - maxBoneLength) / dist);
|
||||
dynamic[i].CurrentPosition -= offset * scale;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the nodes with the new transforms (move back from world-space to node-space)
|
||||
Quaternion prevRotation = Quaternion::Identity;
|
||||
for (int32 i = 0; i < nodesCount; i++)
|
||||
{
|
||||
Transform nodeTransform = nodesRef[i];
|
||||
nodeTransform.Translation = dynamic[i].CurrentPosition;
|
||||
|
||||
// Move back from world-space to model-space
|
||||
nodeTransform = objectTransform.WorldToLocal(nodeTransform);
|
||||
|
||||
if (i + 1 < nodesCount)
|
||||
{
|
||||
// Orient node to point to the next node in the chain
|
||||
Vector3 childNewPos = objectTransform.WorldToLocal(dynamic[i + 1].CurrentPosition);
|
||||
int32 childIndex = nodesIndices[i + 1];
|
||||
Vector3 childRefPos = nodes->Nodes[childIndex].Translation;
|
||||
Vector3 childOldPos = nodeTransform.LocalToWorld(childRefPos);
|
||||
Vector3 oldDir = (childOldPos - nodeTransform.Translation).GetNormalized();
|
||||
Vector3 newDir = (childNewPos - nodeTransform.Translation).GetNormalized();
|
||||
nodeTransform.Orientation = Quaternion::FindBetween(oldDir, newDir) * nodeTransform.Orientation;
|
||||
prevRotation = nodeTransform.Orientation;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Tip (last) node copies the orientation of the parent
|
||||
nodeTransform.Orientation = prevRotation;
|
||||
}
|
||||
|
||||
nodes->SetNodeModelTransformation(skeleton, nodesIndices[i], nodeTransform, weight);
|
||||
}
|
||||
|
||||
// When we blend between animated and simulated poses then fetch the final pose from the nodes instead of using the simulated pose directly
|
||||
if (weight < 1.0f)
|
||||
{
|
||||
// TODO: optimize it by getting root node, then using LocalToWorld for following children
|
||||
for (int32 i = 1; i < nodesCount; i++)
|
||||
dynamic[i].CurrentPosition = nodes->GetNodeWorldTransformation(context, skeleton, nodesIndices[i]).Translation;
|
||||
}
|
||||
|
||||
value = nodes;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user