Add AnimationRootMotionFlags to configure root motion component extraction
Add `RootMotionMode` to support extracting root motion from animated skeleton pose center of mass #1429 #2152
This commit is contained in:
@@ -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;
|
||||
|
||||
84
Source/Engine/Animations/AnimationData.cpp
Normal file
84
Source/Engine/Animations/AnimationData.cpp
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
/// <summary>
|
||||
/// Single node animation data container.
|
||||
@@ -50,19 +50,7 @@ public:
|
||||
/// <param name="time">The time to evaluate the curves at.</param>
|
||||
/// <param name="result">The interpolated value from the curve at provided time.</param>
|
||||
/// <param name="loop">If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped.</param>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the animation transformation at the specified time.
|
||||
@@ -70,29 +58,37 @@ public:
|
||||
/// <param name="time">The time to evaluate the curves at.</param>
|
||||
/// <param name="result">The interpolated value from the curve at provided time.</param>
|
||||
/// <param name="loop">If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped.</param>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total amount of keyframes in the animation curves.
|
||||
/// </summary>
|
||||
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;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Root Motion modes that can be applied by the animation. Used as flags for selective behavior.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Skeleton nodes animation data container. Includes metadata about animation sampling, duration and node animations curves.
|
||||
/// </summary>
|
||||
@@ -111,7 +107,7 @@ struct AnimationData
|
||||
/// <summary>
|
||||
/// Enables root motion extraction support from this animation.
|
||||
/// </summary>
|
||||
bool EnableRootMotion = false;
|
||||
AnimationRootMotionFlags RootMotionFlags = AnimationRootMotionFlags::None;
|
||||
|
||||
/// <summary>
|
||||
/// The animation name.
|
||||
@@ -140,49 +136,23 @@ public:
|
||||
return static_cast<float>(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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total amount of keyframes in the all animation channels.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Swaps the contents of object with the other object without copy operation. Performs fast internal data exchange.
|
||||
/// </summary>
|
||||
/// <param name="other">The other object.</param>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Releases data.
|
||||
/// </summary>
|
||||
void Dispose()
|
||||
{
|
||||
Name.Clear();
|
||||
Duration = 0.0;
|
||||
FramesPerSecond = 0.0;
|
||||
RootNodeName.Clear();
|
||||
EnableRootMotion = false;
|
||||
Channels.Resize(0);
|
||||
}
|
||||
void Dispose();
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -92,9 +92,9 @@ enum class BoneTransformMode
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The animated model root motion mode.
|
||||
/// The animated model root motion extraction modes.
|
||||
/// </summary>
|
||||
enum class RootMotionMode
|
||||
enum class RootMotionExtraction
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
|
||||
@@ -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,7 +361,9 @@ 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;
|
||||
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;
|
||||
}
|
||||
@@ -364,7 +371,9 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
|
||||
{
|
||||
// Simple motion delta
|
||||
// (now - before)
|
||||
srcNode.Translation = rootNode.Translation - rootBefore.Translation;
|
||||
if (motionPosition)
|
||||
srcNode.Translation = (rootNode.Translation - rootBefore.Translation) * motionPositionMask;
|
||||
if (motionRotation)
|
||||
srcNode.Orientation = rootBefore.Orientation.Conjugated() * rootNode.Orientation;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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;
|
||||
if (motionPosition)
|
||||
dstNode.Translation += srcNode.Translation * weight * motionPositionMask;
|
||||
if (motionRotation)
|
||||
dstNode.Orientation += srcNode.Orientation * weight;
|
||||
}
|
||||
else if (weighted)
|
||||
{
|
||||
dstNode.Translation = srcNode.Translation * 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ public:
|
||||
FORCE_INLINE SkeletonNode& RootNode()
|
||||
{
|
||||
ASSERT(Nodes.HasItems());
|
||||
return Nodes[0];
|
||||
return Nodes.Get()[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -100,52 +100,24 @@ public:
|
||||
FORCE_INLINE const SkeletonNode& RootNode() const
|
||||
{
|
||||
ASSERT(Nodes.HasItems());
|
||||
return Nodes[0];
|
||||
return Nodes.Get()[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swaps the contents of object with the other object without copy operation. Performs fast internal data exchange.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Releases data.
|
||||
/// </summary>
|
||||
void Dispose()
|
||||
{
|
||||
Nodes.Resize(0);
|
||||
Bones.Resize(0);
|
||||
}
|
||||
void Dispose();
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <ThirdParty/meshoptimizer/meshoptimizer.h>
|
||||
#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<Float3> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,19 @@ public:
|
||||
Custom = 1,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Declares the imported animation Root Motion modes.
|
||||
/// </summary>
|
||||
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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Model import options.
|
||||
/// </summary>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user