diff --git a/Source/Editor/Content/Proxy/AnimationProxy.cs b/Source/Editor/Content/Proxy/AnimationProxy.cs
index 898317aa6..bda0fa98d 100644
--- a/Source/Editor/Content/Proxy/AnimationProxy.cs
+++ b/Source/Editor/Content/Proxy/AnimationProxy.cs
@@ -37,6 +37,19 @@ namespace FlaxEditor.Content
///
public override Type AssetType => typeof(Animation);
+ ///
+ public override bool CanCreate(ContentFolder targetLocation)
+ {
+ return targetLocation.CanHaveAssets;
+ }
+
+ ///
+ public override void Create(string outputPath, object arg)
+ {
+ if (Editor.CreateAsset(Editor.NewAssetType.Animation, outputPath))
+ throw new Exception("Failed to create new asset.");
+ }
+
///
public override void OnThumbnailDrawBegin(ThumbnailRequest request, ContainerControl guiRoot, GPUContext context)
{
diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs
index 02530dab5..75ee0b481 100644
--- a/Source/Editor/Editor.cs
+++ b/Source/Editor/Editor.cs
@@ -789,6 +789,11 @@ namespace FlaxEditor
/// The .
///
AnimationGraphFunction = 10,
+
+ ///
+ /// The .
+ ///
+ Animation = 11,
}
///
diff --git a/Source/Editor/GUI/Timeline/AnimationTimeline.cs b/Source/Editor/GUI/Timeline/AnimationTimeline.cs
index 23b8c15d2..c3b425acc 100644
--- a/Source/Editor/GUI/Timeline/AnimationTimeline.cs
+++ b/Source/Editor/GUI/Timeline/AnimationTimeline.cs
@@ -1,6 +1,8 @@
// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
using System;
+using FlaxEditor.Content;
+using FlaxEditor.GUI.Drag;
using FlaxEditor.GUI.Timeline.Tracks;
using FlaxEditor.Viewport.Previews;
using FlaxEngine;
@@ -24,6 +26,7 @@ namespace FlaxEditor.GUI.Timeline
}
private AnimationPreview _preview;
+ internal Guid _id;
///
/// Gets or sets the animated preview used for the animation playback.
@@ -62,6 +65,7 @@ namespace FlaxEditor.GUI.Timeline
TrackArchetypes.Add(AnimationChannelTrack.GetArchetype());
TrackArchetypes.Add(AnimationChannelDataTrack.GetArchetype());
TrackArchetypes.Add(AnimationEventTrack.GetArchetype());
+ TrackArchetypes.Add(NestedAnimationTrack.GetArchetype());
}
///
@@ -169,5 +173,48 @@ namespace FlaxEditor.GUI.Timeline
base.OnSeek(frame);
}
+
+ ///
+ protected override void SetupDragDrop()
+ {
+ base.SetupDragDrop();
+
+ DragHandlers.Add(new DragHandler(new DragAssets(IsValidAsset), OnDragAsset));
+ }
+
+ private static bool IsValidAsset(AssetItem assetItem)
+ {
+ if (assetItem is BinaryAssetItem binaryAssetItem)
+ {
+ if (typeof(Animation).IsAssignableFrom(binaryAssetItem.Type))
+ {
+ var sceneAnimation = FlaxEngine.Content.LoadAsync(binaryAssetItem.ID);
+ if (sceneAnimation)
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void OnDragAsset(Timeline timeline, DragHelper drag)
+ {
+ foreach (var assetItem in ((DragAssets)drag).Objects)
+ {
+ if (assetItem is BinaryAssetItem binaryAssetItem)
+ {
+ if (typeof(Animation).IsAssignableFrom(binaryAssetItem.Type))
+ {
+ var animation = FlaxEngine.Content.LoadAsync(binaryAssetItem.ID);
+ if (!animation || animation.WaitForLoaded())
+ continue;
+ var track = (NestedAnimationTrack)timeline.NewTrack(NestedAnimationTrack.GetArchetype());
+ track.Asset = animation;
+ track.TrackMedia.DurationFrames = (int)(animation.Length * timeline.FramesPerSecond);
+ track.Rename(assetItem.ShortName);
+ timeline.AddTrack(track);
+ }
+ }
+ }
+ }
}
}
diff --git a/Source/Editor/GUI/Timeline/Tracks/NestedAnimationTrack.cs b/Source/Editor/GUI/Timeline/Tracks/NestedAnimationTrack.cs
new file mode 100644
index 000000000..7e84dbd62
--- /dev/null
+++ b/Source/Editor/GUI/Timeline/Tracks/NestedAnimationTrack.cs
@@ -0,0 +1,183 @@
+// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
+
+using System;
+using System.IO;
+using System.Linq;
+using FlaxEditor.GUI.Timeline.Undo;
+using FlaxEditor.Utilities;
+using FlaxEngine;
+
+namespace FlaxEditor.GUI.Timeline.Tracks
+{
+ ///
+ /// The timeline media that represents a nested animation media event.
+ ///
+ ///
+ public class NestedAnimationMedia : Media
+ {
+ private sealed class Proxy : ProxyBase
+ {
+ ///
+ /// The nested animation to play.
+ ///
+ [EditorDisplay("General"), EditorOrder(10)]
+ public Animation NestedAnimation
+ {
+ get => Track.Asset;
+ set => Track.Asset = value;
+ }
+
+ ///
+ /// If checked, the nested animation will loop when playback exceeds its duration. Otherwise it will stop play.
+ ///
+ [EditorDisplay("General"), EditorOrder(20)]
+ public bool Loop
+ {
+ get => Track.Loop;
+ set => Track.Loop = value;
+ }
+
+ ///
+ /// Animation playback speed.
+ ///
+ [EditorDisplay("General"), EditorOrder(30), Limit(0.0f, 100.0f, 0.001f)]
+ public float Speed
+ {
+ get => Track.Speed;
+ set => Track.Speed = value;
+ }
+
+ ///
+ /// Animation playback start time position (in seconds). Can be used to offset the nested animation start.
+ ///
+ [EditorDisplay("General"), EditorOrder(40)]
+ public float StartTime
+ {
+ get => Track.StartTime;
+ set => Track.StartTime = value;
+ }
+
+ ///
+ public Proxy(NestedAnimationTrack track, NestedAnimationMedia media)
+ : base(track, media)
+ {
+ }
+ }
+
+ ///
+ public override void OnTimelineChanged(Track track)
+ {
+ base.OnTimelineChanged(track);
+
+ PropertiesEditObject = track != null ? new Proxy((NestedAnimationTrack)track, this) : null;
+ }
+ }
+
+ ///
+ /// The timeline track that represents a nested animation playback.
+ ///
+ ///
+ public class NestedAnimationTrack : SingleMediaAssetTrack
+ {
+ ///
+ /// Gets the archetype.
+ ///
+ /// The archetype.
+ public static TrackArchetype GetArchetype()
+ {
+ return new TrackArchetype
+ {
+ TypeId = 20,
+ Name = "Nested Animation",
+ Create = options => new NestedAnimationTrack(ref options),
+ Load = LoadTrack,
+ Save = SaveTrack,
+ };
+ }
+
+ private static void LoadTrack(int version, Track track, BinaryReader stream)
+ {
+ var e = (NestedAnimationTrack)track;
+ Guid id = stream.ReadGuid();
+ e.Asset = FlaxEngine.Content.LoadAsync(id);
+ var m = e.TrackMedia;
+ m.StartFrame = (int)stream.ReadSingle();
+ m.DurationFrames = (int)stream.ReadSingle();
+ e.Speed = stream.ReadSingle();
+ e.StartTime = stream.ReadSingle();
+ }
+
+ private static void SaveTrack(Track track, BinaryWriter stream)
+ {
+ var e = (NestedAnimationTrack)track;
+ stream.WriteGuid(ref e.AssetID);
+ if (e.Media.Count != 0)
+ {
+ var m = e.TrackMedia;
+ stream.Write((float)m.StartFrame);
+ stream.Write((float)m.DurationFrames);
+ }
+ else
+ {
+ stream.Write((float)0);
+ stream.Write((float)track.Timeline.DurationFrames);
+ }
+ stream.Write(e.Speed);
+ stream.Write(e.StartTime);
+ }
+
+ ///
+ /// Nested animation playback speed.
+ ///
+ public float Speed = 1.0f;
+
+ ///
+ /// Nested animation playback start time position (in seconds). Can be used to offset the nested animation start.
+ ///
+ public float StartTime = 0.0f;
+
+ ///
+ public NestedAnimationTrack(ref TrackCreateOptions options)
+ : base(ref options)
+ {
+ MinMediaCount = 1;
+ MaxMediaCount = 1;
+ }
+
+ ///
+ protected override void OnAssetChanged()
+ {
+ base.OnAssetChanged();
+
+ CheckCyclicReferences();
+
+ if (Timeline != null && Asset && !Asset.WaitForLoaded())
+ {
+ using (new TrackUndoBlock(this))
+ TrackMedia.Duration = Asset.Length;
+ }
+ }
+
+ ///
+ public override void OnTimelineChanged(Timeline timeline)
+ {
+ base.OnTimelineChanged(timeline);
+
+ CheckCyclicReferences();
+ }
+
+ private void CheckCyclicReferences()
+ {
+ if (Asset && Timeline is AnimationTimeline timeline)
+ {
+ var refs = Asset.GetReferences();
+ var id = timeline._id;
+ if (Asset.ID == id || refs.Contains(id))
+ {
+ Asset = null;
+ throw new Exception("Cannot use nested scene animation (recursion).");
+ }
+ }
+ }
+ }
+}
diff --git a/Source/Editor/GUI/Timeline/Undo/EditTrackAction.cs b/Source/Editor/GUI/Timeline/Undo/EditTrackAction.cs
index 9bdb340f8..f57ece87d 100644
--- a/Source/Editor/GUI/Timeline/Undo/EditTrackAction.cs
+++ b/Source/Editor/GUI/Timeline/Undo/EditTrackAction.cs
@@ -42,8 +42,11 @@ namespace FlaxEditor.GUI.Timeline.Undo
track.Flags = (TrackFlags)stream.ReadByte();
track.Archetype.Load(Timeline.FormatVersion, track, stream);
}
- _timeline.ArrangeTracks();
- _timeline.MarkAsEdited();
+ if (_timeline != null)
+ {
+ _timeline.ArrangeTracks();
+ _timeline.MarkAsEdited();
+ }
track.OnUndo();
}
diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp
index 0e86ad319..3d236cd49 100644
--- a/Source/Editor/Managed/ManagedEditor.Internal.cpp
+++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp
@@ -484,6 +484,7 @@ public:
MaterialFunction = 8,
ParticleEmitterFunction = 9,
AnimationGraphFunction = 10,
+ Animation = 11,
};
static bool CreateAsset(NewAssetType type, MonoString* outputPathObj)
@@ -524,6 +525,9 @@ public:
case NewAssetType::AnimationGraphFunction:
tag = AssetsImportingManager::CreateAnimationGraphFunctionTag;
break;
+ case NewAssetType::Animation:
+ tag = AssetsImportingManager::CreateAnimationTag;
+ break;
default:
return true;
}
diff --git a/Source/Editor/Windows/Assets/AnimationWindow.cs b/Source/Editor/Windows/Assets/AnimationWindow.cs
index e87a8c8a9..37560c7e0 100644
--- a/Source/Editor/Windows/Assets/AnimationWindow.cs
+++ b/Source/Editor/Windows/Assets/AnimationWindow.cs
@@ -359,6 +359,7 @@ namespace FlaxEditor.Windows.Assets
if (_isWaitingForTimelineLoad && _asset.IsLoaded)
{
_isWaitingForTimelineLoad = false;
+ _timeline._id = _asset.ID;
_timeline.Load(_asset);
_undo.Clear();
_timeline.Enabled = true;
diff --git a/Source/Engine/Animations/AnimationData.h b/Source/Engine/Animations/AnimationData.h
index 28bd0229e..ad56e891a 100644
--- a/Source/Engine/Animations/AnimationData.h
+++ b/Source/Engine/Animations/AnimationData.h
@@ -82,7 +82,6 @@ public:
///
/// Gets the total amount of keyframes in the animation curves.
///
- /// The total keyframes count.
int32 GetKeyframesCount() const
{
return Position.GetKeyframes().Count() + Rotation.GetKeyframes().Count() + Scale.GetKeyframes().Count();
@@ -124,7 +123,6 @@ public:
///
/// Gets the length of the animation (in seconds).
///
- /// The length in seconds.
FORCE_INLINE float GetLength() const
{
#if BUILD_DEBUG
@@ -136,7 +134,6 @@ public:
///
/// Gets the total amount of keyframes in the all animation channels.
///
- /// The total keyframes count.
int32 GetKeyframesCount() const
{
int32 result = 0;
diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h
index 72b770664..74b512303 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.h
+++ b/Source/Engine/Animations/Graph/AnimGraph.h
@@ -872,9 +872,17 @@ private:
void ProcessGroupCustom(Box* boxBase, Node* nodeBase, Value& value);
void ProcessGroupFunction(Box* boxBase, Node* node, Value& value);
+ enum class ProcessAnimationMode
+ {
+ Override,
+ Add,
+ BlendAdditive,
+ };
+
int32 GetRootNodeIndex(Animation* anim);
void ExtractRootMotion(const Animation::NodeToChannel* mapping, int32 rootNodeIndex, Animation* anim, float pos, float prevPos, Transform& rootNode, RootMotionData& rootMotion);
- void ProcessAnimEvents(AnimGraphNode* node, bool loop, float length, float startTimePos, float oldTimePos, float animPos, float animPrevPos, Animation* anim, float speed);
+ void ProcessAnimEvents(AnimGraphNode* node, bool loop, float length, float animPos, float animPrevPos, Animation* anim, float speed);
+ void ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* node, bool loop, float length, float pos, float prevPos, Animation* anim, float speed, float weight = 1.0f, ProcessAnimationMode mode = ProcessAnimationMode::Override);
Variant SampleAnimation(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* anim, float speed);
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, float speedA, float speedB, float alpha);
Variant SampleAnimationsWithBlend(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* animA, Animation* animB, Animation* animC, float speedA, float speedB, float speedC, float alphaA, float alphaB, float alphaC);
diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
index 6c7d17c28..02287db35 100644
--- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
+++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
@@ -88,7 +88,7 @@ void AnimGraphExecutor::ExtractRootMotion(const Animation::NodeToChannel* mappin
rootNode = refPose;
}
-void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float length, float startTimePos, float oldTimePos, float animPos, float animPrevPos, Animation* anim, float speed)
+void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float length, float animPos, float animPrevPos, Animation* anim, float speed)
{
if (anim->Events.Count() == 0)
return;
@@ -96,17 +96,16 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
auto& context = Context.Get();
float eventTimeMin = animPrevPos;
float eventTimeMax = animPos;
- if (loop)
+ if (loop && context.DeltaTime * speed < 0)
{
- // Check if animation looped
- const float posNotLooped = startTimePos + oldTimePos;
- if (posNotLooped < 0.0f || posNotLooped > length)
+ // Check if animation looped (for anim events shooting during backwards playback)
+ //const float posNotLooped = startTimePos + oldTimePos;
+ //if (posNotLooped < 0.0f || posNotLooped > length)
+ //const int32 animPosCycle = Math::CeilToInt(animPos / anim->GetDuration());
+ //const int32 animPrevPosCycle = Math::CeilToInt(animPrevPos / anim->GetDuration());
+ //if (animPosCycle != animPrevPosCycle)
{
- if (context.DeltaTime * speed < 0)
- {
- // Playback backwards
- Swap(eventTimeMin, eventTimeMax);
- }
+ Swap(eventTimeMin, eventTimeMax);
}
}
const float eventTime = animPos / static_cast(anim->Data.FramesPerSecond);
@@ -218,49 +217,140 @@ float GetAnimSamplePos(float length, Animation* anim, float pos, float speed)
return animPos;
}
-Variant AnimGraphExecutor::SampleAnimation(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* anim, float speed)
+FORCE_INLINE void GetAnimSamplePos(bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, float& pos, float& prevPos)
{
- // Skip if animation is not ready to use
- if (anim == nullptr || !anim->IsLoaded())
- return Value::Null;
- PROFILE_CPU_ASSET(anim);
- const float oldTimePos = prevTimePos;
-
// Calculate actual time position within the animation node (defined by length and loop mode)
- const float pos = GetAnimPos(newTimePos, startTimePos, loop, length);
- const float prevPos = GetAnimPos(prevTimePos, startTimePos, loop, length);
+ pos = GetAnimPos(newTimePos, startTimePos, loop, length);
+ prevPos = GetAnimPos(prevTimePos, startTimePos, loop, length);
+}
+
+void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* node, bool loop, float length, float pos, float prevPos, Animation* anim, float speed, float weight, ProcessAnimationMode mode)
+{
+ PROFILE_CPU_ASSET(anim);
// Get animation position (animation track position for channels sampling)
const float animPos = GetAnimSamplePos(length, anim, pos, speed);
const float animPrevPos = GetAnimSamplePos(length, anim, prevPos, speed);
- // Sample the animation
- const auto nodes = node->GetNodes(this);
- nodes->RootMotion = RootMotionData::Identity;
- nodes->Position = pos;
- nodes->Length = length;
+ // Evaluate nested animations
+ if (anim->NestedAnims.Count() != 0)
+ {
+ for (auto& e : anim->NestedAnims)
+ {
+ const auto& nestedAnim = e.Second;
+ float nestedAnimPos = animPos - nestedAnim.Time;
+ if (nestedAnimPos >= 0.0f &&
+ nestedAnimPos < nestedAnim.Duration &&
+ nestedAnim.Enabled &&
+ nestedAnim.Anim &&
+ nestedAnim.Anim->IsLoaded())
+ {
+ // Get nested animation time position
+ float nestedAnimPrevPos = animPrevPos - nestedAnim.Time;
+ const float nestedAnimLength = nestedAnim.Anim->GetLength();
+ const float nestedAnimDuration = nestedAnim.Anim->GetDuration();
+ const float nestedAnimSpeed = nestedAnim.Speed * speed;
+ nestedAnimPos = nestedAnimPos / nestedAnimDuration * nestedAnimSpeed;
+ nestedAnimPrevPos = nestedAnimPrevPos / nestedAnimDuration * nestedAnimSpeed;
+ GetAnimSamplePos(nestedAnim.Loop, nestedAnimLength, nestedAnim.StartTime, nestedAnimPrevPos, nestedAnimPos, nestedAnimPos, nestedAnimPrevPos);
+
+ ProcessAnimation(nodes, node, true, nestedAnimLength, nestedAnimPos, nestedAnimPrevPos, nestedAnim.Anim, 1.0f, weight, mode);
+ }
+ }
+ }
+
+ // Evaluate nodes animations
const auto mapping = anim->GetMapping(_graph.BaseModel);
+ const bool weighted = weight < 1.0f;
const auto emptyNodes = GetEmptyNodes();
for (int32 i = 0; i < nodes->Nodes.Count(); i++)
{
const int32 nodeToChannel = mapping->At(i);
- nodes->Nodes[i] = emptyNodes->Nodes[i];
+ Transform& dstNode = nodes->Nodes[i];
+ Transform srcNode = emptyNodes->Nodes[i];
if (nodeToChannel != -1)
{
// Calculate the animated node transformation
- anim->Data.Channels[nodeToChannel].Evaluate(animPos, &nodes->Nodes[i], false);
+ anim->Data.Channels[nodeToChannel].Evaluate(animPos, &srcNode, false);
+ }
+
+ // Blend node
+ if (mode == ProcessAnimationMode::BlendAdditive)
+ {
+ dstNode.Translation += srcNode.Translation * weight;
+ dstNode.Scale += srcNode.Scale * weight;
+ BlendAdditiveWeightedRotation(dstNode.Orientation, srcNode.Orientation, weight);
+ }
+ else if (mode == ProcessAnimationMode::Add)
+ {
+ dstNode.Translation += srcNode.Translation * weight;
+ dstNode.Scale += srcNode.Scale * weight;
+ dstNode.Orientation += srcNode.Orientation * weight;
+ }
+ else if (weighted)
+ {
+ dstNode.Translation = srcNode.Translation * weight;
+ dstNode.Scale = srcNode.Scale * weight;
+ dstNode.Orientation = srcNode.Orientation * weight;
+ }
+ else
+ {
+ dstNode = srcNode;
}
}
// Handle root motion
if (_rootMotionMode != RootMotionMode::NoExtraction && anim->Data.EnableRootMotion)
{
+ // Calculate the root motion node transformation
const int32 rootNodeIndex = GetRootNodeIndex(anim);
- ExtractRootMotion(mapping, rootNodeIndex, anim, animPos, animPrevPos, nodes->Nodes[rootNodeIndex], nodes->RootMotion);
+ Transform rootNode = emptyNodes->Nodes[rootNodeIndex];
+ RootMotionData& dstNode = nodes->RootMotion;
+ RootMotionData srcNode(rootNode);
+ ExtractRootMotion(mapping, rootNodeIndex, anim, animPos, animPrevPos, rootNode, srcNode);
+
+ // Blend root motion
+ if (mode == ProcessAnimationMode::BlendAdditive)
+ {
+ dstNode.Translation += srcNode.Translation * weight;
+ BlendAdditiveWeightedRotation(dstNode.Rotation, srcNode.Rotation, weight);
+ }
+ else if (mode == ProcessAnimationMode::Add)
+ {
+ dstNode.Translation += srcNode.Translation * weight;
+ dstNode.Rotation += srcNode.Rotation * weight;
+ }
+ else if (weighted)
+ {
+ dstNode.Translation = srcNode.Translation * weight;
+ dstNode.Rotation = srcNode.Rotation * weight;
+ }
+ else
+ {
+ dstNode = srcNode;
+ }
}
// Collect events
- ProcessAnimEvents(node, loop, length, startTimePos, oldTimePos, animPos, animPrevPos, anim, speed);
+ if (weight > 0.5f)
+ {
+ ProcessAnimEvents(node, loop, length, animPos, animPrevPos, anim, speed);
+ }
+}
+
+Variant AnimGraphExecutor::SampleAnimation(AnimGraphNode* node, bool loop, float length, float startTimePos, float prevTimePos, float& newTimePos, Animation* anim, float speed)
+{
+ if (anim == nullptr || !anim->IsLoaded())
+ return Value::Null;
+
+ float pos, prevPos;
+ GetAnimSamplePos(loop, length, startTimePos, prevTimePos, newTimePos, pos, prevPos);
+
+ const auto nodes = node->GetNodes(this);
+ InitNodes(nodes);
+ nodes->Position = pos;
+ nodes->Length = length;
+ ProcessAnimation(nodes, node, loop, length, pos, prevPos, anim, speed);
return nodes;
}
@@ -271,73 +361,28 @@ Variant AnimGraphExecutor::SampleAnimationsWithBlend(AnimGraphNode* node, bool l
if (animA == nullptr || !animA->IsLoaded() ||
animB == nullptr || !animB->IsLoaded())
return Value::Null;
- const float oldTimePos = prevTimePos;
- // Calculate actual time position within the animation node (defined by length and loop mode)
- const float pos = GetAnimPos(newTimePos, startTimePos, loop, length);
- const float prevPos = GetAnimPos(prevTimePos, startTimePos, loop, length);
-
- // Get animation position (animation track position for channels sampling)
- const float animPosA = GetAnimSamplePos(length, animA, pos, speedA);
- const float animPrevPosA = GetAnimSamplePos(length, animA, prevPos, speedA);
- const float animPosB = GetAnimSamplePos(length, animB, pos, speedB);
- const float animPrevPosB = GetAnimSamplePos(length, animB, prevPos, speedB);
+ float pos, prevPos;
+ GetAnimSamplePos(loop, length, startTimePos, prevTimePos, newTimePos, pos, prevPos);
// Sample the animations with blending
const auto nodes = node->GetNodes(this);
- nodes->RootMotion = RootMotionData::Identity;
+ InitNodes(nodes);
nodes->Position = pos;
nodes->Length = length;
- const auto mappingA = animA->GetMapping(_graph.BaseModel);
- const auto mappingB = animB->GetMapping(_graph.BaseModel);
- const auto emptyNodes = GetEmptyNodes();
- RootMotionData rootMotionA, rootMotionB;
- int32 rootNodeIndexA = -1, rootNodeIndexB = -1;
- if (_rootMotionMode != RootMotionMode::NoExtraction)
- {
- rootMotionA = rootMotionB = RootMotionData::Identity;
- if (animA->Data.EnableRootMotion)
- rootNodeIndexA = GetRootNodeIndex(animA);
- if (animB->Data.EnableRootMotion)
- rootNodeIndexB = GetRootNodeIndex(animB);
- }
+ ProcessAnimation(nodes, node, loop, length, pos, prevPos, animA, speedA, 1.0f - alpha, ProcessAnimationMode::Override);
+ ProcessAnimation(nodes, node, loop, length, pos, prevPos, animB, speedB, alpha, ProcessAnimationMode::BlendAdditive);
+
+ // Normalize rotations
for (int32 i = 0; i < nodes->Nodes.Count(); i++)
{
- const int32 nodeToChannelA = mappingA->At(i);
- const int32 nodeToChannelB = mappingB->At(i);
- Transform nodeA = emptyNodes->Nodes[i];
- Transform nodeB = nodeA;
-
- // Calculate the animated node transformations
- if (nodeToChannelA != -1)
- {
- animA->Data.Channels[nodeToChannelA].Evaluate(animPosA, &nodeA, false);
- if (rootNodeIndexA == i)
- ExtractRootMotion(mappingA, rootNodeIndexA, animA, animPosA, animPrevPosA, nodeA, rootMotionA);
- }
- if (nodeToChannelB != -1)
- {
- animB->Data.Channels[nodeToChannelB].Evaluate(animPosB, &nodeB, false);
- if (rootNodeIndexB == i)
- ExtractRootMotion(mappingB, rootNodeIndexB, animB, animPosB, animPrevPosB, nodeB, rootMotionB);
- }
-
- // Blend
- Transform::Lerp(nodeA, nodeB, alpha, nodes->Nodes[i]);
+ nodes->Nodes[i].Orientation.Normalize();
}
-
- // Handle root motion
if (_rootMotionMode != RootMotionMode::NoExtraction)
{
- RootMotionData::Lerp(rootMotionA, rootMotionB, alpha, nodes->RootMotion);
+ nodes->RootMotion.Rotation.Normalize();
}
- // Collect events
- if (alpha > 0.5f)
- ProcessAnimEvents(node, loop, length, startTimePos, oldTimePos, animPosB, animPrevPosB, animB, speedB);
- else
- ProcessAnimEvents(node, loop, length, startTimePos, oldTimePos, animPosA, animPrevPosA, animA, speedA);
-
return nodes;
}
@@ -348,114 +393,30 @@ Variant AnimGraphExecutor::SampleAnimationsWithBlend(AnimGraphNode* node, bool l
animB == nullptr || !animB->IsLoaded() ||
animC == nullptr || !animC->IsLoaded())
return Value::Null;
- const float oldTimePos = prevTimePos;
- // Calculate actual time position within the animation node (defined by length and loop mode)
- const float pos = GetAnimPos(newTimePos, startTimePos, loop, length);
- const float prevPos = GetAnimPos(prevTimePos, startTimePos, loop, length);
-
- // Get animation position (animation track position for channels sampling)
- const float animPosA = GetAnimSamplePos(length, animA, pos, speedA);
- const float animPrevPosA = GetAnimSamplePos(length, animA, prevPos, speedA);
- const float animPosB = GetAnimSamplePos(length, animB, pos, speedB);
- const float animPrevPosB = GetAnimSamplePos(length, animB, prevPos, speedB);
- const float animPosC = GetAnimSamplePos(length, animC, pos, speedC);
- const float animPrevPosC = GetAnimSamplePos(length, animC, prevPos, speedC);
+ float pos, prevPos;
+ GetAnimSamplePos(loop, length, startTimePos, prevTimePos, newTimePos, pos, prevPos);
// Sample the animations with blending
const auto nodes = node->GetNodes(this);
- nodes->RootMotion = RootMotionData::Identity;
+ InitNodes(nodes);
nodes->Position = pos;
nodes->Length = length;
- const auto mappingA = animA->GetMapping(_graph.BaseModel);
- const auto mappingB = animB->GetMapping(_graph.BaseModel);
- const auto mappingC = animC->GetMapping(_graph.BaseModel);
- Transform tmp, t;
- const auto emptyNodes = GetEmptyNodes();
- RootMotionData rootMotionA, rootMotionB, rootMotionC;
- int32 rootNodeIndexA = -1, rootNodeIndexB = -1, rootNodeIndexC = -1;
- if (_rootMotionMode != RootMotionMode::NoExtraction)
- {
- rootMotionA = rootMotionB = rootMotionC = RootMotionData::Identity;
- if (animA->Data.EnableRootMotion)
- rootNodeIndexA = GetRootNodeIndex(animA);
- if (animB->Data.EnableRootMotion)
- rootNodeIndexB = GetRootNodeIndex(animB);
- if (animC->Data.EnableRootMotion)
- rootNodeIndexC = GetRootNodeIndex(animC);
- }
ASSERT(Math::Abs(alphaA + alphaB + alphaC - 1.0f) <= ANIM_GRAPH_BLEND_THRESHOLD); // Assumes weights are normalized
- for (int32 i = 0; i < nodes->Nodes.Count(); i++)
- {
- const int32 nodeToChannelA = mappingA->At(i);
- t = emptyNodes->Nodes[i];
- if (nodeToChannelA != -1)
- {
- // Override
- tmp = t;
- animA->Data.Channels[nodeToChannelA].Evaluate(animPosA, &tmp, false);
- if (rootNodeIndexA == i)
- ExtractRootMotion(mappingA, rootNodeIndexA, animA, animPosA, animPrevPosA, tmp, rootMotionA);
- t.Translation = tmp.Translation * alphaA;
- t.Orientation = tmp.Orientation * alphaA;
- t.Scale = tmp.Scale * alphaA;
- }
- nodes->Nodes[i] = t;
- }
- for (int32 i = 0; i < nodes->Nodes.Count(); i++)
- {
- const int32 nodeToChannelB = mappingB->At(i);
- const int32 nodeToChannelC = mappingC->At(i);
- t = nodes->Nodes[i];
- if (nodeToChannelB != -1)
- {
- // Additive
- tmp = emptyNodes->Nodes[i];
- animB->Data.Channels[nodeToChannelB].Evaluate(animPosB, &tmp, false);
- if (rootNodeIndexB == i)
- ExtractRootMotion(mappingB, rootNodeIndexB, animB, animPosB, animPrevPosB, tmp, rootMotionB);
- t.Translation += tmp.Translation * alphaB;
- t.Scale += tmp.Scale * alphaB;
- BlendAdditiveWeightedRotation(t.Orientation, tmp.Orientation, alphaB);
- }
- if (nodeToChannelC != -1)
- {
- // Additive
- tmp = emptyNodes->Nodes[i];
- animC->Data.Channels[nodeToChannelC].Evaluate(animPosC, &tmp, false);
- if (rootNodeIndexC == i)
- ExtractRootMotion(mappingC, rootNodeIndexC, animC, animPosC, animPrevPosC, tmp, rootMotionC);
- t.Translation += tmp.Translation * alphaC;
- t.Scale += tmp.Scale * alphaC;
- BlendAdditiveWeightedRotation(t.Orientation, tmp.Orientation, alphaC);
- }
- t.Orientation.Normalize();
- nodes->Nodes[i] = t;
- }
+ ProcessAnimation(nodes, node, loop, length, pos, prevPos, animA, speedA, alphaA, ProcessAnimationMode::Override);
+ ProcessAnimation(nodes, node, loop, length, pos, prevPos, animB, speedB, alphaB, ProcessAnimationMode::BlendAdditive);
+ ProcessAnimation(nodes, node, loop, length, pos, prevPos, animC, speedC, alphaC, ProcessAnimationMode::BlendAdditive);
- // Handle root motion
+ // Normalize rotations
+ for (int32 i = 0; i < nodes->Nodes.Count(); i++)
+ {
+ nodes->Nodes[i].Orientation.Normalize();
+ }
if (_rootMotionMode != RootMotionMode::NoExtraction)
{
- nodes->RootMotion.Translation = rootMotionA.Translation * alphaA;
- nodes->RootMotion.Rotation = rootMotionA.Rotation * alphaA;
-
- nodes->RootMotion.Translation += rootMotionB.Translation * alphaB;
- nodes->RootMotion.Rotation += rootMotionB.Rotation * alphaB;
- BlendAdditiveWeightedRotation(nodes->RootMotion.Rotation, rootMotionB.Rotation, alphaB);
-
- nodes->RootMotion.Translation = rootMotionC.Translation * alphaC;
- nodes->RootMotion.Rotation = rootMotionC.Rotation * alphaC;
- BlendAdditiveWeightedRotation(nodes->RootMotion.Rotation, rootMotionB.Rotation, alphaC);
+ nodes->RootMotion.Rotation.Normalize();
}
- // Collect events
- if (alphaC > 0.5f)
- ProcessAnimEvents(node, loop, length, startTimePos, oldTimePos, animPosC, animPrevPosC, animC, speedC);
- else if (alphaB > 0.5f)
- ProcessAnimEvents(node, loop, length, startTimePos, oldTimePos, animPosB, animPrevPosB, animB, speedB);
- else
- ProcessAnimEvents(node, loop, length, startTimePos, oldTimePos, animPosA, animPrevPosA, animA, speedA);
-
return nodes;
}
@@ -693,6 +654,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Animation
case 0:
{
+ ANIM_GRAPH_PROFILE_EVENT("Animation");
const float length = anim ? anim->GetLength() : 0.0f;
// Calculate new time position
diff --git a/Source/Engine/Content/Assets/Animation.cpp b/Source/Engine/Content/Assets/Animation.cpp
index ca79a3ee6..e6d39e684 100644
--- a/Source/Engine/Content/Assets/Animation.cpp
+++ b/Source/Engine/Content/Assets/Animation.cpp
@@ -7,6 +7,7 @@
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Animations/CurveSerialization.h"
#include "Engine/Animations/AnimEvent.h"
+#include "Engine/Animations/SceneAnimations/SceneAnimation.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Serialization/MemoryReadStream.h"
@@ -64,7 +65,11 @@ Animation::InfoData Animation::GetInfo() const
info.ChannelsCount = 0;
info.KeyframesCount = 0;
}
+ info.MemoryUsage += Events.Capacity() * sizeof(Pair>);
+ info.MemoryUsage += NestedAnims.Capacity() * sizeof(Pair);
info.MemoryUsage += MappingCache.Capacity() * (sizeof(void*) + sizeof(NodeToChannel) + 1);
+ for (auto& e : Events)
+ info.MemoryUsage += e.Second.GetKeyframes().Capacity() * sizeof(StepCurve);
for (auto& e : MappingCache)
info.MemoryUsage += e.Value.Capacity() * sizeof(int32);
return info;
@@ -145,7 +150,7 @@ void Animation::LoadTimeline(BytesContainer& result) const
const float fpsInv = 1.0f / fps;
stream.WriteFloat(fps);
stream.WriteInt32((int32)Data.Duration);
- int32 tracksCount = Data.Channels.Count() + Events.Count();
+ int32 tracksCount = Data.Channels.Count() + NestedAnims.Count() + Events.Count();
for (auto& channel : Data.Channels)
tracksCount +=
(channel.Position.GetKeyframes().HasItems() ? 1 : 0) +
@@ -232,6 +237,29 @@ void Animation::LoadTimeline(BytesContainer& result) const
trackIndex++;
}
}
+ for (auto& e : NestedAnims)
+ {
+ auto& nestedAnim = e.Second;
+ byte flags = 0;
+ if (!nestedAnim.Enabled)
+ flags |= (byte)SceneAnimation::Track::Flags::Mute;
+ if (nestedAnim.Loop)
+ flags |= (byte)SceneAnimation::Track::Flags::Loop;
+ Guid id = nestedAnim.Anim.GetID();
+
+ // Nested Animation track
+ stream.WriteByte(20); // Track Type
+ stream.WriteByte(flags); // Track Flags
+ stream.WriteInt32(-1); // Parent Index
+ stream.WriteInt32(0); // Children Count
+ stream.WriteString(e.First, -13); // Name
+ stream.Write(&Color32::White); // Color
+ stream.Write(&id);
+ stream.WriteFloat(nestedAnim.Time);
+ stream.WriteFloat(nestedAnim.Duration);
+ stream.WriteFloat(nestedAnim.Speed);
+ stream.WriteFloat(nestedAnim.StartTime);
+ }
for (auto& e : Events)
{
// Animation Event track
@@ -290,6 +318,7 @@ bool Animation::SaveTimeline(BytesContainer& data)
// Tracks
Data.Channels.Clear();
Events.Clear();
+ NestedAnims.Clear();
Dictionary animationChannelTrackIndexToChannelIndex;
animationChannelTrackIndexToChannelIndex.EnsureCapacity(tracksCount * 3);
for (int32 trackIndex = 0; trackIndex < tracksCount; trackIndex++)
@@ -392,6 +421,23 @@ bool Animation::SaveTimeline(BytesContainer& data)
}
break;
}
+ case 20:
+ {
+ // Nested Animation
+ auto& nestedTrack = NestedAnims.AddOne();
+ nestedTrack.First = name;
+ auto& nestedAnim = nestedTrack.Second;
+ Guid id;
+ stream.Read(&id);
+ stream.ReadFloat(&nestedAnim.Time);
+ stream.ReadFloat(&nestedAnim.Duration);
+ stream.ReadFloat(&nestedAnim.Speed);
+ stream.ReadFloat(&nestedAnim.StartTime);
+ nestedAnim.Anim = id;
+ nestedAnim.Enabled = (trackFlags & (byte)SceneAnimation::Track::Flags::Mute) == 0;
+ nestedAnim.Loop = (trackFlags & (byte)SceneAnimation::Track::Flags::Loop) != 0;
+ break;
+ }
default:
LOG(Error, "Unsupported track type {0} for animation.", trackType);
return true;
@@ -431,7 +477,7 @@ bool Animation::Save(const StringView& path)
MemoryWriteStream stream(4096);
// Info
- stream.WriteInt32(101);
+ stream.WriteInt32(102);
stream.WriteDouble(Data.Duration);
stream.WriteDouble(Data.FramesPerSecond);
stream.WriteBool(Data.EnableRootMotion);
@@ -464,6 +510,27 @@ bool Animation::Save(const StringView& path)
}
}
+ // Nested animations
+ stream.WriteInt32(NestedAnims.Count());
+ for (int32 i = 0; i < NestedAnims.Count(); i++)
+ {
+ auto& e = NestedAnims[i];
+ stream.WriteString(e.First, 172);
+ auto& nestedAnim = e.Second;
+ Guid id = nestedAnim.Anim.GetID();
+ byte flags = 0;
+ if (nestedAnim.Enabled)
+ flags |= 1;
+ if (nestedAnim.Loop)
+ flags |= 2;
+ stream.WriteByte(flags);
+ stream.Write(&id);
+ stream.WriteFloat(nestedAnim.Time);
+ stream.WriteFloat(nestedAnim.Duration);
+ stream.WriteFloat(nestedAnim.Speed);
+ stream.WriteFloat(nestedAnim.StartTime);
+ }
+
// Set data to the chunk asset
auto chunk0 = GetOrCreateChunk(0);
ASSERT(chunk0 != nullptr);
@@ -534,6 +601,7 @@ Asset::LoadResult Animation::load()
{
case 100:
case 101:
+ case 102:
{
stream.ReadInt32(&headerVersion);
stream.ReadDouble(&Data.Duration);
@@ -616,6 +684,31 @@ Asset::LoadResult Animation::load()
}
}
+ // Nested animations
+ if (headerVersion >= 102)
+ {
+ int32 nestedAnimationsCount;
+ stream.ReadInt32(&nestedAnimationsCount);
+ NestedAnims.Resize(nestedAnimationsCount, false);
+ for (int32 i = 0; i < nestedAnimationsCount; i++)
+ {
+ auto& e = NestedAnims[i];
+ stream.ReadString(&e.First, 172);
+ auto& nestedAnim = e.Second;
+ byte flags;
+ stream.ReadByte(&flags);
+ nestedAnim.Enabled = flags & 1;
+ nestedAnim.Loop = flags & 2;
+ Guid id;
+ stream.Read(&id);
+ nestedAnim.Anim = id;
+ stream.ReadFloat(&nestedAnim.Time);
+ stream.ReadFloat(&nestedAnim.Duration);
+ stream.ReadFloat(&nestedAnim.Speed);
+ stream.ReadFloat(&nestedAnim.StartTime);
+ }
+ }
+
return LoadResult::Ok;
}
@@ -639,6 +732,7 @@ void Animation::unload(bool isReloading)
}
}
Events.Clear();
+ NestedAnims.Clear();
}
AssetChunksFlag Animation::getChunksToPreload() const
diff --git a/Source/Engine/Content/Assets/Animation.h b/Source/Engine/Content/Assets/Animation.h
index 3d30a7a7f..826f60c1d 100644
--- a/Source/Engine/Content/Assets/Animation.h
+++ b/Source/Engine/Content/Assets/Animation.h
@@ -5,6 +5,7 @@
#include "../BinaryAsset.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Animations/AnimationData.h"
+#include "Engine/Content/AssetReference.h"
class SkinnedModel;
class AnimEvent;
@@ -61,6 +62,20 @@ API_CLASS(NoSpawn) class FLAXENGINE_API Animation : public BinaryAsset
#endif
};
+ ///
+ /// Contains instance.
+ ///
+ struct FLAXENGINE_API NestedAnimData
+ {
+ float Time = 0.0f;
+ float Duration = 0.0f;
+ float Speed = 1.0f;
+ float StartTime = 0.0f;
+ bool Enabled = false;
+ bool Loop = false;
+ AssetReference Anim;
+ };
+
private:
#if USE_EDITOR
bool _registeredForScriptingReload = false;
@@ -78,6 +93,11 @@ public:
///
Array>> Events;
+ ///
+ /// The nested animations (animation per named track).
+ ///
+ Array> NestedAnims;
+
///
/// Contains the mapping for every skeleton node to the animation data channels.
/// Can be used for a simple lookup or to check if a given node is animated (unused nodes are using -1 index).
diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.cpp b/Source/Engine/ContentImporters/AssetsImportingManager.cpp
index 4d8d49c87..9c654aada 100644
--- a/Source/Engine/ContentImporters/AssetsImportingManager.cpp
+++ b/Source/Engine/ContentImporters/AssetsImportingManager.cpp
@@ -31,6 +31,7 @@
#include "CreateParticleEmitterFunction.h"
#include "CreateAnimationGraphFunction.h"
#include "CreateVisualScript.h"
+#include "CreateAnimation.h"
#include "CreateJson.h"
// Tags used to detect asset creation mode
@@ -51,6 +52,7 @@ const String AssetsImportingManager::CreateSceneAnimationTag(TEXT("SceneAnimatio
const String AssetsImportingManager::CreateMaterialFunctionTag(TEXT("MaterialFunction"));
const String AssetsImportingManager::CreateParticleEmitterFunctionTag(TEXT("ParticleEmitterFunction"));
const String AssetsImportingManager::CreateAnimationGraphFunctionTag(TEXT("AnimationGraphFunction"));
+const String AssetsImportingManager::CreateAnimationTag(TEXT("Animation"));
const String AssetsImportingManager::CreateVisualScriptTag(TEXT("VisualScript"));
class AssetsImportingManagerService : public EngineService
@@ -467,6 +469,7 @@ bool AssetsImportingManagerService::Init()
{ AssetsImportingManager::CreateMaterialFunctionTag, CreateMaterialFunction::Create },
{ AssetsImportingManager::CreateParticleEmitterFunctionTag, CreateParticleEmitterFunction::Create },
{ AssetsImportingManager::CreateAnimationGraphFunctionTag, CreateAnimationGraphFunction::Create },
+ { AssetsImportingManager::CreateAnimationTag, CreateAnimation::Create },
{ AssetsImportingManager::CreateVisualScriptTag, CreateVisualScript::Create },
};
AssetsImportingManager::Creators.Add(InBuildCreators, ARRAY_COUNT(InBuildCreators));
diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.h b/Source/Engine/ContentImporters/AssetsImportingManager.h
index 67cafec17..0ec23d6bb 100644
--- a/Source/Engine/ContentImporters/AssetsImportingManager.h
+++ b/Source/Engine/ContentImporters/AssetsImportingManager.h
@@ -108,6 +108,11 @@ public:
///
static const String CreateAnimationGraphFunctionTag;
+ ///
+ /// The create animation asset tag.
+ ///
+ static const String CreateAnimationTag;
+
///
/// The create visual script asset tag.
///
diff --git a/Source/Engine/ContentImporters/CreateAnimation.h b/Source/Engine/ContentImporters/CreateAnimation.h
new file mode 100644
index 000000000..1f708d9b6
--- /dev/null
+++ b/Source/Engine/ContentImporters/CreateAnimation.h
@@ -0,0 +1,52 @@
+// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#include "Types.h"
+
+#if COMPILE_WITH_ASSETS_IMPORTER
+
+#include "Engine/Content/Assets/Animation.h"
+#include "Engine/Serialization/MemoryWriteStream.h"
+
+///
+/// Creating animation utility
+///
+class CreateAnimation
+{
+public:
+ static CreateAssetResult Create(CreateAssetContext& context)
+ {
+ // Base
+ IMPORT_SETUP(Animation, 1);
+
+ // Serialize empty animation data to the stream
+ MemoryWriteStream stream(256);
+ {
+ // Info
+ stream.WriteInt32(102);
+ stream.WriteDouble(5 * 60.0);
+ stream.WriteDouble(60.0);
+ stream.WriteBool(false);
+ stream.WriteString(StringView::Empty, 13);
+
+ // Animation channels
+ stream.WriteInt32(0);
+
+ // Animation events
+ stream.WriteInt32(0);
+
+ // Nested animations
+ stream.WriteInt32(0);
+ }
+
+ // Copy to asset chunk
+ if (context.AllocateChunk(0))
+ return CreateAssetResult::CannotAllocateChunk;
+ context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition());
+
+ return CreateAssetResult::Ok;
+ }
+};
+
+#endif