From d08843900e11e41e89707fb46c6bf0d806903a8d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 9 Feb 2024 15:37:29 +0100 Subject: [PATCH] Add `AnimationRootMotionFlags` to configure root motion component extraction Add `RootMotionMode` to support extracting root motion from animated skeleton pose center of mass #1429 #2152 --- .../Editor/Content/Import/ModelImportEntry.cs | 1 + Source/Engine/Animations/AnimationData.cpp | 84 ++++++++++ Source/Engine/Animations/AnimationData.h | 96 ++++------- Source/Engine/Animations/Curve.h | 2 +- .../Animations/Graph/AnimGraph.Base.cpp | 2 +- Source/Engine/Animations/Graph/AnimGraph.cpp | 2 +- Source/Engine/Animations/Graph/AnimGraph.h | 6 +- .../Animations/Graph/AnimGroup.Animation.cpp | 55 +++++-- Source/Engine/Content/Assets/Animation.cpp | 21 ++- .../ContentImporters/CreateAnimationGraph.cpp | 2 +- Source/Engine/Graphics/Models/ModelData.cpp | 10 +- Source/Engine/Graphics/Models/SkeletonData.h | 46 ++---- Source/Engine/Graphics/Models/SkinnedMesh.cpp | 63 ++++++++ Source/Engine/Tools/ModelTool/ModelTool.cpp | 150 +++++++++++++++++- Source/Engine/Tools/ModelTool/ModelTool.h | 20 ++- 15 files changed, 416 insertions(+), 144 deletions(-) create mode 100644 Source/Engine/Animations/AnimationData.cpp diff --git a/Source/Editor/Content/Import/ModelImportEntry.cs b/Source/Editor/Content/Import/ModelImportEntry.cs index 484efb1d9..fac782cbd 100644 --- a/Source/Editor/Content/Import/ModelImportEntry.cs +++ b/Source/Editor/Content/Import/ModelImportEntry.cs @@ -16,6 +16,7 @@ namespace FlaxEngine.Tools private bool ShowModel => Type == ModelType.Model || Type == ModelType.Prefab; private bool ShowSkinnedModel => Type == ModelType.SkinnedModel || Type == ModelType.Prefab; private bool ShowAnimation => Type == ModelType.Animation || Type == ModelType.Prefab; + private bool ShowRootMotion => ShowAnimation && RootMotion != RootMotionMode.None; private bool ShowSmoothingNormalsAngle => ShowGeometry && CalculateNormals; private bool ShowSmoothingTangentsAngle => ShowGeometry && CalculateTangents; private bool ShowFramesRange => ShowAnimation && Duration == AnimationDuration.Custom; diff --git a/Source/Engine/Animations/AnimationData.cpp b/Source/Engine/Animations/AnimationData.cpp new file mode 100644 index 000000000..8c3611d7a --- /dev/null +++ b/Source/Engine/Animations/AnimationData.cpp @@ -0,0 +1,84 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#include "AnimationData.h" + +void NodeAnimationData::Evaluate(float time, Transform* result, bool loop) const +{ + if (Position.GetKeyframes().HasItems()) +#if USE_LARGE_WORLDS + { + Float3 position; + Position.Evaluate(position, time, loop); + result->Translation = position; + } +#else + Position.Evaluate(result->Translation, time, loop); +#endif + if (Rotation.GetKeyframes().HasItems()) + Rotation.Evaluate(result->Orientation, time, loop); + if (Scale.GetKeyframes().HasItems()) + Scale.Evaluate(result->Scale, time, loop); +} + +void NodeAnimationData::EvaluateAll(float time, Transform* result, bool loop) const +{ + Float3 position; + Position.Evaluate(position, time, loop); + result->Translation = position; + Rotation.Evaluate(result->Orientation, time, loop); + Scale.Evaluate(result->Scale, time, loop); +} + +int32 NodeAnimationData::GetKeyframesCount() const +{ + return Position.GetKeyframes().Count() + Rotation.GetKeyframes().Count() + Scale.GetKeyframes().Count(); +} + +uint64 NodeAnimationData::GetMemoryUsage() const +{ + return NodeName.Length() * sizeof(Char) + Position.GetMemoryUsage() + Rotation.GetMemoryUsage() + Scale.GetMemoryUsage(); +} + +uint64 AnimationData::GetMemoryUsage() const +{ + uint64 result = (Name.Length() + RootNodeName.Length()) * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData); + for (const auto& e : Channels) + result += e.GetMemoryUsage(); + return result; +} + +int32 AnimationData::GetKeyframesCount() const +{ + int32 result = 0; + for (int32 i = 0; i < Channels.Count(); i++) + result += Channels[i].GetKeyframesCount(); + return result; +} + +NodeAnimationData* AnimationData::GetChannel(const StringView& name) +{ + for (auto& e : Channels) + if (e.NodeName == name) + return &e; + return nullptr; +} + +void AnimationData::Swap(AnimationData& other) +{ + ::Swap(Duration, other.Duration); + ::Swap(FramesPerSecond, other.FramesPerSecond); + ::Swap(RootMotionFlags, other.RootMotionFlags); + ::Swap(Name, other.Name); + ::Swap(RootNodeName, other.RootNodeName); + Channels.Swap(other.Channels); +} + +void AnimationData::Dispose() +{ + Name.Clear(); + Duration = 0.0; + FramesPerSecond = 0.0; + RootNodeName.Clear(); + RootMotionFlags = AnimationRootMotionFlags::None; + Channels.Resize(0); +} diff --git a/Source/Engine/Animations/AnimationData.h b/Source/Engine/Animations/AnimationData.h index c69de8afa..00717b05f 100644 --- a/Source/Engine/Animations/AnimationData.h +++ b/Source/Engine/Animations/AnimationData.h @@ -3,8 +3,8 @@ #pragma once #include "Engine/Core/Types/String.h" -#include "Engine/Animations/Curve.h" #include "Engine/Core/Math/Transform.h" +#include "Engine/Animations/Curve.h" /// /// Single node animation data container. @@ -50,19 +50,7 @@ public: /// The time to evaluate the curves at. /// The interpolated value from the curve at provided time. /// If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped. - void Evaluate(float time, Transform* result, bool loop = true) const - { - if (Position.GetKeyframes().HasItems()) - { - Float3 position; - Position.Evaluate(position, time, loop); - result->Translation = position; - } - if (Rotation.GetKeyframes().HasItems()) - Rotation.Evaluate(result->Orientation, time, loop); - if (Scale.GetKeyframes().HasItems()) - Scale.Evaluate(result->Scale, time, loop); - } + void Evaluate(float time, Transform* result, bool loop = true) const; /// /// Evaluates the animation transformation at the specified time. @@ -70,29 +58,37 @@ public: /// The time to evaluate the curves at. /// The interpolated value from the curve at provided time. /// If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped. - void EvaluateAll(float time, Transform* result, bool loop = true) const - { - Float3 position; - Position.Evaluate(position, time, loop); - result->Translation = position; - Rotation.Evaluate(result->Orientation, time, loop); - Scale.Evaluate(result->Scale, time, loop); - } + void EvaluateAll(float time, Transform* result, bool loop = true) const; /// /// Gets the total amount of keyframes in the animation curves. /// - int32 GetKeyframesCount() const - { - return Position.GetKeyframes().Count() + Rotation.GetKeyframes().Count() + Scale.GetKeyframes().Count(); - } + int32 GetKeyframesCount() const; - uint64 GetMemoryUsage() const - { - return NodeName.Length() * sizeof(Char) + Position.GetMemoryUsage() + Rotation.GetMemoryUsage() + Scale.GetMemoryUsage(); - } + uint64 GetMemoryUsage() const; }; +/// +/// Root Motion modes that can be applied by the animation. Used as flags for selective behavior. +/// +API_ENUM(Attributes="Flags") enum class AnimationRootMotionFlags : byte +{ + // No root motion. + None = 0, + // Root node position along XZ plane. Applies horizontal movement. Good for stationary animations (eg. idle). + RootPositionXZ = 1 << 0, + // Root node position along Y axis (up). Applies vertical movement. Good for all 'grounded' animations unless jumping is handled from code. + RootPositionY = 1 << 1, + // Root node rotation. Applies orientation changes. Good for animations that have baked-in root rotation (eg. turn animations). + RootRotation = 1 << 2, + // Root node position. + RootPosition = RootPositionXZ | RootPositionY, + // Root node position and rotation. + RootTransform = RootPosition | RootRotation, +}; + +DECLARE_ENUM_OPERATORS(AnimationRootMotionFlags); + /// /// Skeleton nodes animation data container. Includes metadata about animation sampling, duration and node animations curves. /// @@ -111,7 +107,7 @@ struct AnimationData /// /// Enables root motion extraction support from this animation. /// - bool EnableRootMotion = false; + AnimationRootMotionFlags RootMotionFlags = AnimationRootMotionFlags::None; /// /// The animation name. @@ -140,49 +136,23 @@ public: return static_cast(Duration / FramesPerSecond); } - uint64 GetMemoryUsage() const - { - uint64 result = (Name.Length() + RootNodeName.Length()) * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData); - for (const auto& e : Channels) - result += e.GetMemoryUsage(); - return result; - } + uint64 GetMemoryUsage() const; /// /// Gets the total amount of keyframes in the all animation channels. /// - int32 GetKeyframesCount() const - { - int32 result = 0; - for (int32 i = 0; i < Channels.Count(); i++) - result += Channels[i].GetKeyframesCount(); - return result; - } + int32 GetKeyframesCount() const; + + NodeAnimationData* GetChannel(const StringView& name); /// /// Swaps the contents of object with the other object without copy operation. Performs fast internal data exchange. /// /// The other object. - void Swap(AnimationData& other) - { - ::Swap(Duration, other.Duration); - ::Swap(FramesPerSecond, other.FramesPerSecond); - ::Swap(EnableRootMotion, other.EnableRootMotion); - ::Swap(Name, other.Name); - ::Swap(RootNodeName, other.RootNodeName); - Channels.Swap(other.Channels); - } + void Swap(AnimationData& other); /// /// Releases data. /// - void Dispose() - { - Name.Clear(); - Duration = 0.0; - FramesPerSecond = 0.0; - RootNodeName.Clear(); - EnableRootMotion = false; - Channels.Resize(0); - } + void Dispose(); }; diff --git a/Source/Engine/Animations/Curve.h b/Source/Engine/Animations/Curve.h index 74ad4168d..e0e7bd6b5 100644 --- a/Source/Engine/Animations/Curve.h +++ b/Source/Engine/Animations/Curve.h @@ -730,7 +730,7 @@ public: void TransformTime(float timeScale, float timeOffset) { for (int32 i = 0; i < _keyframes.Count(); i++) - _keyframes[i].Time = _keyframes[i].Time * timeScale + timeOffset;; + _keyframes[i].Time = _keyframes[i].Time * timeScale + timeOffset; } uint64 GetMemoryUsage() const diff --git a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp index c05b82b70..af5508705 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp @@ -157,7 +157,7 @@ bool AnimGraphBase::onNodeLoaded(Node* n) if (_rootNode->Values.Count() < 1) { _rootNode->Values.Resize(1); - _rootNode->Values[0] = (int32)RootMotionMode::NoExtraction; + _rootNode->Values[0] = (int32)RootMotionExtraction::NoExtraction; } break; // Animation diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp index 09c03b71a..93bfc2609 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.cpp @@ -213,7 +213,7 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) // Init data from base model _skeletonNodesCount = skeleton.Nodes.Count(); - _rootMotionMode = (RootMotionMode)(int32)_graph._rootNode->Values[0]; + _rootMotionMode = (RootMotionExtraction)(int32)_graph._rootNode->Values[0]; // Prepare context data for the evaluation context.GraphStack.Clear(); diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h index f3c148e0a..b570cf9fe 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.h +++ b/Source/Engine/Animations/Graph/AnimGraph.h @@ -92,9 +92,9 @@ enum class BoneTransformMode }; /// -/// The animated model root motion mode. +/// The animated model root motion extraction modes. /// -enum class RootMotionMode +enum class RootMotionExtraction { /// /// Don't extract nor apply the root motion. @@ -815,7 +815,7 @@ class AnimGraphExecutor : public VisjectExecutor friend AnimGraphNode; private: AnimGraph& _graph; - RootMotionMode _rootMotionMode = RootMotionMode::NoExtraction; + RootMotionExtraction _rootMotionMode = RootMotionExtraction::NoExtraction; int32 _skeletonNodesCount = 0; // Per-thread context to allow async execution diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index 536ed7d83..30b1bb022 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -21,13 +21,13 @@ namespace base += additive; } - FORCE_INLINE void NormalizeRotations(AnimGraphImpulse* nodes, RootMotionMode rootMotionMode) + FORCE_INLINE void NormalizeRotations(AnimGraphImpulse* nodes, RootMotionExtraction rootMotionMode) { for (int32 i = 0; i < nodes->Nodes.Count(); i++) { nodes->Nodes[i].Orientation.Normalize(); } - if (rootMotionMode != RootMotionMode::NoExtraction) + if (rootMotionMode != RootMotionExtraction::NoExtraction) { nodes->RootMotion.Orientation.Normalize(); } @@ -323,16 +323,21 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* } // Handle root motion - if (_rootMotionMode != RootMotionMode::NoExtraction && anim->Data.EnableRootMotion) + if (_rootMotionMode != RootMotionExtraction::NoExtraction && anim->Data.RootMotionFlags != AnimationRootMotionFlags::None) { // Calculate the root motion node transformation + const bool motionPositionXZ = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootPositionXZ); + const bool motionPositionY = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootPositionY); + const bool motionRotation = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootRotation); + const Vector3 motionPositionMask(motionPositionXZ ? 1.0f : 0.0f, motionPositionY ? 1.0f : 0.0f, motionPositionXZ ? 1.0f : 0.0f); + const bool motionPosition = motionPositionXZ | motionPositionY; const int32 rootNodeIndex = GetRootNodeIndex(anim); const Transform& refPose = emptyNodes->Nodes[rootNodeIndex]; Transform& rootNode = nodes->Nodes[rootNodeIndex]; Transform& dstNode = nodes->RootMotion; Transform srcNode = Transform::Identity; const int32 nodeToChannel = mapping.NodesMapping[rootNodeIndex]; - if (_rootMotionMode == RootMotionMode::Enable && nodeToChannel != -1) + if (_rootMotionMode == RootMotionExtraction::Enable && nodeToChannel != -1) { // Get the root bone transformation Transform rootBefore = refPose; @@ -356,16 +361,20 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* // Complex motion calculation to preserve the looped movement // (end - before + now - begin) // It sums the motion since the last update to anim end and since the start to now - srcNode.Translation = rootEnd.Translation - rootBefore.Translation + rootNode.Translation - rootBegin.Translation; - srcNode.Orientation = rootEnd.Orientation * rootBefore.Orientation.Conjugated() * (rootNode.Orientation * rootBegin.Orientation.Conjugated()); + if (motionPosition) + srcNode.Translation = (rootEnd.Translation - rootBefore.Translation + rootNode.Translation - rootBegin.Translation) * motionPositionMask; + if (motionRotation) + srcNode.Orientation = rootEnd.Orientation * rootBefore.Orientation.Conjugated() * (rootNode.Orientation * rootBegin.Orientation.Conjugated()); //srcNode.Orientation = Quaternion::Identity; } else { // Simple motion delta // (now - before) - srcNode.Translation = rootNode.Translation - rootBefore.Translation; - srcNode.Orientation = rootBefore.Orientation.Conjugated() * rootNode.Orientation; + if (motionPosition) + srcNode.Translation = (rootNode.Translation - rootBefore.Translation) * motionPositionMask; + if (motionRotation) + srcNode.Orientation = rootBefore.Orientation.Conjugated() * rootNode.Orientation; } // Convert root motion from local-space to the actor-space (eg. if root node is not actually a root and its parents have rotation/scale) @@ -379,28 +388,40 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* } } - // Remove root node motion after extraction - rootNode = refPose; + // Remove root node motion after extraction (only extracted components) + if (motionPosition) + rootNode.Translation = refPose.Translation * motionPositionMask + rootNode.Translation * (Vector3::One - motionPositionMask); + if (motionRotation) + rootNode.Orientation = refPose.Orientation; // Blend root motion if (mode == ProcessAnimationMode::BlendAdditive) { - dstNode.Translation += srcNode.Translation * weight; - BlendAdditiveWeightedRotation(dstNode.Orientation, srcNode.Orientation, weight); + if (motionPosition) + dstNode.Translation += srcNode.Translation * weight * motionPositionMask; + if (motionRotation) + BlendAdditiveWeightedRotation(dstNode.Orientation, srcNode.Orientation, weight); } else if (mode == ProcessAnimationMode::Add) { - dstNode.Translation += srcNode.Translation * weight; - dstNode.Orientation += srcNode.Orientation * weight; + if (motionPosition) + dstNode.Translation += srcNode.Translation * weight * motionPositionMask; + if (motionRotation) + dstNode.Orientation += srcNode.Orientation * weight; } else if (weighted) { - dstNode.Translation = srcNode.Translation * weight; - dstNode.Orientation = srcNode.Orientation * weight; + if (motionPosition) + dstNode.Translation = srcNode.Translation * weight * motionPositionMask; + if (motionRotation) + dstNode.Orientation = srcNode.Orientation * weight; } else { - dstNode = srcNode; + if (motionPosition) + dstNode.Translation = srcNode.Translation * motionPositionMask; + if (motionRotation) + dstNode.Orientation = srcNode.Orientation; } } diff --git a/Source/Engine/Content/Assets/Animation.cpp b/Source/Engine/Content/Assets/Animation.cpp index 8aa635244..ec418c5fb 100644 --- a/Source/Engine/Content/Assets/Animation.cpp +++ b/Source/Engine/Content/Assets/Animation.cpp @@ -413,10 +413,10 @@ bool Animation::Save(const StringView& path) MemoryWriteStream stream(4096); // Info - stream.WriteInt32(102); + stream.WriteInt32(103); stream.WriteDouble(Data.Duration); stream.WriteDouble(Data.FramesPerSecond); - stream.WriteBool(Data.EnableRootMotion); + stream.WriteByte((byte)Data.RootMotionFlags); stream.WriteString(Data.RootNodeName, 13); // Animation channels @@ -532,17 +532,22 @@ Asset::LoadResult Animation::load() int32 headerVersion = *(int32*)stream.GetPositionHandle(); switch (headerVersion) { - case 100: - case 101: - case 102: - { + case 103: stream.ReadInt32(&headerVersion); stream.ReadDouble(&Data.Duration); stream.ReadDouble(&Data.FramesPerSecond); - Data.EnableRootMotion = stream.ReadBool(); + stream.ReadByte((byte*)&Data.RootMotionFlags); + stream.ReadString(&Data.RootNodeName, 13); + break; + case 100: + case 101: + case 102: + stream.ReadInt32(&headerVersion); + stream.ReadDouble(&Data.Duration); + stream.ReadDouble(&Data.FramesPerSecond); + Data.RootMotionFlags = stream.ReadBool() ? AnimationRootMotionFlags::RootPositionXZ : AnimationRootMotionFlags::None; stream.ReadString(&Data.RootNodeName, 13); break; - } default: stream.ReadDouble(&Data.Duration); stream.ReadDouble(&Data.FramesPerSecond); diff --git a/Source/Engine/ContentImporters/CreateAnimationGraph.cpp b/Source/Engine/ContentImporters/CreateAnimationGraph.cpp index 695e5c2a7..e9c88b68c 100644 --- a/Source/Engine/ContentImporters/CreateAnimationGraph.cpp +++ b/Source/Engine/ContentImporters/CreateAnimationGraph.cpp @@ -20,7 +20,7 @@ CreateAssetResult CreateAnimationGraph::Create(CreateAssetContext& context) rootNode.Type = GRAPH_NODE_MAKE_TYPE(9, 1); rootNode.ID = 1; rootNode.Values.Resize(1); - rootNode.Values[0] = (int32)RootMotionMode::NoExtraction; + rootNode.Values[0] = (int32)RootMotionExtraction::NoExtraction; rootNode.Boxes.Resize(1); rootNode.Boxes[0] = AnimGraphBox(&rootNode, 0, VariantType::Void); diff --git a/Source/Engine/Graphics/Models/ModelData.cpp b/Source/Engine/Graphics/Models/ModelData.cpp index 6e6ae04b5..cc95ed6ba 100644 --- a/Source/Engine/Graphics/Models/ModelData.cpp +++ b/Source/Engine/Graphics/Models/ModelData.cpp @@ -911,10 +911,10 @@ bool ModelData::Pack2AnimationHeader(WriteStream* stream, int32 animIndex) const } // Info - stream->WriteInt32(100); // Header version (for fast version upgrades without serialization format change) + stream->WriteInt32(103); // Header version (for fast version upgrades without serialization format change) stream->WriteDouble(anim.Duration); stream->WriteDouble(anim.FramesPerSecond); - stream->WriteBool(anim.EnableRootMotion); + stream->WriteByte((byte)anim.RootMotionFlags); stream->WriteString(anim.RootNodeName, 13); // Animation channels @@ -928,6 +928,12 @@ bool ModelData::Pack2AnimationHeader(WriteStream* stream, int32 animIndex) const Serialization::Serialize(*stream, channel.Scale); } + // Animation events + stream->WriteInt32(0); + + // Nested animations + stream->WriteInt32(0); + return false; } diff --git a/Source/Engine/Graphics/Models/SkeletonData.h b/Source/Engine/Graphics/Models/SkeletonData.h index 816f69c85..83908d18a 100644 --- a/Source/Engine/Graphics/Models/SkeletonData.h +++ b/Source/Engine/Graphics/Models/SkeletonData.h @@ -91,7 +91,7 @@ public: FORCE_INLINE SkeletonNode& RootNode() { ASSERT(Nodes.HasItems()); - return Nodes[0]; + return Nodes.Get()[0]; } /// @@ -100,52 +100,24 @@ public: FORCE_INLINE const SkeletonNode& RootNode() const { ASSERT(Nodes.HasItems()); - return Nodes[0]; + return Nodes.Get()[0]; } /// /// Swaps the contents of object with the other object without copy operation. Performs fast internal data exchange. /// - void Swap(SkeletonData& other) - { - Nodes.Swap(other.Nodes); - Bones.Swap(other.Bones); - } + void Swap(SkeletonData& other); - int32 FindNode(const StringView& name) const - { - for (int32 i = 0; i < Nodes.Count(); i++) - { - if (Nodes[i].Name == name) - return i; - } - return -1; - } + Transform GetNodeTransform(int32 nodeIndex) const; + void SetNodeTransform(int32 nodeIndex, const Transform& value); - int32 FindBone(int32 nodeIndex) const - { - for (int32 i = 0; i < Bones.Count(); i++) - { - if (Bones[i].NodeIndex == nodeIndex) - return i; - } - return -1; - } + int32 FindNode(const StringView& name) const; + int32 FindBone(int32 nodeIndex) const; - uint64 GetMemoryUsage() const - { - uint64 result = Nodes.Capacity() * sizeof(SkeletonNode) + Bones.Capacity() * sizeof(SkeletonBone); - for (const auto& e : Nodes) - result += (e.Name.Length() + 1) * sizeof(Char); - return result; - } + uint64 GetMemoryUsage() const; /// /// Releases data. /// - void Dispose() - { - Nodes.Resize(0); - Bones.Resize(0); - } + void Dispose(); }; diff --git a/Source/Engine/Graphics/Models/SkinnedMesh.cpp b/Source/Engine/Graphics/Models/SkinnedMesh.cpp index 48ff53549..fc0dccf8a 100644 --- a/Source/Engine/Graphics/Models/SkinnedMesh.cpp +++ b/Source/Engine/Graphics/Models/SkinnedMesh.cpp @@ -18,6 +18,69 @@ #include "Engine/Threading/Task.h" #include "Engine/Threading/Threading.h" +void SkeletonData::Swap(SkeletonData& other) +{ + Nodes.Swap(other.Nodes); + Bones.Swap(other.Bones); +} + +Transform SkeletonData::GetNodeTransform(int32 nodeIndex) const +{ + const int32 parentIndex = Nodes[nodeIndex].ParentIndex; + if (parentIndex == -1) + { + return Nodes[nodeIndex].LocalTransform; + } + const Transform parentTransform = GetNodeTransform(parentIndex); + return parentTransform.LocalToWorld(Nodes[nodeIndex].LocalTransform); +} + +void SkeletonData::SetNodeTransform(int32 nodeIndex, const Transform& value) +{ + const int32 parentIndex = Nodes[nodeIndex].ParentIndex; + if (parentIndex == -1) + { + Nodes[nodeIndex].LocalTransform = value; + return; + } + const Transform parentTransform = GetNodeTransform(parentIndex); + parentTransform.WorldToLocal(value, Nodes[nodeIndex].LocalTransform); +} + +int32 SkeletonData::FindNode(const StringView& name) const +{ + for (int32 i = 0; i < Nodes.Count(); i++) + { + if (Nodes[i].Name == name) + return i; + } + return -1; +} + +int32 SkeletonData::FindBone(int32 nodeIndex) const +{ + for (int32 i = 0; i < Bones.Count(); i++) + { + if (Bones[i].NodeIndex == nodeIndex) + return i; + } + return -1; +} + +uint64 SkeletonData::GetMemoryUsage() const +{ + uint64 result = Nodes.Capacity() * sizeof(SkeletonNode) + Bones.Capacity() * sizeof(SkeletonBone); + for (const auto& e : Nodes) + result += (e.Name.Length() + 1) * sizeof(Char); + return result; +} + +void SkeletonData::Dispose() +{ + Nodes.Resize(0); + Bones.Resize(0); +} + void SkinnedMesh::Init(SkinnedModel* model, int32 lodIndex, int32 index, int32 materialSlotIndex, const BoundingBox& box, const BoundingSphere& sphere) { _model = model; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index d0aa537f6..447301f46 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -19,15 +19,15 @@ #include "Engine/Content/Content.h" #include "Engine/Serialization/MemoryWriteStream.h" #if USE_EDITOR +#include "Engine/Core/Utilities.h" +#include "Engine/Core/Types/StringView.h" #include "Engine/Core/Types/DateTime.h" #include "Engine/Core/Types/TimeSpan.h" #include "Engine/Core/Types/Pair.h" #include "Engine/Core/Types/Variant.h" +#include "Engine/Platform/FileSystem.h" #include "Engine/Graphics/Models/SkeletonUpdater.h" #include "Engine/Graphics/Models/SkeletonMapping.h" -#include "Engine/Core/Utilities.h" -#include "Engine/Core/Types/StringView.h" -#include "Engine/Platform/FileSystem.h" #include "Engine/Tools/TextureTool/TextureTool.h" #include "Engine/ContentImporters/AssetsImportingManager.h" #include "Engine/ContentImporters/CreateMaterial.h" @@ -35,6 +35,7 @@ #include "Engine/ContentImporters/CreateCollisionData.h" #include "Engine/Serialization/Serialization.h" #include "Editor/Utilities/EditorUtilities.h" +#include "Engine/Animations/Graph/AnimGraph.h" #include #endif @@ -361,7 +362,8 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(SkipEmptyCurves); SERIALIZE(OptimizeKeyframes); SERIALIZE(ImportScaleTracks); - SERIALIZE(EnableRootMotion); + SERIALIZE(RootMotion); + SERIALIZE(RootMotionFlags); SERIALIZE(RootNodeName); SERIALIZE(GenerateLODs); SERIALIZE(BaseLOD); @@ -410,7 +412,8 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(SkipEmptyCurves); DESERIALIZE(OptimizeKeyframes); DESERIALIZE(ImportScaleTracks); - DESERIALIZE(EnableRootMotion); + DESERIALIZE(RootMotion); + DESERIALIZE(RootMotionFlags); DESERIALIZE(RootNodeName); DESERIALIZE(GenerateLODs); DESERIALIZE(BaseLOD); @@ -435,6 +438,15 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(AnimationIndex); if (AnimationIndex != -1) ObjectIndex = AnimationIndex; + + // [Deprecated on 08.02.2024, expires on 08.02.2026] + bool EnableRootMotion = false; + DESERIALIZE(EnableRootMotion); + if (EnableRootMotion) + { + RootMotion = RootMotionMode::ExtractNode; + RootMotionFlags = AnimationRootMotionFlags::RootPositionXZ; + } } void RemoveNamespace(String& name) @@ -809,6 +821,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option break; case ModelType::Animation: options.ImportTypes = ImportDataTypes::Animations; + if (options.RootMotion == RootMotionMode::ExtractCenterOfMass) + options.ImportTypes |= ImportDataTypes::Skeleton; break; case ModelType::Prefab: options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Animations; @@ -1373,6 +1387,129 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option } } + // Process root motion setup + animation.RootMotionFlags = options.RootMotion != RootMotionMode::None ? options.RootMotionFlags : AnimationRootMotionFlags::None; + animation.RootNodeName = options.RootNodeName.TrimTrailing(); + if (animation.RootMotionFlags != AnimationRootMotionFlags::None && animation.Channels.HasItems()) + { + if (options.RootMotion == RootMotionMode::ExtractNode) + { + if (animation.RootNodeName.HasChars() && animation.GetChannel(animation.RootNodeName) == nullptr) + { + LOG(Warning, "Missing Root Motion node '{}'", animation.RootNodeName); + } + } + else if (options.RootMotion == RootMotionMode::ExtractCenterOfMass && data.Skeleton.Nodes.HasItems()) // TODO: finish implementing this + { + // Setup root node animation track + const auto& rootName = data.Skeleton.Nodes.First().Name; + auto rootChannelPtr = animation.GetChannel(rootName); + if (!rootChannelPtr) + { + animation.Channels.Insert(0, NodeAnimationData()); + rootChannelPtr = &animation.Channels[0]; + rootChannelPtr->NodeName = rootName; + } + animation.RootNodeName = rootName; + auto& rootChannel = *rootChannelPtr; + rootChannel.Position.Clear(); + + // Calculate skeleton center of mass position over the animation frames + const int32 frames = (int32)animation.Duration; + const int32 nodes = data.Skeleton.Nodes.Count(); + Array centerOfMass; + centerOfMass.Resize(frames); + for (int32 frame = 0; frame < frames; frame++) + { + auto& key = centerOfMass[frame]; + + // Evaluate skeleton pose at the animation frame + AnimGraphImpulse pose; + pose.Nodes.Resize(nodes); + for (int32 nodeIndex = 0; nodeIndex < nodes; nodeIndex++) + { + Transform srcNode = data.Skeleton.Nodes[nodeIndex].LocalTransform; + auto& node = data.Skeleton.Nodes[nodeIndex]; + if (auto* channel = animation.GetChannel(node.Name)) + channel->Evaluate(frame, &srcNode, false); + pose.Nodes[nodeIndex] = srcNode; + } + + // Calculate average location of the pose (center of mass) + key = Float3::Zero; + for (int32 nodeIndex = 0; nodeIndex < nodes; nodeIndex++) + key += pose.GetNodeModelTransformation(data.Skeleton, nodeIndex).Translation; + key /= nodes; + } + + // Calculate skeleton center of mass movement over the animation frames + rootChannel.Position.Resize(frames); + const Float3 centerOfMassRefPose = centerOfMass[0]; + for (int32 frame = 0; frame < frames; frame++) + { + auto& key = rootChannel.Position[frame]; + key.Time = frame; + key.Value = centerOfMass[frame] - centerOfMassRefPose; + } + + // Remove root motion from the children (eg. if Root moves, then Hips should skip that movement delta) + Float3 maxMotion = Float3::Zero; + for (int32 i = 0; i < animation.Channels.Count(); i++) + { + auto& anim = animation.Channels[i]; + const int32 animNodeIndex = data.Skeleton.FindNode(anim.NodeName); + + // Skip channels that have one of their parents already animated + { + int32 nodeIndex = animNodeIndex; + nodeIndex = data.Skeleton.Nodes[nodeIndex].ParentIndex; + while (nodeIndex > 0) + { + const String& nodeName = data.Skeleton.Nodes[nodeIndex].Name; + if (animation.GetChannel(nodeName) != nullptr) + break; + nodeIndex = data.Skeleton.Nodes[nodeIndex].ParentIndex; + } + if (nodeIndex > 0 || &anim == rootChannelPtr) + continue; + } + + // Remove motion + auto& animPos = anim.Position.GetKeyframes(); + for (int32 frame = 0; frame < animPos.Count(); frame++) + { + auto& key = animPos[frame]; + + // Evaluate root motion at the keyframe location + Float3 rootMotion; + rootChannel.Position.Evaluate(rootMotion, key.Time, false); + Float3::Max(maxMotion, rootMotion, maxMotion); + + // Evaluate skeleton pose at the animation frame + AnimGraphImpulse pose; + pose.Nodes.Resize(nodes); + pose.Nodes[0] = data.Skeleton.Nodes[0].LocalTransform; // Use ref pose of root + for (int32 nodeIndex = 1; nodeIndex < nodes; nodeIndex++) // Skip new root + { + Transform srcNode = data.Skeleton.Nodes[nodeIndex].LocalTransform; + auto& node = data.Skeleton.Nodes[nodeIndex]; + if (auto* channel = animation.GetChannel(node.Name)) + channel->Evaluate(frame, &srcNode, false); + pose.Nodes[nodeIndex] = srcNode; + } + + // Convert root motion to the local space of this node so the node stays at the same location after adding new root channel + Transform parentNodeTransform = pose.GetNodeModelTransformation(data.Skeleton, data.Skeleton.Nodes[animNodeIndex].ParentIndex); + rootMotion = parentNodeTransform.WorldToLocal(rootMotion); + + // Remove motion + key.Value -= rootMotion; + } + } + LOG(Info, "Calculated root motion: {}", maxMotion); + } + } + // Optimize the keyframes if (options.OptimizeKeyframes) { @@ -1395,9 +1532,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option const int32 after = animation.GetKeyframesCount(); LOG(Info, "Optimized {0} animation keyframe(s). Before: {1}, after: {2}, Ratio: {3}%", before - after, before, after, Utilities::RoundTo2DecimalPlaces((float)after / before)); } - - animation.EnableRootMotion = options.EnableRootMotion; - animation.RootNodeName = options.RootNodeName; } } diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index 90292d72c..6468c1a43 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -129,6 +129,19 @@ public: Custom = 1, }; + /// + /// Declares the imported animation Root Motion modes. + /// + API_ENUM(Attributes="HideInEditor") enum class RootMotionMode + { + // Root Motion feature is disabled. + None = 0, + // Motion is extracted from the root node (or node specified by name). + ExtractNode = 1, + // Motion is extracted from the center of mass movement (estimated based on the skeleton pose animation). + ExtractCenterOfMass = 2, + }; + /// /// Model import options. /// @@ -228,9 +241,12 @@ public: bool ImportScaleTracks = false; // Enables root motion extraction support from this animation. API_FIELD(Attributes="EditorOrder(1060), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowAnimation))") - bool EnableRootMotion = false; + RootMotionMode RootMotion = RootMotionMode::None; + // Adjusts root motion applying flags. Can customize how root node animation can affect target actor movement (eg. apply both position and rotation changes). + API_FIELD(Attributes="EditorOrder(1060), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowRootMotion))") + AnimationRootMotionFlags RootMotionFlags = AnimationRootMotionFlags::RootPositionXZ; // The custom node name to be used as a root motion source. If not specified the actual root node will be used. - API_FIELD(Attributes="EditorOrder(1070), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowAnimation))") + API_FIELD(Attributes="EditorOrder(1070), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowRootMotion))") String RootNodeName = TEXT(""); public: // Level Of Detail