diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs index 410f6d33f..b29d027f8 100644 --- a/Source/Editor/Surface/Archetypes/Animation.cs +++ b/Source/Editor/Surface/Archetypes/Animation.cs @@ -1092,6 +1092,40 @@ namespace FlaxEditor.Surface.Archetypes Utils.GetEmptyArray(), }, }, + 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."), + } + }, }; } } diff --git a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp index e4d7eda70..1cc9c66a4 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp @@ -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 diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp index 38f802ec8..58b4a92c2 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.cpp @@ -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(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); diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h index ac429bd36..e2fe1ab83 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.h +++ b/Source/Engine/Animations/Graph/AnimGraph.h @@ -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); }; /// @@ -270,6 +273,18 @@ public: float Data[4]; }; + struct SpringBonePhysicsBucket + { + uint64 LastUpdateFrame; + int32 StateDataStart; + }; + + struct SpringBonePhysicsDynamic + { + Vector3 CurrentPosition; + Vector3 PreviousPosition; + }; + /// /// The single data storage bucket for the instanced animation graph node. Used to store the node state (playback position, state, transition data). /// @@ -283,6 +298,7 @@ public: StateMachineBucket StateMachine; SlotBucket Slot; InstanceDataBucket InstanceData; + SpringBonePhysicsBucket SpringBonePhysics; }; }; @@ -322,6 +338,11 @@ public: /// Array State; + /// + /// The animation state extended data. Managed by the specific node types and shared for a whole instance. Can be resized at runtime. + /// + Array DynamicState; + /// /// The per-node final transformations in actor local-space. /// @@ -368,6 +389,12 @@ public: /// void InvokeAnimEvents(); + /// + /// Gets the world-space transformation of the animated object. + /// + /// Transformation from model/local space to world space. + Transform GetObjectTransform() const; + public: // Anim Graph logic tracing feature that allows to collect insights of animations sampling and skeleton poses operations. bool EnableTracing = false; diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index eb72d9788..71f922792 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -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> nodesIndices; + Array> 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; }