Animation Insight部分原理

Animation Insight 部分原理简介

Animation Insight 是什么

Animation Insight是Unreal提供的一款分析动画行为、记录追踪动画信息的插件,事实上Unreal还提供了分析其他系统的其他的Insight工具,此处暂不赘述。

若对使用感兴趣,可以参见以下文档或博客:

Animation Insights 概述

简述 UnrealInsights Unreal Insight 概述

Animation Insight 通信原理

笔者比较关心的事情是,因为这套Insight工具是一套插件或者独立程序(目前Animation Insight是一套插件而其余Insight工具作为独立程序),这些动画信息和行为是从引擎那边收集起来并且通知到插件的呢?

概括的说,这套系统是一个比较典型的观察者模式,动画信息的收集方是被观察者,而Insight方则为观察者,两者间通过Socket进行解耦、通信。

信息处理

首先从观察者方来进行研究,来看我们需要的是什么数据,这些数据是怎么来的,以下以AnimMontage为例。

观察者的基类是 IAnalyzer, 其有两个关键方法;用于订阅事件的 OnAnalysisBegin() ,以及用于接收这些订阅的 OnEvent()

下面是 FAnimationAnalyzer 关于Montage的部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void FAnimationAnalyzer::OnAnalysisBegin(const FOnAnalysisContext& Context)
{
auto& Builder = Context.InterfaceBuilder;
// ...
Builder.RouteEvent(RouteId_Montage, "Animation", "Montage");
}
bool FAnimationAnalyzer::OnEvent(uint16 RouteId, const FOnEventContext& Context)
{
Trace::FAnalysisSessionEditScope _(Session);
const auto& EventData = Context.EventData;
switch (RouteId)
{
case RouteId_Montage:
{
uint64 Cycle = EventData.GetValue<uint64>("Cycle");
uint64 AnimInstanceId = EventData.GetValue<uint64>("AnimInstanceId");
uint64 MontageId = EventData.GetValue<uint64>("MontageId");
uint32 CurrentSectionNameId = EventData.GetValue<uint32>("CurrentSectionNameId");
uint32 NextSectionNameId = EventData.GetValue<uint32>("NextSectionNameId");
float Weight = EventData.GetValue<float>("Weight");
float DesiredWeight = EventData.GetValue<float>("DesiredWeight");
uint16 FrameCounter = EventData.GetValue<uint16>("FrameCounter");
AnimationProvider.AppendMontage(AnimInstanceId, Context.SessionContext.TimestampFromCycle(Cycle), MontageId, CurrentSectionNameId, NextSectionNameId, Weight, DesiredWeight, FrameCounter); // 将收集到的Montage信息记录下来
break;
}
}
return true;
}

在上面的代码中,我们注意到有一个叫 AnimationProvider 的变量,其是 FAnimationProvider 类的一个实例对象,其作用是用来保存收集到的关于Animation的信息,以用作记录、分析动画信息,关于这个的信息分析本文不作讨论。

这个类的关键在于,由于每个动画信息的时间、先后都是非常重要的,因此它使用了一个 TimeLine 的设计,Montage采用的是 TPointTimeline 类来记录,这个类会记录每一次Montage播放时的 BeginTime 和 EndTime 等信息。

从 IAnalyzer 往上查找引用你能找到例如 FAnalysisEngine 类,其用于注册所有Analysis,监听与分发 OnEvent 等作用,再往上追查可以找到关于Socket建立与读取的一些信息,具体的实现细节本文不作讨论。

信息收集

Insight 系统的信息收集使用了 Trace 框架,其是一种结构化的日志记录框架,用于跟踪正在运行的流程中的仪表测量事件。此框架旨在生成高频跟踪事件的流,此类事件自我描述,可以轻松使用和共享。

其文档描述如下:Trace

以下继续以Montage的信息收集为例。 首先是Montage的信息定义:

