From 5b50562a9f37c3a31da4d9bf507ccfd857418a74 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 26 Apr 2023 14:27:01 +0200 Subject: [PATCH] Refactor Skeleton Mapping to be handled by Skinned Model instead of Animation asset --- Source/Engine/Animations/Graph/AnimGraph.h | 2 +- .../Animations/Graph/AnimGroup.Animation.cpp | 10 +- Source/Engine/Content/Assets/Animation.cpp | 85 ----------------- Source/Engine/Content/Assets/Animation.h | 26 ------ Source/Engine/Content/Assets/SkinnedModel.cpp | 91 +++++++++++++++++++ Source/Engine/Content/Assets/SkinnedModel.h | 18 ++-- Source/Engine/Graphics/Models/SkeletonData.h | 11 ++- 7 files changed, 117 insertions(+), 126 deletions(-) diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h index ba303ab96..9e4ae86f7 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.h +++ b/Source/Engine/Animations/Graph/AnimGraph.h @@ -885,7 +885,7 @@ private: }; int32 GetRootNodeIndex(Animation* anim); - void ExtractRootMotion(const Animation::NodeToChannel* mapping, int32 rootNodeIndex, Animation* anim, float pos, float prevPos, Transform& rootNode, RootMotionData& rootMotion); + void ExtractRootMotion(Span mapping, int32 rootNodeIndex, Animation* anim, float pos, float prevPos, Transform& rootNode, RootMotionData& rootMotion); 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); diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index ed75c2479..bd6c74630 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -40,10 +40,10 @@ int32 AnimGraphExecutor::GetRootNodeIndex(Animation* anim) return rootNodeIndex; } -void AnimGraphExecutor::ExtractRootMotion(const Animation::NodeToChannel* mapping, int32 rootNodeIndex, Animation* anim, float pos, float prevPos, Transform& rootNode, RootMotionData& rootMotion) +void AnimGraphExecutor::ExtractRootMotion(const Span mapping, int32 rootNodeIndex, Animation* anim, float pos, float prevPos, Transform& rootNode, RootMotionData& rootMotion) { const Transform& refPose = GetEmptyNodes()->Nodes[rootNodeIndex]; - const int32 nodeToChannel = mapping->At(rootNodeIndex); + const int32 nodeToChannel = mapping[rootNodeIndex]; if (_rootMotionMode == RootMotionMode::Enable && nodeToChannel != -1) { // Get the root bone transformation @@ -258,12 +258,14 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* } // Evaluate nodes animations - const auto mapping = anim->GetMapping(_graph.BaseModel); + const Span mapping = _graph.BaseModel->GetSkeletonMapping(anim); + if (mapping.IsInvalid()) + return; 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); + const int32 nodeToChannel = mapping[i]; Transform& dstNode = nodes->Nodes[i]; Transform srcNode = emptyNodes->Nodes[i]; if (nodeToChannel != -1) diff --git a/Source/Engine/Content/Assets/Animation.cpp b/Source/Engine/Content/Assets/Animation.cpp index e9ce2a1cb..8aa635244 100644 --- a/Source/Engine/Content/Assets/Animation.cpp +++ b/Source/Engine/Content/Assets/Animation.cpp @@ -3,7 +3,6 @@ #include "Animation.h" #include "SkinnedModel.h" #include "Engine/Core/Log.h" -#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Content/Factories/BinaryAssetFactory.h" #include "Engine/Animations/CurveSerialization.h" #include "Engine/Animations/AnimEvent.h" @@ -30,9 +29,7 @@ void Animation::OnScriptsReloadStart() for (auto& e : Events) { for (auto& k : e.Second.GetKeyframes()) - { Level::ScriptsReloadRegisterObject((ScriptingObject*&)k.Value.Instance); - } } } @@ -67,72 +64,11 @@ Animation::InfoData Animation::GetInfo() const } 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; } -void Animation::ClearCache() -{ - ScopeLock lock(Locker); - - // Unlink events - for (auto i = MappingCache.Begin(); i.IsNotEnd(); ++i) - { - i->Key->OnUnloaded.Unbind(this); - i->Key->OnReloading.Unbind(this); - } - - // Free memory - MappingCache.Clear(); - MappingCache.SetCapacity(0); -} - -const Animation::NodeToChannel* Animation::GetMapping(SkinnedModel* obj) -{ - ASSERT(obj && obj->IsLoaded() && IsLoaded()); - - ScopeLock lock(Locker); - - // Try quick lookup - NodeToChannel* result = MappingCache.TryGet(obj); - if (result == nullptr) - { - PROFILE_CPU(); - - // Add to cache - NodeToChannel tmp; - auto bucket = MappingCache.Add(obj, tmp); - result = &bucket->Value; - obj->OnUnloaded.Bind(this); - obj->OnReloading.Bind(this); - - // Initialize the mapping - const auto& skeleton = obj->Skeleton; - const int32 nodesCount = skeleton.Nodes.Count(); - result->Resize(nodesCount, false); - result->SetAll(-1); - for (int32 i = 0; i < Data.Channels.Count(); i++) - { - auto& nodeAnim = Data.Channels[i]; - - for (int32 j = 0; j < nodesCount; j++) - { - if (skeleton.Nodes[j].Name == nodeAnim.NodeName) - { - result->At(j) = i; - break; - } - } - } - } - - return result; -} - #if USE_EDITOR void Animation::LoadTimeline(BytesContainer& result) const @@ -552,23 +488,6 @@ bool Animation::Save(const StringView& path) #endif -void Animation::OnSkinnedModelUnloaded(Asset* obj) -{ - ScopeLock lock(Locker); - - const auto key = static_cast(obj); - auto i = MappingCache.Find(key); - ASSERT(i != MappingCache.End()); - - // Unlink event - key->OnUnloaded.Unbind(this); - key->OnReloading.Unbind(this); - - // Clear cache - i->Value.Resize(0, false); - MappingCache.Remove(i); -} - uint64 Animation::GetMemoryUsage() const { Locker.Lock(); @@ -579,9 +498,6 @@ uint64 Animation::GetMemoryUsage() const for (const auto& e : Events) result += e.First.Length() * sizeof(Char) + e.Second.GetMemoryUsage(); result += NestedAnims.Capacity() * sizeof(Pair); - result += MappingCache.Capacity() * sizeof(Pair>); - for (const auto& e : MappingCache) - result += e.Value.Capacity() * sizeof(int32); Locker.Unlock(); return result; } @@ -738,7 +654,6 @@ void Animation::unload(bool isReloading) Level::ScriptsReloadStart.Unbind(this); } #endif - ClearCache(); Data.Dispose(); for (const auto& e : Events) { diff --git a/Source/Engine/Content/Assets/Animation.h b/Source/Engine/Content/Assets/Animation.h index 6d89039fe..5a96e5bc0 100644 --- a/Source/Engine/Content/Assets/Animation.h +++ b/Source/Engine/Content/Assets/Animation.h @@ -98,17 +98,6 @@ public: /// 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). - /// - typedef Array NodeToChannel; - - /// - /// The skeleton nodes to animation channel indices mapping cache. Use it as read-only. It's being maintained internally by the asset. - /// - Dictionary MappingCache; - public: /// /// Gets the length of the animation (in seconds). @@ -139,18 +128,6 @@ public: /// API_PROPERTY() InfoData GetInfo() const; - /// - /// Clears the skeleton mapping cache. - /// - void ClearCache(); - - /// - /// Clears the skeleton mapping cache. - /// - /// The target skinned model to get mapping to its skeleton. - /// The cached node-to-channel mapping for the fast animation sampling for the skinned model skeleton nodes. - const NodeToChannel* GetMapping(SkinnedModel* obj); - #if USE_EDITOR /// /// Gets the animation as serialized timeline data. Used to show it in Editor. @@ -174,9 +151,6 @@ public: bool Save(const StringView& path = StringView::Empty); #endif -private: - void OnSkinnedModelUnloaded(Asset* obj); - public: // [BinaryAsset] uint64 GetMemoryUsage() const override; diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index 603f685d8..63d0a29b8 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -1,6 +1,7 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #include "SkinnedModel.h" +#include "Animation.h" #include "Engine/Core/Log.h" #include "Engine/Engine/Engine.h" #include "Engine/Serialization/MemoryReadStream.h" @@ -15,6 +16,7 @@ #include "Engine/Content/Factories/BinaryAssetFactory.h" #include "Engine/Content/Upgraders/SkinnedModelAssetUpgrader.h" #include "Engine/Debug/Exceptions/ArgumentOutOfRangeException.h" +#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Renderer/DrawCall.h" #define CHECK_INVALID_BUFFER(model, buffer) \ @@ -116,6 +118,7 @@ SkinnedModel::SkinnedModel(const SpawnParams& params, const AssetInfo* info) SkinnedModel::~SkinnedModel() { ASSERT(_streamingTask == nullptr); + ASSERT(_skeletonMappingCache.Count() == 0); } bool SkinnedModel::HasAnyLODInitialized() const @@ -152,6 +155,55 @@ void SkinnedModel::GetLODData(int32 lodIndex, BytesContainer& data) const GetChunkData(chunkIndex, data); } +Span SkinnedModel::GetSkeletonMapping(Asset* source) +{ + if (WaitForLoaded() || !source || source->WaitForLoaded()) + return Span(); + ScopeLock lock(Locker); + Span result; + if (!_skeletonMappingCache.TryGet(source, result)) + { + PROFILE_CPU(); + + // Initialize the mapping + const int32 nodesCount = Skeleton.Nodes.Count(); + result = Span((int32*)Allocator::Allocate(nodesCount * sizeof(int32)), nodesCount); + for (int32 i = 0; i < nodesCount; i++) + result[i] = -1; + if (const auto* sourceAnim = Cast(source)) + { + // Map animation channels to the skeleton nodes (by name) + const auto& channels = sourceAnim->Data.Channels; + for (int32 i = 0; i < channels.Count(); i++) + { + auto& nodeAnim = channels[i]; + for (int32 j = 0; j < nodesCount; j++) + { + if (StringUtils::CompareIgnoreCase(Skeleton.Nodes[j].Name.GetText(), nodeAnim.NodeName.GetText()) == 0) + { + result[j] = i; + break; + } + } + } + } + else + { +#if !BUILD_RELEASE + LOG(Error, "Invalid asset type {0} to use for skeleton mapping", source->GetTypeName()); +#endif + } + + // Add to cache + _skeletonMappingCache.Add(source, result); + source->OnUnloaded.Bind(this); +#if USE_EDITOR + source->OnReloading.Bind(this); +#endif + } + return result; +} + bool SkinnedModel::Intersects(const Ray& ray, const Matrix& world, Real& distance, Vector3& normal, SkinnedMesh** mesh, int32 lodIndex) { return LODs[lodIndex].Intersects(ray, world, distance, normal, mesh); @@ -720,6 +772,36 @@ bool SkinnedModel::Init(const Span& meshesCountPerLod) return false; } +void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj) +{ + ScopeLock lock(Locker); + auto i = _skeletonMappingCache.Find(obj); + ASSERT(i != _skeletonMappingCache.End()); + + // Unlink event + obj->OnUnloaded.Unbind(this); +#if USE_EDITOR + obj->OnReloading.Unbind(this); +#endif + + // Clear cache + Allocator::Free(i->Value.Get()); + _skeletonMappingCache.Remove(i); +} + +uint64 SkinnedModel::GetMemoryUsage() const +{ + Locker.Lock(); + uint64 result = BinaryAsset::GetMemoryUsage(); + result += sizeof(SkinnedModel) - sizeof(BinaryAsset); + result += Skeleton.GetMemoryUsage(); + result += _skeletonMappingCache.Capacity() * sizeof(Dictionary>::Bucket); + for (const auto& e : _skeletonMappingCache) + result += e.Value.Length(); + Locker.Unlock(); + return result; +} + void SkinnedModel::SetupMaterialSlots(int32 slotsCount) { ModelBase::SetupMaterialSlots(slotsCount); @@ -1024,6 +1106,15 @@ void SkinnedModel::unload(bool isReloading) LODs.Clear(); Skeleton.Dispose(); _loadedLODs = 0; + for (auto& e : _skeletonMappingCache) + { + e.Key->OnUnloaded.Unbind(this); +#if USE_EDITOR + e.Key->OnReloading.Unbind(this); +#endif + Allocator::Free(e.Value.Get()); + } + _skeletonMappingCache.Clear(); } bool SkinnedModel::init(AssetInitData& initData) diff --git a/Source/Engine/Content/Assets/SkinnedModel.h b/Source/Engine/Content/Assets/SkinnedModel.h index 02afcacba..ea250244e 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.h +++ b/Source/Engine/Content/Assets/SkinnedModel.h @@ -20,6 +20,7 @@ API_CLASS(NoSpawn) class FLAXENGINE_API SkinnedModel : public ModelBase private: int32 _loadedLODs = 0; StreamSkinnedModelLODTask* _streamingTask = nullptr; + Dictionary> _skeletonMappingCache; public: /// @@ -76,13 +77,11 @@ public: /// /// Determines whether any LOD has been initialized. /// - /// True if any LOD has been initialized, otherwise false. bool HasAnyLODInitialized() const; /// /// Determines whether this model can be rendered. /// - /// True if can render that model, otherwise false. FORCE_INLINE bool CanBeRendered() const { return _loadedLODs > 0; @@ -154,7 +153,13 @@ public: /// The data (may be missing if failed to get it). void GetLODData(int32 lodIndex, BytesContainer& data) const; -public: + /// + /// Gets the skeleton mapping for a given asset (animation or other skinned model). Uses identity mapping or manually created retargeting setup. + /// + /// The source asset (animation or other skinned model) to get mapping to its skeleton. + /// The cached node-to-node mapping for the fast animation sampling for the skinned model skeleton nodes. Each span item is index of the source skeleton node for this skeleton. + Span GetSkeletonMapping(Asset* source); + /// /// Determines if there is an intersection between the SkinnedModel and a Ray in given world using given instance. /// @@ -194,7 +199,6 @@ public: /// The bounding box. API_FUNCTION() BoundingBox GetBox(int32 lodIndex = 0) const; -public: /// /// Draws the meshes. Binds vertex and index buffers and invokes the draw calls. /// @@ -219,7 +223,6 @@ public: /// The packed drawing info data. void Draw(const RenderContextBatch& renderContextBatch, const SkinnedMesh::DrawInfo& info); -public: /// /// Setups the model LODs collection including meshes creation. /// @@ -244,7 +247,6 @@ public: API_FUNCTION() bool SetupSkeleton(const Array& nodes, const Array& bones, bool autoCalculateOffsetMatrix); #if USE_EDITOR - /// /// Saves this asset to the file. Supported only in Editor. /// @@ -253,7 +255,6 @@ public: /// The custom asset path to use for the saving. Use empty value to save this asset to its own storage location. Can be used to duplicate asset. Must be specified when saving virtual asset. /// True if cannot save data, otherwise false. API_FUNCTION() bool Save(bool withMeshDataFromGpu = false, const StringView& path = StringView::Empty); - #endif private: @@ -264,8 +265,11 @@ private: /// True if failed, otherwise false. bool Init(const Span& meshesCountPerLod); + void OnSkeletonMappingSourceAssetUnloaded(Asset* obj); + public: // [ModelBase] + uint64 GetMemoryUsage() const override; void SetupMaterialSlots(int32 slotsCount) override; int32 GetLODsCount() const override; void GetMeshes(Array& meshes, int32 lodIndex = 0) override; diff --git a/Source/Engine/Graphics/Models/SkeletonData.h b/Source/Engine/Graphics/Models/SkeletonData.h index 035f8c10a..1c9d212a9 100644 --- a/Source/Engine/Graphics/Models/SkeletonData.h +++ b/Source/Engine/Graphics/Models/SkeletonData.h @@ -88,7 +88,6 @@ public: /// /// Gets the root node reference. /// - /// The root node. FORCE_INLINE SkeletonNode& RootNode() { ASSERT(Nodes.HasItems()); @@ -98,7 +97,6 @@ public: /// /// Gets the root node reference. /// - /// The root node. FORCE_INLINE const SkeletonNode& RootNode() const { ASSERT(Nodes.HasItems()); @@ -108,7 +106,6 @@ public: /// /// Swaps the contents of object with the other object without copy operation. Performs fast internal data exchange. /// - /// The other object. void Swap(SkeletonData& other) { Nodes.Swap(other.Nodes); @@ -135,6 +132,14 @@ public: return -1; } + 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; + } + /// /// Releases data. ///