Animation Budget Allocator
April 28, 2023
Introduction
The purpose of the Animation Budget Allocator (ABA) is to manage the performance of all skeletal mesh animations on the CPU by assigning a significance to each of them. Based on this significance the ABA will reduce the overall animation costs by reducing the URO (Update Rate Optimisation), which essentially means skipping several frames of an animation each cycle. Ideally, this means that objects closer to the camera - the ones the player is probably paying more attention to - will animate at a higher quality than those further away. Think of it like Animation LODs.
Enabling the Animation Budget Allocator System
In order to use this tool the Animation Budget Allocator plugin will need to be enabled and added as a dependency to the project (see Unreal plugin documentation).
Once the plugin is enabled, the ABA backend must also be explicitly enabled. This must be done per UWorld
by calling the FAnimationBudgetAllocator::SetEnabled()
method, accessing the appropriate object via IAnimationBudgetAllocator::Get(UWorld* InWorld)
AND globally by setting the console variable a.Budget.Enabled 1
.
While the console variable can be set at runtime the Animation Budget Allocator doesn’t like swapping states when any skeletal mehes are animating, so ideally a.Budget.Enabled
should be set in an approprate .ini file. A project using this most likely wants it enabled permenantly anyway!
auto* Interface = IAnimationBudgetAllocator::Get(GetWorld()); Interface->SetEnabled(true);
USkeletalMeshComponentBudgeted
What ties the Animation Budget Allocator and our skeletal meshes together is the USkeletalMeshComponentBudgeted
. This class inherits from USkeletalMeshComponent
and contains all the budget specific code. The USkeletalMeshComponentBudgeted
component will work out of the box, and should be used wherever a USkeletalMeshComponent
would’ve been used previously.
AnimationBudgetAllocator
The IAnimationBudgetAllocator
is has a static Get()
method which allows interfacing with the FAnimationBudgetAllocator
for a given world context. Beyond enabling the system, the most common use case for accessing this is manually registering USkeletalMeshComponents
.
The FAnimationBudgetAllocator
works by having a set budget that is calculated based on the current performance. This budget is that spread out between all the registered USkeletalMeshComponents
based on their significance. A higher significance means a higher budget allocation for a given component.
The overall budget in milliseconds is set via a.Budget.BudgetMs
allowing a default to be specified in an ini file, and for experimental runtime tweaks.
Adding to the Allocator
The most straightforward way to have a component register with the Allocator is through bAutoRegisterWithBudgetAllocator
. In the editor this can be found on the USkeletalMeshComponentBudgeted
details panel, under Budgeting and is true
by default.
Alternatively, as the Animation Budget Allocator Interface is static, you can register a component from anywhere, if a valid world is accessible.
#include "IAnimationBudgetAllocator.h" IAnimationBudgetAllocator::Get(GetWorld())->RegisterComponent(this);
Significance
In the USkeletalMeshComponentBudgeted
class you will notice a static delegate named OnCalculateSignificanceDelegate
. Binding to this delegate will override the significance calculator logic. By default, significance will be calculated based purely on the distance between the player and the relevant animating actor. Slightly more sophisticated calculations could take account of the distance and dot product (better simulating the player’s field of view), or size of the animating actor on screen.
At the time of writing, there is an issue with this original calculation. Existing engine code (UE5.1) takes all controllers into account and not just the players'. We have submitted a small fix for this issue in the following https://github.com/EpicGames/UnrealEngine/pull/10074.
const FVector WorldLocation = GetComponentTransform().GetLocation(); const UWorld* ComponentWorld = GetWorld(); for (FConstControllerIterator It = ComponentWorld->GetControllerIterator(); It; ++It) { if(const AController* PlayerController = It->Get()) { ... } }
Changing all AController
types to APlayerController
inside the USkeletalMeshComponentBudgeted::TickComponent
method will prevent AI controllers being taken in to account. Note a more sophisticated approach may still be needed depending on the exact requirements of a given title.
With the default Significance Calculator, the significance value will range anywhere from 0.0f-1.0f. A significance of 1.0 implies a higher priority to an object with a 0.0f significance. This means that once the Budget has been calculated that frame, more of it will be priotised to the higher sigifance objects.
This whole interaction with signficance means it plays well into the Significance Manager Plugin.
Optimisation in Action
Enabling the Budget Debugger (a.Budget.Debug.Enabled 1
), will show a graph along with a variety of values on each of the components.
The dotted line on the graph represents the budget it is aiming for, while the solid line is what the performance actually is. On start, the animations will immediately be prioritised in order to hit the target budget, and this is clearly visible in the debugger. The budget can be changed at runtime with a.Budget.BudgetMs
.
The number shown above each component’s owning actor is simply the significance of that object.
With the default significance algorithm in use setting a.Budget.AutoCalculatedSignificanceMaxDistance 3500
will give a better representation of the significance value.
There’s plenty of further CVars for a lot of fine-tuning, so be sure to look inside AnimationBudgetAllocatorCVars.cpp
.
Performance Benchmarking
The net-gain in performance can be visualised with the stat fps
and stat anim
console commands. The figures below were generated in a cooked build of First Person Template with the following set: r.Vsync 0
, a.ParallelAnimEvaluation 0
, a.ParallelAnimUpdate 0
, a.ParallelAnimInterpolation 0
.
In the stat anim
view, AnimGameThreadTime gives the best idea of performance. It’s also good to look at the SkinnedMeshCompTick’s CallCount, as when budgeting is off the call count is the exact number of Budgeted Components.
Statistics
Benchmarked in an empty map using the development configuration in a cooked build using increasing numbers of humanoid AI.
The first person character's arms have also been included in the budgeting.
BudgetMs was the default at 1.0ms.
AnimGameThreadTime | SkinnedMeshComp Tick | |||
Optimisation | AI Count | FPS | (Call Count / Time (ms)) | (Call Count / Time (ms)) |
Off | 16 | 73 | 36 / 3.5 | 19 / 3.5 |
On | 16 | 73 | 9 / 1.19 | 6 / 1.2 |
Off | 32 | 63 | 68 / 7.0 | 35 / 7.0 |
On | 32 | 64 | 15 / 2.2 | 9 / 2.2 |
Off | 64 | 49 | 132 / 14.2 | 67 / 14.4 |
On | 64 | 52 | 28 / 4.0 | 16 / 4.0 |
Off | 128 | 25 | 260 / 29.3 | 131 / 29.3 |
On | 128 | 39 | 55 / 7.6 | 29 / 7.7 |
Off | 256 | 12 | 516 / 60.8 | 259 / 61.9 |
On | 256 | 27 | 107 / 15.1 | 55 / 15.3 |
As shown in the table, the Animation Budget Allocator is always superior when optimised, but particularly scales better at higher component counts. The only caveat is that after ~128-255, the quality of the animations degrades to the point where it's immersion breaking.
Conclusion
Overall, the Animation Budget Allocator is a neat plugin that doesn’t require too much set-up to get going, and the auto-adjustment of optimisation based on performance means it’s it scales up well. Since it follows a similar ethos as environmental LODs, it makes sense to implement this as higher fidelity animations will not enhance lower LOD models!
Credit(s): Christopher Robertson (d3t)
Support: Josef Gluyas (Coconut Lizard)