UE4实现MotionMatching(上)
先放下UE4里实现的效果,本文分为上、下两篇,上篇是介绍学习,下篇是具体实现。
下篇链接:UE4实现MotionMatching(下)
本文主要想介绍下MotionMatching实现的一些细节和问题,这里只讨论最基础的MM,不包括神经网络相关的实现。
一、荣耀战魂MotionMatching技术
MM概念就不介具体绍了,可以参考育碧16年关于荣耀战魂的GDC演讲[1],应该算是最全面的了,包含了原理、实现到实际应用中解决的一些问题,这里简单总结一些要点:
- 某些情况下使用骨骼分层混合,但是大多数情况下都是全身动画。
- 算法:目标动画帧匹配当前的情况并且未来的运动模式符合预期,就是Cost最小的。
- Cost函数:PostMatching和TrajectoryMatching对应的特征选择,只是给出了些建议,后面也有提到MotionShader,也就是不同场合使用不同的Cost函数,应该是在育碧其他的MM实现里应用了。
- 姿势匹配:动捕的动画有左右手的姿势区别,全部标记出来,匹配的时候就能根据逻辑需求切换到对应的姿势。
- 轨迹模拟:使用了临界阻尼模型来平滑速度,具体参考《游戏编程精粹4》1.10章节。
- 移动方式:PPT里写的看着像走RootMotion,但其实说的是“do a little bit of displacement from animation”,事实上就是逻辑的位置是模拟出来的,动画也就是Mesh的位置来自RootMotion数据,只不过限定在逻辑位置附近,两边各跑各的,这点在《黑神话:悟空》的分享[2]里说到了。但是这个处理真的很容易滑,不过正常游戏视角不太能看到脚。
- Event匹配:类似前面提到的姿势匹配,这里主要是战斗系统相关的处理,下图左可以理解为类似技能编辑器或者战斗的逻辑状态机,蓝色的节点视频里叫作“logic clips”,会向动画系统发送一些Event事件,节点本身在满足一些逻辑条件后会跳转到别的节点,下图右就是所有动画的列表,会手动添加Event标记,Event还可以带一些Tag,动画系统收到Event请求后会由MotionMatching系统选择最合适的动画帧进行播放。另外还提到某些战斗技能造成的位移是走的RootMotion,因为这些位移过于复杂无法模拟。
- 朝向修正:动捕无法覆盖所有方向上的移动,所以会有RootMotion数据跟模拟数据在运动方向上有差异,处理方式就是需要的时候平滑旋转角色到正确的朝向上。
- 其他一些问题解决:缩放动画数据以适应不同的逻辑速度需求,脚步IK处理滑步,楼梯、斜面的处理等。
二、MotionMatching项目介绍
下面先介绍一些网上能看到的MotionMatching项目,主要集中与匹配特征选择、匹配算法、轨迹模拟、算法稳定性以及工程实现上:
1.UE4商城插件FilmstormMotionMatching
效果不太行,就不演示了,代码跟网上的开源的Hethger/UE4_MotionMatching-一样,只是改了类名、变量名(所以代码直接看这个开源的就行,更好理解一点),实现上同育碧GDC的分享基本一样:
- 动画每0.1秒采样一个匹配数据,PoseFeture是选定骨骼的位置、速度,TrajectoryFeture是未来1秒轨迹的位置,采样10个点,另外还有个单独的Root速度。
- 匹配算法上,输入姿势数据是根据当前实际动画播放时间计算的PoseFeture数据,而不是取的预计算数据,匹配过程就是全遍历找个Cost最小的动画帧,Cost函数是:RootVelDis*Param1+PoseDis*Param2+TrajectoryDis*Param3 分了3部分,各自有个乘数控制最终结果,Dis直接就是向量的距离,Trajectory里只算了最后一个点。
- 轨迹模拟从当前点到根据输入计算的1秒后的点之间进行线性插值,也就是一条直线。
- 稳定性上匹配的动画帧如果同当前动画帧时间差值0.2秒之内就不切换了,继续播放,需要切换的话动画进行过渡混合,混合方式是线性插值,另外也有朝向修正,也就是从当前朝向平滑到输入的朝向。
效果不太好一是Cost函数不太合理,速度、位置直接混在一起算距离,Trajectory甚至只用最后的点,各个乘数也只能瞎试,二是轨迹模拟也过于简单,另外还有匹配骨骼基本上选了全骨骼,可以说是勉强实现了一个可以跑起来的版本。
2.Unity商城插件Motion Matching for Unity(2.2.8)
效果还可以,比较流畅,起、转、停都有较好的细节,MM算法实现上也是跟育碧GDC分享的差不多:
- 50ms采样一次动画数据,PoseFeture是骨骼的速度、位置以及Root速度,骨骼选的是头、左右手、左右脚,TrajectoryFeture是轨迹点的位置、朝向,轨迹点5个:-0.666、-0.333、0.333、0.666、1,包含了过去的轨迹点。
- 匹配时输入的姿势数据根据实时动画时间从预计算采样的2个Pose插值得来,匹配算法同样是全遍历,只不过用了JobSystem加速,Cost计算分为Pose、Trajectory两部分,向量就是欧式距离,轨迹点的朝向存的是Yaw所以就是差值,Cost计算公式里每个计算结果都有对应的乘数控制,还有一些公共的乘数,总共3套参数如下图:
- 轨迹模拟是从当前轨迹指数平滑到目标轨迹,也就是每帧从旧的轨迹插值到最新的直线轨迹,记了20个点,Match时在取对应的点,插值的参数根据运动的速度会有变化,如下图代码里基础值加上配置的乘数:
- 稳定性上有0.2秒检测和朝向修正,朝向修正有一些边界条件如速度过小或者角度过大,还有就是手动在动画上加的“DisableWarp”的Tag。为了减少动画切换的频率,在每帧执行MM算法前会检测下一个Pose的轨迹是否和目标轨迹相似,也就是位置差值、朝向差值都小于配置的阈值,相似的话就不再执行MM算法了,这也挺大程度上减少了CPU消耗。还有就是动画上加了大量的Tag(下图1),尤其是“DoNotUse”,每个动画都有,工作量不小。最后还有个很重要的点:重新制作动画里RootMotion的旋转数据(下图2),原本动捕的Yaw是一直抖动的,制作的RootMotion里的Yaw在转向的地方平滑,直行的地方就是直线。
可以看到作者在MM的实现效果上做了大量的努力,例如大量的参数配置和动画的处理,但MM算法本身的不稳定性还是最大的硬伤,其中很大程度上是轨迹匹配的设计造成的。下面是几个比较不太能接受的效果实例,右边部分是作者开发的调试器,蓝色表示跳转到新的动画帧,绿色是当前混合权重最大的动画帧:
1.持续大转弯时会卡在同一动作上
可以看到预测的轨迹并不十分匹配,但是过去的轨迹很接近,导致一直在切换到同一动作里,所以选择匹配过去的轨迹也有一些局限的地方。
2.180°急转弯动作很生硬
这个在荣耀战魂里也有(不一定是同一个原因),主要是前面的减速过程匹配到了停止的动画,再到启动加速,如果一开始就匹配到了180°转弯动画,后续就比较流畅,这个问题主要是轨迹匹配使用的是坐标,并且采样数量有限,坐标叠到一起就被认成是减速停止了。
3.从转弯回到直行会有多余的转弯动作
还是轨迹匹配造成的,预测轨迹平滑速度较慢,过去轨迹也刚好是个曲线,所以就匹配到了snake动画里了,最后在切到直线,能明显看出奇怪的动作。
这个插件在Locamotion上支持了走、跑、冲刺以及锁定朝向移动的无缝切换,并且还加了跳跃、攀爬、翻越、翻滚、滑铲等机制,Locomotion与其他动作机制是通过状态机切换管理的,只有Locomotion部分是走了MotionMatching,Locomotion里的子状态是通过检测输入改变MM和轨迹模拟的参数,以及改变搜索动画的Tag来实现的。
总结一下,虽然效果还可以,但是实操中有非常大量的参数需要调整,有一部分还藏在代码里,关键是修改起来也是凭感觉边试边改,动画的修改还是思路很清晰的,MM算法还需要不断改进,整体使用成本有点高,不过拿来参考学习还是有价值的。
3.Unity官方动画系统Kinematica(0.8.0-preview)
虽然起了个高大上的名字,但其实就是MotionMatching,效果可以说挺好了,是大佬Michael Buttner最新的MotionMatching实现,可以说MotionMatching就是他发明的,开头GDC视频里也介绍了育碧把他挖了过来并且同时开始4、5个关于这项技术的研究和实现,大佬15年的MM分享[3]很值得学习,19年的分享[4]就是介绍的Kinematica,下面先稍微介绍一下这个分享:
《Machine Learning for Motion Synthesis and Character Control in Games》,从名字可以看出作者想用机器学习来提升MM。对于现有MM算法作者最大的吐槽就是手动调整Cost函数非常痛苦,而且函数本身并不可靠,需要大量的测试调整,扩展性很差,当然优势就是产出的动画效果好,算法易于实现。
作者也研究了一些神经网络相关的实现,结论就是不适合游戏,包括:只关注于Locomotion、Phase的假设不适合所有情况、自回归会损失动画的质量等,其中最大的缺点是神经网络的训练时间太长,无法满足游戏开发的迭代速度(这里提一下PFNN作者最新的LNN[5]论文里说的一句话:“Finally, important to note is that although training times for our method are long compared to basic Motion Matching (which requires no training), our framework still allows artists and designers to iterate quickly as they can use basic Motion Matching until they are happy with their results, and then simply switch to Learned Motion Matching as a post-process.”,感觉还是挺厉害的)。
神经网络没法用,作者还是关注原始的MM算法,最需要解决的就是Cost函数,新的处理是将每个采样点在一段时间内的所有姿势数据都编码到一个矩阵里,后面只需要对这个矩阵做相似性检测找到最近邻就行了(Kinematica真正的实现并不是说的这样的,后面会讲到,包括下下图中的神经网络也是没用到的)。
最近邻算法使用的是乘积量化(Product Quantization)算法,关键字就是量化,也就是做数据压缩,压缩的方法是使用K-means算法也就是K均值聚类,简单理解就是2、3维向量量化的话可以画格子取中心点而K-means是则随机撒点在多次迭代找到这些中心点,由于向量维数会很高,直接K-means效果不好,PQ处理是高维向量分成M个子块,每个子块做K-means,最后所有K-means出来的这些中心点用一个数组ID索引,原来的高维向量数据就直接变成了一堆数组ID了,例如Kinematica里的处理就是特征向量数组按每个Vector聚类,聚类数量是256,结果就是3个float压缩成一个byte,内存减小12倍而聚类中心点内存可以忽略不记(这里是匹配数据的内存),最后执行最近邻搜索就直接拿这些聚类中心点算距离就行了。可以看到这是一个有损的压缩,搜索的结果也是近似的最近邻,数据量大的情况下效果不错,这个算法本身就是为了解决传统KNN算法在内存和效率上的问题。
其他的内容就不具体介绍了,下面直接分析Kinematica的MM实现:
- 采样率每秒30帧,同动画一样,PoseFeture是骨骼的位置和前后一帧内采样3次位移,并没有使用速度,骨骼选的是头、左右脚,总共12个Vector,TrajectoryFeture是未来1秒内采样4个点的速度和朝向,这里用的速度,总共8个Vector,具体参数见下图,可以看到配置挺简单,最后的4个参数是PQ算法里K-means聚类用的。前面说的巨大的矩阵数据并没有在版本里看到,还是传统的PoseFeture和TrajectoryFeture,只不过选的Feture不太一样。
- 因为使用了PQ算法,所以需要处理Feture向量,机器学习里在训练模型之前一般都会对数据进行预处理,例如归一化、标准化,以便不同维度的数据可以直接一起比较,也就不需要像项目二中搞一堆权重了。这里用的是向量归一化,其中速度包含了速度大小和方向2个信息,处理是大小Min-Max量化到255范围存成一个单独的byte,方向直接normalize,朝向就是normalize过的不需要处理,位置和位移使用BondingBox归一化。前面说了按每个Vector各自的数据集进行聚类,最后PoseFeture变成12个聚类中心的ID,TrajectoryFeture是8个聚类中心加单独的量化的速度大小,最后也是大小12的byte数组。
- 匹配算法里,输入的当前姿势数据直接取自当前的采样帧数据,没有插值计算更快,还有个影响是连续播放的话下一帧的PoseCost是0,连续播放的优势会更大一点。搜索上因为存的Feture是数组ID,需要还原成聚类中心,PQ算法处理是输入的Feture计算到每个聚类中心的距离,这里位置类的向量计算的是欧式距离,方向类的向量计算的是余弦值,最后存成查找数组,候选的Feture就可以直接拿记的ID查到每个Vector到对应聚类中心的距离,最后所有Vector距离结果相加就行,消耗就是256*Feture数量的距离计算,加上全遍历的数组查找和结果相加,可以看到前面的距离计算可以忽略,后面的全遍历也非常简单,性能提升不小。Cost函数本身只剩一个responsiveness只用于调整Pose匹配和Trajectory匹配的倾向了,Feture选的也相对合理,基本上就可以告别调整Cost函数的痛苦了。
- 轨迹模拟使用的是ExponentialDecay来平滑速度和朝向,配置参数控制平滑速率,因为走的是RootMotion,每帧从当前实际的速度、朝向平滑到目标速度、朝向。下图是控制的参数,配置起来也非常简单。
- 稳定性上并没有判断0.2秒,而是看匹配的TrajectoryCost是否比当前的TrajectoryCost更小(差值大于一个阈值),也就是新的TrajectoryCost足够优秀就可以切换了,这种方法比简单的判断时间间隔更科学一点,这里对应就是解决上面PPT里的“Back-in-time”问题。动画数据的处理上并没有使用任何Tag,下图中的LocomotionTag只是为了在翻越、攀爬动画上进行区分,RootMotion旋转数据Yaw可以看下下图应该是简单的平滑处理一下。
- 朝向修正上,每帧从RootMotion产生的旋转平滑到预测轨迹里的旋转,插值的计算来自配置的百分比和速度区间,参数控制见下图。这里直接用的当前帧的旋转数据,并不是1秒后的旋转数据,这个处理方式会快速的把角色拉到预期的朝向,否则的话因为偶而匹配的轨迹朝向和目标差别有点大,角色就会有一段时间跑偏在跑回来,操控感较差,但是这也造成了很容易看见角色非常奇怪的快速滑步转圈行为(代码里是直接旋转了RootMotion数据,并不是改的角色朝向),详见下下图。
- 180°急转弯除了上面这个常见的问题之外,还有一个就是有时候细节表现不太行,同项目二的Unity商城插件类似,切到了停止,再到启动转身,不过表现还是比项目二好多了,而且造成的原因也不一样,这里是检测摇杆推到了中间,自然就认成了输入停止,根本上也是轨迹匹配的问题。
总的来说,除了转圈滑步问题,几乎很少看到其他奇怪的表现,细节表现很好,配置起来十分简单,Cost函数也基本没有修改的必要,动画数据也不怎么需要处理,使用成本还可以,此外还有一个非常强大的调试器,实时显示动画播放状态、轨迹、Cost函数结果,还有一个图表化的界面显示MM执行情况,关键是可以录制一段时间然后拖动查看每帧的情况并且还可以在任意帧进行调试!
Kinematica的Locomotion部分支持了走、跑、疾跑的无缝切换,并且完全是按照输入的轨迹来匹配的,并没有使用任何Tag或者其他潜规则,可见算法的稳定性还是很好的,除了Locomotion外还支持了翻越和攀爬,虽然不是直接用的MM,但是都是基于同一套底层的动画数据结构,并且用到了PoseMatching和TrajectoryMatching的方法找到最佳的过渡播放点,表现上看起来十分流畅,不过这两个机制的手感很一般,应该是后期演示需求加进去的。
Kinematica是一套全新的动画管线,和之前的流程并不兼容,要达到可用的程度感觉还是有一段距离的,可惜官方已经宣布暂停开发了,大佬去年已经去了Nvidia,估计Unity是不准备继续搞了。
参考
- ^荣耀战魂MotionMatching技术 https://www.gdcvault.com/play/1022985/Motion-Matching-and-The-Road
- ^《黑神话:悟空》的Motion Matching | 游戏科学 招文勇 https://www.bilibili.com/video/BV1GK4y1S7Zw
- ^[Nucl.ai 2015] Motion Matching - The Road to Next Gen Animation https://www.youtube.com/watch?v=z_wpgHFSWss&t=660s&ab_channel=MichaelButtner
- ^I3D19 Keynote: "Machine Learning for Motion Synthesis and Character Control" (Michael Buttner) https://www.youtube.com/watch?v=zuvmQxcCOM4&ab_channel=I3DSymposium
- ^Learned motion matching https://dl.acm.org/doi/10.1145/3386569.3392440