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); 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) 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 , 0 , #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); struct FActionable { void Write(uint8* __restrict Ptr) const { ::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; }
|