官方动画优化文档,建议认真查看:https://docs.unrealengine.com/en-us/Engine/Animation/Optimization
https://gameinstitute.qq.com/course/detail/10131
引用 来自Epic Games 工程师王祢语录:
URO(Update Rate Optimization),我们其实没有必要对所有的角色在每一帧都做骨骼计算。比如画面中一个角色的POSE上半身动作是怎么样,下半身动作是怎么样,是否需要融合,什么频率融合,中间是不是要插值,这些设置可以非常大程度决定骨骼更新的计算量。大家可以看到下面的图,左一是每一帧都更新;左边二是每四帧更新一次,中间用插值;第三张图是每十帧更新一次,中间用插值;最后一张图是每四帧更新一次,不用插值。大家可以看到当角色占屏面积比较小,离得比较远的时候其实是没有大差别的。
找到对应相关关键代码,抛砖引玉:
SkinnedMeshComponent如下代码
// Update Rate
/** if TRUE, Owner will determine how often animation will be updated and evaluated. See AnimUpdateRateTick()
* This allows to skip frames for performance. (For example based on visibility and size on screen). */
UPROPERTY(EditAnywhere, AdvancedDisplay, BlueprintReadWrite, Category=Optimization)
uint8 bEnableUpdateRateOptimizations:1;
bool ShouldUseUpdateRateOptimizations() const;//是否使用更新率优化 基于这个函数跟进代码会看到很多内容
/** Animation Update Rate optimization parameters. */
struct FAnimUpdateRateParameters* AnimUpdateRateParams;//重点:动画更新结构体参数,可以修改里面的参数优化
//以下控制台查看命令
static TAutoConsoleVariable<int32> CVarEnableAnimRateOptimization(
TEXT("a.URO.Enable"),
1,
TEXT("True to anim rate optimization."));
static TAutoConsoleVariable<int32> CVarDrawAnimRateOptimization(
TEXT("a.URO.Draw"),
0,
TEXT("True to draw color coded boxes for anim rate."));
SkeletalMeshComponent如下代码
/** @return whether we should tick animation (we may want to skip it due to URO) */
bool ShouldTickAnimation() const;
/** Used to scale speed of all animations on this skeletal mesh. */
UPROPERTY(EditAnywhere, AdvancedDisplay, BlueprintReadWrite, Category=Animation)
float GlobalAnimRateScale;
void USkeletalMeshComponent::TickAnimation(float DeltaTime, bool bNeedsValidRootMotion)
{
SCOPED_NAMED_EVENT(USkeletalMeshComponent_TickAnimation, FColor::Yellow);
SCOPE_CYCLE_COUNTER(STAT_AnimGameThreadTime);
SCOPE_CYCLE_COUNTER(STAT_AnimTickTime);
if (SkeletalMesh != nullptr)
{
// We're about to UpdateAnimation, this will potentially queue events that we'll need to dispatch.
bNeedsQueuedAnimEventsDispatched = true;
// We update sub instances first incase we're using either root motion or non-threaded update.
// This ensures that we go through the pre update process and initialize the proxies correctly.
for(UAnimInstance* SubInstance : SubInstances)
{
SubInstance->UpdateAnimation(DeltaTime * GlobalAnimRateScale, false);
}
if (AnimScriptInstance != nullptr)
{
// Tick the animation
AnimScriptInstance->UpdateAnimation(DeltaTime * GlobalAnimRateScale, bNeedsValidRootMotion);
}
if(ShouldUpdatePostProcessInstance())
{
PostProcessAnimInstance->UpdateAnimation(DeltaTime * GlobalAnimRateScale, false);
}
/**
If we're called directly for autonomous proxies, TickComponent is not guaranteed to get called.
So dispatch all queued events here if we're doing MontageOnly ticking.
*/
if (ShouldOnlyTickMontages(DeltaTime))
{
ConditionallyDispatchQueuedAnimEvents();
}
}
}
void USkeletalMeshComponent::TickPose(float DeltaTime, bool bNeedsValidRootMotion)
{
Super::TickPose(DeltaTime, bNeedsValidRootMotion);
if (ShouldTickAnimation())
{
// Don't care about roll over, just care about uniqueness (and 32-bits should give plenty).
LastPoseTickFrame = static_cast<uint32>(GFrameCounter);
const bool bUseUpdateRateOptimizations = ShouldUseUpdateRateOptimizations();
float TimeAdjustment = bUseUpdateRateOptimizations ? AnimUpdateRateParams->GetTimeAdjustment() : 0.0f;
TickAnimation(DeltaTime + TimeAdjustment, bNeedsValidRootMotion);
if (CVarSpewAnimRateOptimization.GetValueOnGameThread() > 0 && Ticked.Increment()==500)
{
UE_LOG(LogTemp, Display, TEXT("%d Ticked %d NotTicked"), Ticked.GetValue(), NotTicked.GetValue());
Ticked.Reset();
NotTicked.Reset();
}
}
else
{
if (AnimScriptInstance)
{
AnimScriptInstance->OnUROSkipTickAnimation();
}
for(UAnimInstance* SubInstance : SubInstances)
{
SubInstance->OnUROSkipTickAnimation();
}
if(PostProcessAnimInstance)
{
PostProcessAnimInstance->OnUROSkipTickAnimation();
}
if (CVarSpewAnimRateOptimization.GetValueOnGameThread())
{
NotTicked.Increment();
}
}
}
virtual void TickPose(float DeltaTime, bool bNeedsValidRootMotion) { TickUpdateRate(DeltaTime, bNeedsValidRootMotion); }
void USkinnedMeshComponent::TickUpdateRate(float DeltaTime, bool bNeedsValidRootMotion)
{
SCOPE_CYCLE_COUNTER(STAT_TickUpdateRate);
if (ShouldUseUpdateRateOptimizations())
{
if (GetOwner())
{
// Tick Owner once per frame. All attached SkinnedMeshComponents will share the same settings.
FAnimUpdateRateManager::TickUpdateRateParameters(this, DeltaTime, bNeedsValidRootMotion);
#if ENABLE_DRAW_DEBUG
if ((CVarDrawAnimRateOptimization.GetValueOnGameThread() > 0) || bDisplayDebugUpdateRateOptimizations)
{
FColor DrawColor = AnimUpdateRateParams->GetUpdateRateDebugColor();
DrawDebugBox(GetWorld(), Bounds.Origin, Bounds.BoxExtent, FQuat::Identity, DrawColor, false);
FString DebugString = FString::Printf(TEXT("%s UpdateRate(%d) EvaluationRate(%d) ShouldInterpolateSkippedFrames(%d) ShouldSkipUpdate(%d) Interp(%d) AdditionalTime(%f)"),
*GetNameSafe(SkeletalMesh), AnimUpdateRateParams->UpdateRate, AnimUpdateRateParams->EvaluationRate,
AnimUpdateRateParams->ShouldInterpolateSkippedFrames(), AnimUpdateRateParams->ShouldSkipUpdate(), AnimUpdateRateParams->AdditionalTime);
GEngine->AddOnScreenDebugMessage(INDEX_NONE, 0.f, FColor::Red, DebugString, false);
}
#endif // ENABLE_DRAW_DEBUG
}
}
}
void TickUpdateRateParameters(USkinnedMeshComponent* SkinnedComponent, float DeltaTime, bool bNeedsValidRootMotion)
{
// Convert current frame counter from 64 to 32 bits.
const uint32 CurrentFrame32 = uint32(GFrameCounter % MAX_uint32);
UObject* TrackerIndex = GetMapIndexForComponent(SkinnedComponent);
FAnimUpdateRateParametersTracker* Tracker = ActorToUpdateRateParams.FindChecked(TrackerIndex);
if (CurrentFrame32 != Tracker->AnimUpdateRateFrameCount)
{
Tracker->AnimUpdateRateFrameCount = CurrentFrame32;
AnimUpdateRateTick(Tracker, DeltaTime, bNeedsValidRootMotion);
}
}
void AnimUpdateRateTick(FAnimUpdateRateParametersTracker* Tracker, float DeltaTime, bool bNeedsValidRootMotion)
{
// Go through components and figure out if they've been recently rendered, and the biggest MaxDistanceFactor
bool bRecentlyRendered = false;
bool bPlayingNetworkedRootMotionMontage = false;
bool bUsingRootMotionFromEverything = true;
float MaxDistanceFactor = 0.f;
int32 MinLod = MAX_int32;
const TArray<USkinnedMeshComponent*>& SkinnedComponents = Tracker->RegisteredComponents;
for (USkinnedMeshComponent* Component : SkinnedComponents)
{
bRecentlyRendered |= Component->bRecentlyRendered;
MaxDistanceFactor = FMath::Max(MaxDistanceFactor, Component->MaxDistanceFactor);
bPlayingNetworkedRootMotionMontage |= Component->IsPlayingNetworkedRootMotionMontage();
bUsingRootMotionFromEverything &= Component->IsPlayingRootMotionFromEverything();
MinLod = FMath::Min(MinLod, Tracker->UpdateRateParameters.bShouldUseMinLod ? Component->MinLodModel : Component->PredictedLODLevel);
}
bNeedsValidRootMotion &= bPlayingNetworkedRootMotionMontage;
// Figure out which update rate should be used.
AnimUpdateRateSetParams(Tracker, DeltaTime, bRecentlyRendered, MaxDistanceFactor, MinLod, bNeedsValidRootMotion, bUsingRootMotionFromEverything);
}
AnimUpdateRateParams结构体内容优化,动态改写