漫画软件源码下载(UE4 UE5 动画系统源码 同步组)

wufei123 发布于 2023-11-28 阅读(486)

本文作者为Motphys实习生DarkFlameMaster,文章转自作者知乎前段时间我在写书,写到同步组与同步标记时,才发现我对于如何使用这个功能其实是暧昧不清的毕竟我平常干的事情也只是:将几个播放器放到同一个名字的同步组里,再用动画修饰符自动生成L和R标记,仅此而已。

至于对同步组和同步标记的深入理解,委实是谈不上了但我不想再让读者们读我的书时,也感受到这种暧昧,仅仅在感性上把握“好像是那么一回事”,而缺乏对功能的理性认识这对于发生问题时寻找bug是非常不利的在搞清楚同步组和同步标记的过程中,我赫然发现里面其实是埋了很多的坑的,只是自己平常运气比较好,按照UE文档里给出的同样暧昧的建议,侥幸还是没有踩进过坑里。

否则一旦不慎误踩,并且还没搞清楚这些坑是个什么德行,想要爬出来可能就代价巨大了本文分为三个部分:第一部分简单介绍同步组与同步标记,以前没接触过的读者可以借此部分了解和学习一下;第二部分会简单梳理一下源码,没有代码基础的读者可以选择跳过,直接看第三部分的结论,当然我个人还是推荐尽量看一看,毕竟对源码有数,才能知道为什么会出现第三部分里提到的坑;第三部分则是各种

隐藏的暗坑,以及在工作中应如何规避01同步组与同步标记的使用1.1 为了解决什么问题同步组与同步标记最重要的一个用途就是:混合拥有不同长度周期的步行和跑步动画(我们把这些动画统称为运动循环动画)有尝试自己制作过简单一些的动画状态机(含走跑切换)的读者应该有注意到,。

在走跑切换的过程中,很容易出现暧昧不清的小碎步的现象。下图的这张GIF便是我模拟了走跑切换时,ALS的行走与奔跑动画各占0.5时的样子。

我们知道,标准混合的本质是将骨骼的局部变换取两个姿势的加权平均假如A动画与B动画都是走跑相关的运动循环,当A开始往B过渡时,如果有某一个时刻(比如正好过渡到一半时),A动画中是左腿在前,B动画中是右腿在前,那么此时二者一中和,啪,两条腿都变成在原地了。

如上图,当左腿朝前与左腿朝后的两个动画一混合,混合的比例还正好是一半一半时,就会导致左腿不朝前也不朝后,落到了正中间右腿也是同理这就是角色从起步阶段到循环阶段,出现暧昧的小碎步的最主要的原因理想状况下,我们希望两个动画左脚在前时与左脚在前时混合,右脚在前时与右脚在前时混合。

但现实是常常出现左脚在前与左脚在后时混合——混合出来后左右脚都失去了角度,造成步幅严重缩水,就出现了小碎步的现象于是便引入了本文的主角:同步组1.2 同步组启用同步组的方法很简单,随便哪个播放器,在细节面板中的同步-Method里选择SyncGroup来指定它需要使用的同步组进行同步。

然后在GroupName里起一个你认为合适的同步组名字就行后续你想往这个组里加任何一个新的播放器实例,都只需要将后面的播放器也选择SyncGroup,并使用相同的同步组名字,就能让它们处于一个同步组里了。

再说说同步组的效果,它可以让整个同步组里的所有播放器播放的动画,都变为同样的播放时长(也就是归一化动画时长)那这个时长到底是由谁决定的呢?这就要引入“领路者”和“跟随者”的概念了担任“领路者”的播放器,将会以完全自由的、原始的方式进行播放(就像它没被添加进同步时那样)。

它自己的动画时长是多少,它就会花多久的时间来播放而成为“跟随者”的播放器,它将被迫减缓或加速播放自己的动画,以确保自己播放完动画的时间与“领路者”相同。

当然,虚幻引擎文档里的这张图指的是一个比较理想的情况行走和奔跑动画在这一段动画里的循环次数正好都是2因此哪怕用同步组放缩其中一个动画后(其实就是放缩了一个动画的周期),二者的每一步也能正好对得上号(也就是相位都一样)。