1
2
3
4
5
6
7
8
9
10
UE_TRACE_EVENT_BEGIN(Animation, Montage)	// Animation 是这个这个事件的LoggerName, Montage是这个事件的EventName 使用这两个Name来进行后续的消息派发的路由
UE_TRACE_EVENT_FIELD(uint64, Cycle)
UE_TRACE_EVENT_FIELD(uint64, AnimInstanceId)
UE_TRACE_EVENT_FIELD(uint64, MontageId)
UE_TRACE_EVENT_FIELD(uint32, CurrentSectionNameId)
UE_TRACE_EVENT_FIELD(uint32, NextSectionNameId)
UE_TRACE_EVENT_FIELD(float, Weight)
UE_TRACE_EVENT_FIELD(float, DesiredWeight)
UE_TRACE_EVENT_FIELD(uint16, FrameCounter)
UE_TRACE_EVENT_END()

这里其实是用宏创建了一个类型,然后其每一个字段用一个 TField 类的实例来表示,这里的宏展开后有一个比较有趣的事情,就是使用 decltype 来计算了字段的序号和偏移值,以方便后续的写入缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define TRACE_PRIVATE_EVENT_BEGIN_IMPL(LinkageType, LoggerName, EventName, ...) \
// ...
Trace::TField<0 /*Index*/, 0 /*Offset*/,
#define TRACE_PRIVATE_EVENT_FIELD(FieldType, FieldName) \
FieldType> const FieldName = Trace::FLiteralName(#FieldName); \
Trace::TField< \
decltype(FieldName)::Index + 1, \
decltype(FieldName)::Offset + decltype(FieldName)::Size,

template <int InIndex, int InOffset, typename Type>
struct TField
{
TRACE_PRIVATE_FIELD(InIndex, InOffset, Type); // 这里声明关于Index、Offset的一些常量
struct FActionable
{
void Write(uint8* __restrict Ptr) const // 这里用于后面的数据写入,利用到了字段的Index和Offset
{
::memcpy(Ptr + Offset, &Value, Size);
}
};
};

Montage的相关信息在 UAnimInstance::UpdateMontage 方法中进行收集如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define TRACE_ANIM_MONTAGE(AnimInstance, MontageInstance) \
FAnimTrace::OutputMontage(AnimInstance, MontageInstance);

void UAnimInstance::UpdateMontage(float DeltaSeconds)
{
//...
#if ANIM_TRACE_ENABLED
for (FAnimMontageInstance* MontageInstance : MontageInstances)
{
TRACE_ANIM_MONTAGE(this, *MontageInstance); // 对蒙太奇信息进行收集发送
}
#endif
}

比较有趣的地方是 OutputMontage 方法中发送信息时的技巧,使用对象的生命周期来控制发送信息,如同智能指针的实现一样。

1
2
3
4
5
6
7
8
9
UE_TRACE_LOG(Animation, Montage, AnimationChannel)
<< Montage.Cycle(FPlatformTime::Cycles64())
<< Montage.AnimInstanceId(FObjectTrace::GetObjectId(InAnimInstance))
<< Montage.MontageId(FObjectTrace::GetObjectId(InMontageInstance.Montage))
<< Montage.CurrentSectionNameId(CurrentSectionNameId)
<< Montage.NextSectionNameId(NextSectionNameId)
<< Montage.Weight(InMontageInstance.GetWeight())
<< Montage.DesiredWeight(InMontageInstance.GetDesiredWeight())
<< Montage.FrameCounter(FObjectTrace::GetObjectWorldTickCounter(InAnimInstance));

展开宏以后,会发现其实在宏中创建了一个 FLogScope 类型的实例对象,利用它的构造函数来构建数据包的包头,而利用其 << 操作符以将蒙太奇的一些数据并写入缓存,再利用其析构函数将这些数据进行提交。 其重点代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
inline FEventDef::FLogScope::FLogScope(uint16 EventUid, uint16 Size, uint32 EventFlags)
{
const bool bMaybeHasAux = EventFlags & FEventDef::Flag_MaybeHasAux;
Instance = (EventFlags & FEventDef::Flag_NoSync)
? Writer_BeginLogNoSync(EventUid, Size, bMaybeHasAux)
: Writer_BeginLog(EventUid, Size, bMaybeHasAux);
}
inline FEventDef::FLogScope::~FLogScope()
{
Writer_EndLog(Instance);
}
template <typename ActionType>
inline const FEventDef::FLogScope& operator << (const FEventDef::FLogScope& Lhs, const ActionType& Rhs)
{
Rhs.Write(Lhs.Instance.Ptr);
return Lhs;
}