From 3b52914416279061816a2deef5181d0240f62791 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 31 Jul 2022 15:53:09 +0200 Subject: [PATCH] Add **Nested Animations** for compositing animation clips --- Source/Editor/Content/Proxy/AnimationProxy.cs | 13 + Source/Editor/Editor.cs | 5 + .../Editor/GUI/Timeline/AnimationTimeline.cs | 47 +++ .../Timeline/Tracks/NestedAnimationTrack.cs | 183 ++++++++++ .../GUI/Timeline/Undo/EditTrackAction.cs | 7 +- .../Editor/Managed/ManagedEditor.Internal.cpp | 4 + .../Editor/Windows/Assets/AnimationWindow.cs | 1 + Source/Engine/Animations/AnimationData.h | 3 - Source/Engine/Animations/Graph/AnimGraph.h | 10 +- .../Animations/Graph/AnimGroup.Animation.cpp | 318 ++++++++---------- Source/Engine/Content/Assets/Animation.cpp | 98 +++++- Source/Engine/Content/Assets/Animation.h | 20 ++ .../AssetsImportingManager.cpp | 3 + .../ContentImporters/AssetsImportingManager.h | 5 + .../Engine/ContentImporters/CreateAnimation.h | 52 +++ 15 files changed, 583 insertions(+), 186 deletions(-) create mode 100644 Source/Editor/GUI/Timeline/Tracks/NestedAnimationTrack.cs create mode 100644 Source/Engine/ContentImporters/CreateAnimation.h 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