但如果,我们拿到的动画资源中,行走动画是走了3次,或者走了4次呢?

如图,奔跑动画还是只迈了两步,而行走动画迈了3步(现实里的动画师肯定不会这么干,一般都是偶数步不过为了方便理解,我们不举4步的例子,举3步的)很明显地,在使用同步组同步了两段动画的长度后,行走和奔跑动画的每一步的周期变得不同了(如果你觉得同样的时间内,行走动画走了3步,而奔跑动画反而只跑了两步显得很诡异,不要忘了,这是以行走动画为“领路者”,将奔跑动画播放速率延缓的结果)。

比如行走动画开始迈第三步的时候,奔跑动画才把第二步迈到一半自然地,这样又会导致上面说的由于迈步的相位不同导致的暧昧小碎步的问题重新出现(还没理解的读者可以看一下具体解释:我们假设走跑迈步都是左右左右地进行,那么奔跑动画迈第二步是右脚,迈到一半时右脚在身体前方的半空,而行走动画这时第二步已经迈完了,右脚已经收回到了身体正下方,这二者一混合,直接会导致此时右脚的迈步幅度缩减一半)。

此时此刻,我们迫切地需要一种不关注整段动画有多长,在同步时不关心两段动画的整体是否同时播放完,而是更脚踏实地,关注“每一小段(Phase)”动画是否同时播放完的方法试想,如果我们将目光聚焦于走跑动画的每一步是否同时播放完,只要能确保行走与奔跑,迈左脚的周期总是相同,迈右脚的周期也总是相同,这样不就能确保迈步的相位相同了吗?何必要去在意整个动画序列是否同时播放完毕呢?(反正都是循环动画)。

1.3 同步标记我们上面说要把目光聚焦于“每一步”是否同时播放完,但是虚幻引擎该怎么知道动画从第几帧到第几帧,是代表了迈左脚或右脚呢?有个自然的想法就是让用户自己对动画的某些帧打上标记,来把动画切割成一段段小片段,并自行赋予这些片段具体的意义。

下面要介绍的同步标记就是为此而生的。

想要使用同步标记进行同步,只需要在上面同步组设置的基础上,在组内的动画序列里打上同步标记就行了如何打同步标记请参见—知乎 [虚幻引擎中的动画同步组 | 虚幻引擎5.1文档 (unrealengine.com)]。

