Refactor Skeleton Mapping to be handled by Skinned Model instead of Animation asset

This commit is contained in:
Wojtek Figat
2023-04-26 14:27:01 +02:00
parent 39b89ada6c
commit 5b50562a9f
7 changed files with 117 additions and 126 deletions

View File

@@ -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<String, StepCurve<AnimEventData>>);
info.MemoryUsage += NestedAnims.Capacity() * sizeof(Pair<String, NestedAnimData>);
info.MemoryUsage += MappingCache.Capacity() * (sizeof(void*) + sizeof(NodeToChannel) + 1);
for (auto& e : Events)
info.MemoryUsage += e.Second.GetKeyframes().Capacity() * sizeof(StepCurve<AnimEventData>);
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<Animation, &Animation::OnSkinnedModelUnloaded>(this);
i->Key->OnReloading.Unbind<Animation, &Animation::OnSkinnedModelUnloaded>(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<Animation, &Animation::OnSkinnedModelUnloaded>(this);
obj->OnReloading.Bind<Animation, &Animation::OnSkinnedModelUnloaded>(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<SkinnedModel*>(obj);
auto i = MappingCache.Find(key);
ASSERT(i != MappingCache.End());
// Unlink event
key->OnUnloaded.Unbind<Animation, &Animation::OnSkinnedModelUnloaded>(this);
key->OnReloading.Unbind<Animation, &Animation::OnSkinnedModelUnloaded>(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<String, NestedAnimData>);
result += MappingCache.Capacity() * sizeof(Pair<String, StepCurve<AnimEventData>>);
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<Animation, &Animation::OnScriptsReloadStart>(this);
}
#endif
ClearCache();
Data.Dispose();
for (const auto& e : Events)
{

View File

@@ -98,17 +98,6 @@ public:
/// </summary>
Array<Pair<String, NestedAnimData>> NestedAnims;
/// <summary>
/// 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).
/// </summary>
typedef Array<int32> NodeToChannel;
/// <summary>
/// The skeleton nodes to animation channel indices mapping cache. Use it as read-only. It's being maintained internally by the asset.
/// </summary>
Dictionary<SkinnedModel*, NodeToChannel> MappingCache;
public:
/// <summary>
/// Gets the length of the animation (in seconds).
@@ -139,18 +128,6 @@ public:
/// </summary>
API_PROPERTY() InfoData GetInfo() const;
/// <summary>
/// Clears the skeleton mapping cache.
/// </summary>
void ClearCache();
/// <summary>
/// Clears the skeleton mapping cache.
/// </summary>
/// <param name="obj">The target skinned model to get mapping to its skeleton.</param>
/// <returns>The cached node-to-channel mapping for the fast animation sampling for the skinned model skeleton nodes.</returns>
const NodeToChannel* GetMapping(SkinnedModel* obj);
#if USE_EDITOR
/// <summary>
/// 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;

View File

@@ -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<int32> SkinnedModel::GetSkeletonMapping(Asset* source)
{
if (WaitForLoaded() || !source || source->WaitForLoaded())
return Span<int32>();
ScopeLock lock(Locker);
Span<int32> result;
if (!_skeletonMappingCache.TryGet(source, result))
{
PROFILE_CPU();
// Initialize the mapping
const int32 nodesCount = Skeleton.Nodes.Count();
result = Span<int32>((int32*)Allocator::Allocate(nodesCount * sizeof(int32)), nodesCount);
for (int32 i = 0; i < nodesCount; i++)
result[i] = -1;
if (const auto* sourceAnim = Cast<Animation>(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<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(this);
#if USE_EDITOR
source->OnReloading.Bind<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(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<int32>& 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<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(this);
#if USE_EDITOR
obj->OnReloading.Unbind<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(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<Asset*, Span<int32>>::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<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(this);
#if USE_EDITOR
e.Key->OnReloading.Unbind<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(this);
#endif
Allocator::Free(e.Value.Get());
}
_skeletonMappingCache.Clear();
}
bool SkinnedModel::init(AssetInitData& initData)

View File

@@ -20,6 +20,7 @@ API_CLASS(NoSpawn) class FLAXENGINE_API SkinnedModel : public ModelBase
private:
int32 _loadedLODs = 0;
StreamSkinnedModelLODTask* _streamingTask = nullptr;
Dictionary<Asset*, Span<int32>> _skeletonMappingCache;
public:
/// <summary>
@@ -76,13 +77,11 @@ public:
/// <summary>
/// Determines whether any LOD has been initialized.
/// </summary>
/// <returns>True if any LOD has been initialized, otherwise false.</returns>
bool HasAnyLODInitialized() const;
/// <summary>
/// Determines whether this model can be rendered.
/// </summary>
/// <returns>True if can render that model, otherwise false.</returns>
FORCE_INLINE bool CanBeRendered() const
{
return _loadedLODs > 0;
@@ -154,7 +153,13 @@ public:
/// <param name="data">The data (may be missing if failed to get it).</param>
void GetLODData(int32 lodIndex, BytesContainer& data) const;
public:
/// <summary>
/// Gets the skeleton mapping for a given asset (animation or other skinned model). Uses identity mapping or manually created retargeting setup.
/// </summary>
/// <param name="source">The source asset (animation or other skinned model) to get mapping to its skeleton.</param>
/// <returns>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.</returns>
Span<int32> GetSkeletonMapping(Asset* source);
/// <summary>
/// Determines if there is an intersection between the SkinnedModel and a Ray in given world using given instance.
/// </summary>
@@ -194,7 +199,6 @@ public:
/// <returns>The bounding box.</returns>
API_FUNCTION() BoundingBox GetBox(int32 lodIndex = 0) const;
public:
/// <summary>
/// Draws the meshes. Binds vertex and index buffers and invokes the draw calls.
/// </summary>
@@ -219,7 +223,6 @@ public:
/// <param name="info">The packed drawing info data.</param>
void Draw(const RenderContextBatch& renderContextBatch, const SkinnedMesh::DrawInfo& info);
public:
/// <summary>
/// Setups the model LODs collection including meshes creation.
/// </summary>
@@ -244,7 +247,6 @@ public:
API_FUNCTION() bool SetupSkeleton(const Array<SkeletonNode>& nodes, const Array<SkeletonBone>& bones, bool autoCalculateOffsetMatrix);
#if USE_EDITOR
/// <summary>
/// Saves this asset to the file. Supported only in Editor.
/// </summary>
@@ -253,7 +255,6 @@ public:
/// <param name="path">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.</param>
/// <returns>True if cannot save data, otherwise false.</returns>
API_FUNCTION() bool Save(bool withMeshDataFromGpu = false, const StringView& path = StringView::Empty);
#endif
private:
@@ -264,8 +265,11 @@ private:
/// <returns>True if failed, otherwise false.</returns>
bool Init(const Span<int32>& meshesCountPerLod);
void OnSkeletonMappingSourceAssetUnloaded(Asset* obj);
public:
// [ModelBase]
uint64 GetMemoryUsage() const override;
void SetupMaterialSlots(int32 slotsCount) override;
int32 GetLODsCount() const override;
void GetMeshes(Array<MeshBase*>& meshes, int32 lodIndex = 0) override;