Files
FlaxEngine/Source/Engine/Animations/Animations.cpp
T
mafiesto4 bc36168318 Optimize Animated Model rendering with hardware instancing
All models are using the same global buffer for skinned bones which allows to share shader binding for instancing.
Refactor draw call for batching skinned mesh draws.
Remove `SkinnedMeshDrawData` and merge it into `AnimatedModel` internals.
2026-06-15 17:59:41 +02:00

180 lines
5.3 KiB
C++

// Copyright (c) Wojciech Figat. All rights reserved.
#include "Animations.h"
#include "AnimEvent.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Level/Actors/AnimatedModel.h"
#include "Engine/Engine/Time.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Threading/TaskGraph.h"
class AnimationsService : public EngineService
{
public:
Array<AnimatedModel*> UpdateList;
AnimationsService()
: EngineService(TEXT("Animations"), -10)
{
}
bool Init() override;
void Dispose() override;
};
class AnimationsSystem : public TaskGraphSystem
{
public:
float DeltaTime, UnscaledDeltaTime, Time, UnscaledTime;
bool Active;
void Job(int32 index);
void Execute(TaskGraph* graph) override;
void PostExecute(TaskGraph* graph) override;
};
namespace
{
FORCE_INLINE bool CanUpdateModel(const AnimatedModel* animatedModel)
{
auto skinnedModel = animatedModel->SkinnedModel.Get();
auto animGraph = animatedModel->AnimationGraph.Get();
return animGraph && animGraph->IsLoaded()
&& skinnedModel && skinnedModel->IsLoaded()
#if USE_EDITOR
// It may happen in editor so just add safe check to prevent any crashes
&& animGraph->Graph.Parameters.Count() == animatedModel->GraphInstance.Parameters.Count()
#endif
&& animGraph->Graph.IsReady();
}
}
AnimationsService AnimationManagerInstance;
TaskGraphSystem* Animations::System = nullptr;
ReadWriteLock Animations::SystemLocker;
#if USE_EDITOR
Delegate<Animations::DebugFlowInfo> Animations::DebugFlow;
#endif
AnimEvent::AnimEvent(const SpawnParams& params)
: SerializableScriptingObject(params)
{
}
AnimContinuousEvent::AnimContinuousEvent(const SpawnParams& params)
: AnimEvent(params)
{
}
bool AnimationsService::Init()
{
PROFILE_MEM(Animations);
Animations::System = New<AnimationsSystem>();
Engine::UpdateGraph->AddSystem(Animations::System);
return false;
}
void AnimationsService::Dispose()
{
UpdateList.Resize(0);
SAFE_DELETE(Animations::System);
}
void AnimationsSystem::Job(int32 index)
{
PROFILE_CPU_NAMED("Animations.Job");
PROFILE_MEM(Animations);
auto animatedModel = AnimationManagerInstance.UpdateList[index];
if (CanUpdateModel(animatedModel))
{
auto graph = animatedModel->AnimationGraph.Get();
#if COMPILE_WITH_PROFILER && TRACY_ENABLE
const StringView graphName(graph->GetPath());
ZoneName(*graphName, graphName.Length());
#endif
// Animation delta time can be based on a time since last update or the current delta
float dt = animatedModel->UseTimeScale ? DeltaTime : UnscaledDeltaTime;
float t = animatedModel->UseTimeScale ? Time : UnscaledTime;
const float lastUpdateTime = animatedModel->GraphInstance.LastUpdateTime;
if (lastUpdateTime > 0 && t > lastUpdateTime)
{
dt = t - lastUpdateTime;
}
dt *= animatedModel->UpdateSpeed;
animatedModel->GraphInstance.LastUpdateTime = t;
// Evaluate animated nodes pose
graph->GraphExecutor.Update(animatedModel->GraphInstance, dt);
// Update gameplay
animatedModel->OnAnimationUpdated_Async();
}
}
void AnimationsSystem::Execute(TaskGraph* graph)
{
if (AnimationManagerInstance.UpdateList.Count() == 0)
return;
Active = true;
// Ensure no animation assets can be reloaded/modified during async update
Animations::SystemLocker.ReadLock();
// Setup data for async update
const auto& tickData = Time::Update;
DeltaTime = tickData.DeltaTime.GetTotalSeconds();
UnscaledDeltaTime = tickData.UnscaledDeltaTime.GetTotalSeconds();
Time = tickData.Time.GetTotalSeconds();
UnscaledTime = tickData.UnscaledTime.GetTotalSeconds();
#if USE_EDITOR
// If debug flow is registered, then warm it up (eg. static cached method inside DebugFlow_ManagedWrapper) so it doesn't crash on highly multi-threaded code
if (Animations::DebugFlow.IsBinded())
Animations::DebugFlow(Animations::DebugFlowInfo());
#endif
// Schedule work to update all animated models in async
Function<void(int32)> job;
job.Bind<AnimationsSystem, &AnimationsSystem::Job>(this);
graph->DispatchJob(job, AnimationManagerInstance.UpdateList.Count());
}
void AnimationsSystem::PostExecute(TaskGraph* graph)
{
if (!Active)
return;
PROFILE_CPU_NAMED("Animations.PostExecute");
PROFILE_MEM(Animations);
// Update gameplay
for (int32 index = 0; index < AnimationManagerInstance.UpdateList.Count(); index++)
{
auto animatedModel = AnimationManagerInstance.UpdateList[index];
if (CanUpdateModel(animatedModel))
{
animatedModel->GraphInstance.InvokeAnimEvents();
animatedModel->OnAnimationUpdated_Sync();
}
}
// Cleanup
AnimationManagerInstance.UpdateList.Clear();
Animations::SystemLocker.ReadUnlock();
Active = false;
}
void Animations::AddToUpdate(AnimatedModel* obj)
{
ScopeWriteLock lock(SystemLocker);
AnimationManagerInstance.UpdateList.Add(obj);
}
void Animations::RemoveFromUpdate(AnimatedModel* obj)
{
ScopeWriteLock lock(SystemLocker);
AnimationManagerInstance.UpdateList.Remove(obj);
}