链接:(https://docs.unrealengine.com/5.1/zh-CN/animation-sync-groups-in-unreal-engine/)如图,这是一个很经典的给走跑动画打上左右脚的同步标记的例子。

将左脚落地打上L标记,右脚落地打上R标记,这样L和R两个标记之间包裹着那一段动画就成为了同步组进行同步的最小单元

还是拿刚刚那个例子。现在我们给走跑动画打上了同步标记,来展示一下它们是怎么进行同步的。在这个例子中行走动画为“领路者”。现在开始迈第一步。也就是进行第一个小节的同步:

行走动画播放到了第一小节的50%处,如果只使用同步组的话,为了确保两份动画整体的播放进度一样,奔跑动画这时肯定会与行走动画的播放进度保持一致但现在使用了同步标记后,在整个动画序列来看,奔跑动画播放得更快,但在我们人为手工标记的局部动画小节中,两份动画的播放进度保持相同。

现在我们让行走动画播放到第二小节的80%处自然,奔跑动画也得老老实实跟着播放到它的第二小节的80%处关于同步标记的使用介绍暂时就说这么多对于怎么打L标记和R标记,其实可以使用动画修饰符插件(Anim Modifier)来一键打标记。

先启用好插件。然后到任意一个动画序列里,在动画修饰符面板上点击添加修饰符,选择需要的修饰符(没有需要的可以自己写),然后应用修饰符即可。

需要注意的是,在Lyra中的给运动循环动画打左右脚标记的动画修饰符,它的原理不是看左脚落地没,右脚抬起没之类的,它只是单纯地看左脚什么时候越过root骨骼,它就什么时候打上L标记而已因此可能会与我们的直觉有一定的偏差。

但关系并不是特别大,就像Sin(x)三角函数一样,你可以说周期的起点是x=0,也可以说周期的起点是x=pi,反正周期都是一样长的,选谁作为起点暂时没有那么要紧1.4 UE5新增的Graph在播放器的细节面板里,Method一共有3个选项:不同步、同步组、以及图表(Graph)。

图表是UE5新增的一种方式,右键在动画图表中唤出Sync节点,

为它设置同步组名字与Group Role这样一来,它左侧的所有将同步Method设为Graph的节点的同步设置,都将会被Sync节点里预设的同步组名字与Group Role覆写如下图,将左边的两个播放器的同步Method设置为Graph,它们就会接收右侧的这个Sync节点的设置。

虽然看起来似乎这两个节点只接收了Sync节点的同步组名字的设置,但实际上连GroupRole也一起接收了,具体在源码中长这样:case EAnimSyncMethod::Graph:// Override sync group/role supplied with our group

       check(InSyncParams.GroupName == NAME_None);        NewSyncParams.GroupName = SyncGroup;        NewSyncParams.Role = GroupRole;

本来关于图表写到这就想完了的,直到昨天我组长提了两个角度清奇但确实很有可能发生的问题……1. 虽然UE动画图表的节点呈树状,一个姿势引脚无法连出1根以上的连线但如果使用缓存姿势节点缓存播放器的姿势输出,再使用缓存姿势节点,让它成为两个Sync节点的子节点,那它会被哪一个Sync节点的同步设置覆写呢?。

答案:(有蓝色时钟标记的代表正确答案,这里虹夏赢了,波奇酱输了)。

2. 如果一个播放器,在父节点中有两个Sync节点想要覆写同步设置(一个父节点,一个爷爷节点),那谁会赢?

可以看到,这次是是父节点虹夏,和爷爷节点喜多的对决,虹夏赢了从这两个例子中我们能看到,将Sync节点的威力确实能透过缓存姿势传递到它背后的播放器上而在发生Sync节点抢夺覆写权时,讲究一个名字起得好,谁名字叫虹夏,谁就会赢……咳咳,讲究一个先到先得,谁最先和播放器进行接触,谁离播放器最近,谁就能先拿下它的覆写权。

02源码分析关于怎样概括这个源码的流程我想了很久因为事无巨细地讲解固然可以让读者清楚地知道每个异常表现的来源,但也更可能劝退绝大多数读者,而且这样也会丢失主干,使得叙述陷入过多的细节中因此我打算采用简略的概括~详细的概括这种一级级展开的方式,让不同需要的读者可以各取所需。

简略概括:UE5在更新时会以“组”为单位来更新所有的动画播放器一个同步组为一个更新的组在这个组内会先更新“领路者”,然后更新“跟随者”“领路者”只需自然播放,然后将自己所处的同步小节以及比例传递给“跟随者”。

"跟随者"则需要根据动画是否循环、所处小节对自己来说是否合法等问题来决定自己是自然播放,还是一动不动,还是按比例来跟随"领路者"2.1 详细一些的概括1. 由于每一个同步组在蓝图里的唯一标识就是它的名字,因此UE用了一个 的映射来保存整个动画蓝图实例中所有的同步组们,并在更新时遍历这个映射来逐个更新它们。

2. 在每一个同步组内部,又有多个动画播放器(可以是序列播放器、混合空间播放器等,只要是能添加进同步组的就行)UE会尝试找出这个同步组内的“领路者”是谁(剩下的自然都是“跟随者”了)而找出“领路者”主要依赖的是这个播放器在动画蓝图里设置时选定的Group Role(在这个同步组中扮演的角色)。

可以看一下文档里对这5个选项的解释:

找“领路者”的具体过程是为每个播放器赋一个领路者分数(或者译为领导潜力)LeaderScore,赋分的多少靠的就是看这个播放器的GroupRole选的啥赋完分后会对播放器实例的替身(AnimTickRecord)进行排序,这个排序时的比较其实就是在比较LeaderScore,UE把这个类的“ < 操作符”重载了。

3. 选出“领路者”后,UE会获取到“领路者”的同步标记名字列表,并遍历其他“跟随者”的同步标记名字列表,取这些名字的最小交集,作为这个同步组中有效的同步标记名字这个其实就是UE文档里提示的,在使用同步标记进行同步时,只会使用大家共有的同步标记。

而如果最后取交集的结果发现,大伙的同步标记名字都不一样,没有交集,那就默认只使用同步组来对动画进行同步

4. 接着UE会尝试读取上一帧的历史位置(表示动画播放位置有两种方法,一种是使用浮点数Time,一种则是使用两个同步标记来表示一个同步小节,然后再用一个浮点数来表示动画播放到了这个小节的百分之多少的地方,后者我们称为两个标记+比例表示法)。

源码中上一帧的历史位置也有用两种方法分别表示用浮点数表示的存储在TimeAccumulator中,而用两标记+比例表示的存储在上一帧的EndPosition中如果历史位置不合法或不存在(比如播放的第一帧,或者第一次调用步进函数),UE会尝试根据之前的历史位置、CurrentTime等信息生成出一个合法的位置出来。

5. 接着以自然播放的方式,在历史位置的基础上步进“领路者”,然后将步进后“领路者”播放器当前播放到的位置,处于当前小节的比例,作为参数传递给所有“跟随者”需要强调的是,两标记+比例表示法是存在多义性的。

因为两标记用的是两个标记的名字而不是两个标记的Index(索引)6.“跟随者”更新的时候会先检查一下这一帧的目标位置是否合法如果不合法的话它就会自然播放,合法的话则会调用自己的步进函数在步进函数中,会向右推进自己的PreviousMarkerIndex还有NextMarkerIndex 。

PreviousMarkerIndex不仅会向与播放方向相同的方向改变(比如正向播放,那Index就是递增),而且还一定会和“领路者”刚刚经过的同步标记名字一样而NextMarkerIndex则是会不断向右步进,直至与这一帧结束时的“领路者”的位置(即EndPostion)保持一致。

03陷阱3.1 基础陷阱1 同步标记是基于标记的名字进行的第一个陷阱算是比较基础的了对同步标记用的比较多的读者应该都能避开这样的坑假设一个这样的情景:我们要混合两个走跑循环类的动画保险起见,我们给它们都打上同步标记,用上同步组,来避免两段动画因为相位不同,动画长度不同而导致出一些错误的姿势。

比如这是一个行走动画,我们在它的第10、20、30帧打上了3个同步标记。

然后同步组里的另一个行走动画也是类似:

看起来似乎一切都很正常这两个动画的长度甚至都是一样的那如果将它们使用同步组进行同步,想必播放所需的时间以及进度都会一模一样吧!然而,无情的现实击碎了这一直觉:下面这个GIF记录了Forward播放器为“跟随者”,与“领路者”时的情况。

观察播放器节点上那个表示播放位置的滑动条,可以看到无论哪种状况下,两个播放器的播放速率始终是不一样的,周期、相位都不同这是因为虽然看起来二者的同步标记位置上是对齐的,但名字上并没有对齐假如Forward动画此刻位于0.0s处,是“领路者”,那么这时的位置信息应该是:Right-Right,33%。

对于Forward动画来说,一个Right-Right构成的同步小节只有15帧的长度,而Back动画的Right-Right的同步小节则有整整35帧长,Back动画在尝试跟随Forward时,播放速度自然是它的两倍之多了。

3.2 陷阱2 循环动画Bug导致的时间敏感性带来的跳变我们来看一个例子这里我将diaoyan设为”恒为领路者",diaoyan1设为"跟随者"(注意,如果要复现这个例子,两个播放器都必须设置为循环动画。

因为只有循环动画才会在开始的时候就反向寻找左侧有没有合适的同步标记,而非循环动画则无法越过动画边界,在开始那段会选择自然播放)下图是"领路者",四个标记LRLR,分别在标准的13帧,16帧(0.533s),20帧(0.667s),24帧(0.8s)。

"跟随者"用的动画资产是"领路者"的直接拷贝的结果也就是说4个同步标记的位置都与"领路者"完全相同但这样就会出现令人难以置信的跳变现象我们把目光主要放在"跟随者"(syncdiaoyan1),可以看到当我完成编译,启动运行的一瞬间,"跟随者"是瞬间跳变到了中间的位置。

但如果我们对"跟随者"的同步标记做非常轻微的修改:L,R,L,R(0.816s,仅右移了0.016s)

就能发现巨大的不同。二者的同步变得非常平滑且一致。

真是奇了怪了,同步标记的名字、位置都完全一致时会发生跳变现象,有一点偏差时反而相安无事,这是为什么呢?这个问题只有深入源码才能知道答案这是由于在第一次运行的时候,初始的同步标记都是非法的,需要根据历史记录重新生成,而第一次运行压根没历史记录,所以只能根据时间来生成。

"领路者"根据时间生成的结果是,左标记为24帧的R,右标记为13帧的L,此刻CurrentTime为0,处在这一小节的43.4%处(动画总长度33帧,33-24+13=22,9/22=43%) 而"跟随者"只要利用利用刚刚"领路者"生成的这个位置就行了。

"领路者"给出的位置是:左R右L,43.4%"跟随者"会遍历整个同步标记列表,来找出哪个情况下,这个描述的位置与CurrentTime最接近本来按理来说,最后一个R标记与第一个L标记,加上43%,组合出来的位置应该与CurrentTime是完全重合的。

但是UE的代码中因为这里写的是ThisCurrentTime>动画长度,而不是“>=”,导致计算出来的结果反而是离CurrentTime最远的(减一下就变成0,成最近的了)

看起来这只是UE里的一个漏写了等于号的bug,但其实不是的即便加上了等号,虚幻引擎同步标记对于扰动还是会极端敏感比如还是这个例子,倘若"跟随者"在打标记时的最后一个R标记,往左偏了一点点,也会直接导致计算出来的结果落在了动画序列的最右端,而不是最左端,使得计算的结果取了动画中间的那一组同步标记,产生跳变现象。

这个问题的解决方案我认为不应该是去把>修改成>=号,而是应该考虑到循环动画的情况,在计算结果与CurrentTime的差距时,既向左找距离,又向右找距离从两个方向去找,以最短的距离作为参考这里的问题在于,只考虑到了计算结果越过PlayLength的情况。

UE在这里没考虑到即便没越过右动画边界,我们在为循环动画寻找计算结果与上一帧Time的时间差量时,也要考虑从右越过边界再到上一帧Time这种绕了一圈的情况会不会更近我给大伙献丑小修一手bug:

按照这种方式改了源码后,再进行测试,就非常稳定了。

3.3 陷阱3 非循环动画的停滞现象Bug复现:(前提:都是非循环动画)关闭观看更多更多正在加载正在加载退出全屏视频加载失败,请刷新页面再试

刷新

视频详情 Bug原因:TickAssetPlayer函数中,按比例追随"领路者"的逻辑原本是检查StartPos是否合法如果有效就按比例追随"领路者",如果无效则自然播放

在"领路者"刚经过第一个同步标记的那一帧,StartPos非法(因为左标记为动画边界),EndPos合法此时MarkerPassedThisTick中的标记数为1(这一帧领路者刚经过的第一个标记)因为StartPos非法,所以"跟随者"还是自然播放。

而下一帧,StartPos被更新为上一帧 的EndPos,变为合法"跟随者"可以进入到TickByMarkerAsFollower函数里了,但是MarkerPassedThisTick中的标记数为0(清空了),导致"跟随者"即便进了这个函数,却依然由于MarkerPassedThisTick列表为空,导致无法步进。

PreviousMarker到AuthoredMarkers[0]的位置,PreviousMarker依然为-1,指向左侧动画边界。因此导致"跟随者"既无法自然播放,也无法按比例追随"领路者"。

最终导致了完全停滞的现象只要将TickAssetPlayer函数中,按比例追随"领路者"的逻辑原本是检查StartPos是否合法的逻辑改为检查EndPos是否合法即可3.4 陷阱4 为什么要把起步停步动画设为总为领路者。

文档里建议我们,当朝起步或停步状态过渡时,将起步或停步设置为“总为领路者”这是因为起步和停步动画一般会被设置为非循环动画而走跑循环动画会被设置为循环动画这意味着即便把前面两节提到的虚幻引擎的几个BUG都修了,由于设计上的原因,还是会导致起步动画在开始时会一直停滞。

直到走跑循环播放到第一个同步标记的瞬间,才会跳变到自己的第二个同步标记“L”处(这个设计上的原因比较复杂,主要是"跟随者"选择的确定位置的方式导致的,如果有想知道的我可以展开细说)(当然,对一些动画来说,是可以接受从中途开始播放的,比如停步动画,可以只播放后半截。

但是起步动画不行)

此外,这其实也暴露了另一个问题:如果你想让一个动画是非循环的,那就不要把它放在以循环动画为"领路者"的同步组中当"跟随者",否则它也会跟着"领路者"一起循环的3.5 陷阱5 非循环动画由于跟随者步进方式的设计导致的跳变现象。

同步组中的各个播放器里打的同步标记,最好是位置相同,名字和顺序也相同。这对于走跑循环动画来说并不是什么问题。而对于非循环动画来说则要小心,这很容易引起"跟随者"的跳变现象。例子:领路者动画:

跟随者动画:

结果:

这个问题不是像3.2、3.3节那样由于Bug引起的,这纯粹是由于UE步进"跟随者"的PreviousMarkerIndex方式的设计导致的有仔细看源码的读者应该能知道我在说什么可以思考一下到底是UE这样确保了正确性,保证Index会与动画播放方向同向推进,。

还是说通过CurrentTime来辅助寻找PreviousMarkerIndex,保证动画不会出现大段的跳变现象,但牺牲了逻辑上的正确性,这两种设计方式哪种更好3.6 陷阱6 同步标记名字一模一样时的超快速播放现象。

其实这个到底是Bug,还是设计上导致的无法避免的问题,还是挺难说的。再来看一个例子:让"领路者"和"跟随者"的所有标记的起为同一个名字,并且位置保持一模一样。让二者都为非循环动画,测试一下运行。

结果如下:(如果二者都设置为循环动画的话,则"跟随者"会一直以领路者5倍的速度播放,感兴趣的读者可以自行尝试一下)

原因:虚幻引擎确认"跟随者"的哪个同步标记小节是与"领路者"匹配的主要方式是靠名字在这种无论"领路者"走到哪,"领路者"所处的小节,名字永远是R-R的情况下,对于“跟随者”来说,就像是从未离开过这个小节一样。

所以按理来说“跟随者”应该怎么样也出不去这个小节才是但由于GetCurrentTimeFromMarkers函数中没有把blooping作为参数传进来,使得在判断是否循环时只能通过PrevTime是否>=NextTime来判断。

如果把>=改为>,则会在循环动画的状况下出现bug(那种绕了整整一圈又找到自己身上的情况会被跟着排除掉),而如果保持现在这种样子,又会导致PrevIndex与NextIndex都保持不变,二者的时间相同,使得PrevTime直接给减到负3去了,然后“跟随者”就会以超快的速度进行播放。

(“领路者”走一个小节,“跟随者”要走完一次整个动画)

这个问题暂时没想好怎么解决就算引入了bLooping参数,来明确地区分开循环动画与非循环动画标记名字相同这两种情况,这样也只能解决非循环动画标记名字相同时发生的异常快进现象却无法解决非循环动画标记相同时,小节不会随着“领路者”步进的情况(因为是按照名字来进行确认的,名字都完全不变了,自然就不会动了……)。

04尾声:设计的艺术只有开始以引擎开发者的角度去思考时,我才逐渐意识到UE的同步标记其实有不少设计上的东西可以权衡比如,对于非循环动画的左边界和右边界附近,是像UE那样让“跟随者”默认保持自然播放好,还是强制地由引擎将非循环动画的边界也视为一个同步标记好?。

UE的做法其实是将选择权交给了用户,用户如果想让边界附近的“跟随者”也能按比例跟随“领路者”的话,只要在第0帧和最后一帧自己打两个标记就行了但我们同样要问,有些东西的自由交给用户真的会更好吗?倘若C++里不再有private和public之分,把随时随地修改变量的自由交给用户,无疑是有害的。

同样,UE将这样的自由交给用户,却又不提醒用户这样可能导致的自然播放、跳变、停滞等异常现象,也没有在文档里给出怎样规避这些问题的建议,最后只有深入看了源码两个星期的我,或者看到这篇文章的你,才能规避掉这些问题。

这样的自由,也许不要也罢再比如,在“跟随者”中应该怎么确定哪一个同步小节才是和“领路者”的同步小节对应?只按照索引?但这样在意义上会失去正确性,比如“领路者”第一个标记是L,“跟随者”第一个标记是R,这样会导致“领路者”迈左脚的动画与跟随者迈右脚的动画发生同步,显然是错误的。

但只按照索引来确定的话,又可以完美避免掉3.6节的陷阱6的产生根据同步小节的名字?比如“领路者”此时此刻PreviousMarker是L,NextMarker是R,比例是0.3在动画序列的轨道中找到符合PreviousMarker是L,NextMarker是R的可能性就行了……吗?也不尽然吧,轨道中的L标记和R标记可是有好几个呢,它们组合起来有很多可能性,也就是说,使用这种方式来确定同步小节,一定会存在。

多义性那怎么排除掉多义性呢?可以记录“跟随者”上一帧的历史时间(浮点数Time表示),通过计算所有可能的L-R-0.3的组合中,哪一种可能计算出来的位置与上一帧的Time最接近,我们就认为那个结果是正确的。

这种做法听起来很合理,但真的是正确的吗?如下图,“跟随者”明明处于中间那个LR小节中,但假如在“领路者”下一帧传递给了L-R-0.4的位置,“跟随者”在计算结果中发现,最外层的LR小节(第5帧的L标记,第77帧的R标记)的40%的位置,比起刚刚自己待的LR小节的40%位置,更接近刚刚的1.26秒,于是它就义无反顾地将自己所待的同步小节又切换成了最外层的这一对L-R。

你看,很显然,这样会使同步标记失去它原本的意义,原本我们用L-R表示左脚的一次迈起和落下,用R-L表示右脚的一次迈起和落下,而如今“跟随者”却有可能身处于第1次抬起左脚和最后一次抬起右脚,这两个完全不相邻的步伐构成的同步小节中。

甚至,还有可能使动画播放出现倒放的现象(比如某种情况下发现计算结果最接近上一帧Time的结果,比Time来得小,当然,你也可以通过限制结果必须大于Time来规避这个问题)虚幻引擎中只有在历史位置非法时(比如第一次调用步进跟随者函数,压根没有历史位置可以借鉴的时候),才会使用我们上面说的这种办法去为跟随者生成一个位置。

其余时候它采用了更稳健的推进跟随者的同步小节的办法每一帧更新,都会确保NextMarker与领路者的EndPos中的NextMarker名字一样且在领路者到达新的同步小节时,还会使PreviousMarker不仅起码达到跟随者的NextMarkerIndex的之前所在的位置(或更右边),而且保证PreviousMarkerIndex指向的同步标记的名字与领路者在这一帧中经过的最后一个同步标记名字相同。

但UE这样的两种办法相结合的设计是最好的么?首先,它会导致3.6节同步标记名字都一样时的问题(跟随者依赖于名字推进,在名字都一样时就无法推进了)其次,会导致3.5节非循环动画中,跟随者会将NextMarker推进与到EndPos的NextMarker名字相同,PreviousMarker也会跟着一起跟着步进,为此会产生瞬间跨越大段时间(跳变)的危险。

所以你看,无论采用哪种方式,都有绕不过去的缺点与危险想要采用只按Index确定同步小节的方式,就得面临逻辑上不正确的危险;想要采用只按名字确定同步小节的方式,就得面临多义性(非唯一解)的困扰;想要采用名字+历史时间确定同步小节的方式,就得面临标记倒退,乃至时间倒退这种逻辑错误的威胁;。

即使打算只在历史同步小节的基础上默默往前推进,也得承受同步标记名字完全相同时跟随者推进不动,以及推进时发生时间跳变的风险设计的艺术就是权衡的艺术希望在我动手写一份同步标记方案时,能够找到博采众长、兼容并蓄、克服各种缺点与风险的方案吧。

就像我曾经写的那篇《虚幻引擎内功:垃圾回收》那样,里面提到了JVM等高级语言是将多种原始的垃圾回收算法缝合起来,才能克服大多数算法绕不开的缺点希望我也能找到这样的道路

亲爱的读者们,感谢您花时间阅读本文。如果您对本文有任何疑问或建议,请随时联系我。我非常乐意与您交流。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。