UE5中的MotionMatching-实践(一)
前言
之前写的大都是MotionMatching各个模块如何使用,如何实现,如何配置参数等文档,实践太少。真正实践后,才能切身感受到MotionMatching的优点(当然还有缺点),才能碰到众多想象不到的问题。要知道,MotionMatching从能跑到商用有很长很长的路要走,而且这段路上的坑极其多,很多公司也是走到半路就放弃了。我写这个系列就是提前踩踩坑,踩坑的过程记录下来,方便后来人能够坚持走下去。
我的UE5-Main是在2022.8.22日更新的,PoseSearch(UE5对MotionMaching的称呼)本身就处于试验阶段,所以不确定将来是否会有大的改动(其实最近一段时间一直有提交)。
PoseSearch插件路径:UnrealEngine\Engine\Plugins\Experimental\Animation\PoseSearch
如果你对MotionMatching感兴趣,可以看下我的其他文章。
MotionMatching 中的代码驱动移动和动画驱动移动
MotionMatching中的DataNormalization
UE5中的MotionMatching(一) MotionTrajectory
UE5中的MotionMatching(二) 创建可运行的PoseSearch工程
UE5中的MotionMatching(三) PoseMatching
UE5中的MotionMatching(四) MotionMatching
UE5中的MotionMatching(五) Mirroring Animation
UE5中的MotionMatching(六) RewindDebugger
新建测试关卡
测试关卡在我们实践中特别有用,它就像测试人员使用checklist一样,我们要做的就是保证每次调整完参数或者导入新的动画库后原有功能依然正常运行,不能出现按下葫芦浮起瓢的情况。
还有就是为了方便测试各种情景,会在测试关卡中摆放许多障碍物或者目标路线等,以《最后生还者2》举例:
DebugPanel
有的时候希望能在游戏中对一些参数进行微调,而不是退出游戏-调整参数-启动游戏的流程,这样迭代会更快一些,所以有必要创建一个DebugPanel始终显示在最上方,方便调试。
动画资源
我们先从简单的动画入手,从虚幻商城中找到了这个动画库MovementAnisetPro并且我们仅仅使用它们Walk相关的几个动画并进行本次的测试。
工程的基本设置
Schema中Trajectory采样点为5个,时间分别为-0.25, 0, 0.15, 0.35, 0.45, Flags包括Velocity, Position以及FacingDirection,Pose的话骨骼设置为foot_l和foot_r, Flags包括Velocity以及Position, 采样时间仅仅设置0即可。
稳定的Idle
向Database中导入Idle动画
我们碰到的第一个需求就是没有任何输入的情况下,MotionMatching应该循环播放Idle动画
如果你是8月20号之后更新的代码,这里已经没问题了,因为我在之前向官方反馈了比较Cost时数值不稳定的问题,开发者Braeden Shosa随后解决了这个问题,所以这里就不用再谈了。
起步并直线行走
向Database中导入WalkFwdStart以及WalkFwdLoop
刚开始的时候导入的并不是WalkFwdLoop动画而是一个动捕的Walk动画,并且该动画前后几帧都有TPose, 我们希望在设置SampleRange时截断他们,并且Range.Min和Range.Max帧相同,当时希望播放到最后帧Range.Max后紧接着播放Range.Min,为了支持这个功能,我们特意将该动画的Loop设置为true,但是事与愿违,运行时发现播放帧的顺序为1,2,3,4,5,6,5,6,5,6.....播放到末尾第6帧后进行了查询,发现第5帧为理想跳转帧,后面5,6一直循环,我们并不希望这样,因为我们希望从第1帧开始播放,通过代码(FMotionMatchingState::CanAdvance)我们发现SampleRange截断后再Advance为false,这样会紧接着调整到Search的结果上,因此会造成上面的5,6循环问题。所以解决方案是我们导入引擎前就要保证这个动画是循环动画并且首尾衔接,这样就不要设置SampleRange。如何理解呢?AnimSequence中的Loop表示这个动画本身为首尾衔接的Loop动画,导入到Database中设置了SampleRange,这时候不能指望Range内的动画依然可以首尾衔接。所以结论就是:如果想给Database中导入一个循环动画,那么该动画在DDC制作时最好已经是成品了
WalkFwdLoop生成Mirrored版本数据
WalkFwdLoop MirrorOption设置为OriginalAndMirrored
某些功能需要我们生成镜像版本的数据,我们再次运行游戏,稳定输入下角色走路一直在JumpPose导致脚部有滑步
我们通过RewindDebugger发现切换的原因是MirroredWalkFwdLoop的某一个PoseCost值为0.001,而ContinuityPoseCost为0.003,所以系统认定MirroredWalkFwdLoop的Pose比ContinuityPose更加适合,所以选择了跳转。如此反复导致了Pose的频繁切换,如何解决呢?一种可行的解决方法是增加MirroringMismatchCost,这样如果Origin动画和Mirrored动画相互切换时就产生了额外的Cost,我们设置为0.02即可。设置后我们发现稳定输入的情况下,不再频繁切换Pose了。
站立状态下向不同方向起步
创建BlendSpace1D,包含WalkFwdStart, WalkFwdStart90_L, WalkFwdStart135_L, WalkFwdStart180_L,命名为BSWalkFwdLeftStart
引入到Database中并将MirrorOption设置为OriginalAndMirrored
UseGridForSampling设置为false
NumberOfHorizontalSamples设置为20
NumberOfVerticalSamples设置为1
上面我们仅仅导入了WalkFwdStart动画处理向前起步的问题,但游戏中可能会向各种方向起步,我们不太可能制作各个角度的起步动画,这个时候Database支持BlendSpace类型的动画就很有必要了,我们制作好BSWalkFwdLeftStart后,将NumberOfHorizontalSamples设置为20,表示横向生成多少个采样动画,20就意味着会生成20个采样动画, MirrorOption设置为OriginalAndMirrored可以生成Right版本的动画。
当角色向后背方向起步转身行走时,我们通过RewindDebugger发现这时候选择动画出现了一些问题,理想情况应该是始终播放WalkFwdStartL180这个动画,但实际情况是播放了一段WalkFwdStartL180后又选择了WalkFwdStartL135的动画,我们在RewindDebugger将时间轴拖到发生跳转的那一刻,看下发生了什么。
可以看到两者的Cost还是很近的(180L动画Cost为0.591, 135L动画Cost为0.507)180L吃亏在第三个Trajectory采样点的FaceDirection上,解决方案有多种:
- 调整Trajectory采样时间,每个采样时间点权重不同等
- 修改Schema中的ContinuingPoseCostBias,在《最后生还者2》中的Motion Matching中我们已经讲过了Bias的含义了, 我们这里设置为-0.12
- WalkFwdStart系列动画中添加PoseSearchBlockTransitionNotifyState,将BeginTime设置为0.13,这样一旦选择好转身动画后几乎不会再反复横跳了
这里还有一个重大的改变就是Movement的运动全部基于SpringDamper而不是虚幻原生加速度,摩擦力,旋转速度那一套,为什么这么修改呢,SpringDamper有哪些好处呢?
- 对于旋转来讲,虚幻原生的RotationRate并不能反映真实情况,人在转身90或者180的时候并不是按照一个固定速度旋转的,SpringDamper要比固定的RotationRate更合理
- 虚幻原生那套计算速度的函数CalcVelocity太麻烦了,不管对于策划还是程序,需要调整摩擦力,加速度,如果MaxWalkSpeed换了,这些值可能还得再调一遍,停步时还得调整摩擦力和减速度,SpringDamper相对来讲要好很多,策划一般仅调整一个参数即可
- 运动模型应该尽可能地简单,因为将来会开发Maya相关的插件用来检查动画和游戏运动模型是否很好地匹配,虚幻原生的运动模型显然没有SpringDamper简单
修改的话也比较简单,自定义MovementComponent和TrajectoryComponent, 对于MovementComponent来说,需要重载PhysicsRotation和CalcVelocity函数,我们可以在这两个函数中求出TargetRotation和TargetVelocity, 并且知道当前的CurRotation和CurVelocity, 调用QuaternionSpringInterp和VectorSpringInterp就能求出当前的Rotation和Velocity了。TrajectoryComponent需要重载StepPrediction和StepRotationWS函数,利用相同的方法实现SpringDamperSmooth,需要注意的是需要在PredictTrajectory调用前将MovementCompoent的SpringState同步给TrajectoryComponent的SpringState,保证预测更加准确。
通过以上调整,我们再看下各方向起步的效果:
视频中有两个点可以说下
- 运动方向与角色朝向分别是0,90,135,180的时候,分别选择了相应的动画并播放,符合我们的预期;
- 特别有意思的是,我们提供的资源里面并没有Pivot相关的动画,但是视频后半部分角色在进行Pivot相关运动时,可以看到角色利用WalkStart动画展示出了Pivot效果,表现相当不错,也一点也展示出了MotionMatching相比于传统方案的优势
停步动画
Database导入WalkFwdStop_RU和WalkFwdStop_LU
添加停步动画时刚开始想法是使用一个WalkFwdStop_RU, 通过配置OriginalAndMirrored来生成左脚的动画,但运行时发现有问题,因为没有配套的Idle动画导致最后出现了旋转的问题,所以没有采用镜像来生成而是提供了两个停步动画WalkFwdStop_LU和WalkFwdStop_RU
当我们选择停步时两脚的情况是不确定,MotionMatching给出的结果可能是WalkFwdStop_LU 0.8s开始播放,但是我们Movement停步计算出的时间几乎每次都是相同的,这将导致WalkStop动画的脚已经站定了但Movement还在移动或者相反的情况,如果是DataDriven或者有Clamp设置还好,但CodeDriven会经常出现滑步的问题,除了IK解决以外也可以考虑添加更多种类型的WalkStop动画(像《FF7重制版》做的那样),但特别注意的是,如果不是配套的动画资源直接拿过来使用的话,会导致很多问题,比如下面视频里看到的,Idle不一致,这是个很严重的问题,CorePose始终应该是确定且唯一的
弧线行走
Database导入WalkArchLoop_L和WalkArchLoop_R
添加WalkArchLoop动画前的效果:
添加WalkArchLoop动画后的效果:
Strafe
Database导入Strafe系列动画
Strafe需要在Character上写少量代码即Gamepad_LeftTrigger Pressed触发时将bUseControllerDesiredRotation设置为true,bOrientRotationToMovement设置为false, Released设置相反即可。我们导入动画后没有做其他任何设置:
讨论
MotionMatching优势是啥?本次实践给我最大的感受就是工作流。比如策划希望在原来的状态机基础上添加一个‘受伤’状态,受伤状态需要Fullbody动画来表现全身受伤的状态,如果是传统做法,动画师和策划都需要做哪些工作呢?
动画师:需要做的动画有很多,受伤状态下Idle, 行走,跑动,射击,下蹲,匍匐前进等,更可怕的是受伤状态与其他各种状态的过渡动画也要做,而且还有小心翼翼地去切割动画,这里是Loop,这里是Transition,首尾帧要一致,哦? 刚才是不是攻击动作没有考虑呢 :(
策划:负责状态机的策划同学更忙了,受伤状态到其他各种状态的过渡都要考虑 :(
如果使用MotionMatching呢,事情可能会变的好一点:
动画师:跑到动捕室里动捕受伤相关的一堆动画,第二天按照程序要求进行修正剪辑,虽然工作量没有减少多少,但是再也不用去按照各种状态要求去减动画了,动画师可以把更多的时间放到内容本身以及最后的动画分类上,可以说从繁琐的细节中解放出来了。
策划:策划的工作更简单了,把动画导入到Database中即可。
理想中MotionMatching的工作流就是这样,如果状态机异常复杂它的优势就更加明显,所以说它特别适合3A级别写实类的游戏。
缺点呢?上面也提到了-动捕,MotionMatching和动捕几乎是分不开的,MotionMatching特别依赖众多的动画数据,而动捕就意味着成本,也有人问不能用大量手Key动画驱动MotionMatching吗?何必为难动画师呢。。。传统方案对于一般游戏来讲也是很香的
另外一个缺点是要求经验丰富的动画TA,否则根本搞不定
Daniel Holden在演讲中对于工作流做了更细致的介绍,强烈推荐
Next
使用MotionMatching需要面对的核心问题:
1. 动画数据需要RootMotion数据,在大量动捕数据(无根骨骼信息)的前提下,如何快速生成相对准确的RootMotion?
2. 动画师需要按照什么标准修改根骨骼数据以匹配Gameplay的需要,是自动化检测工具提醒还是其他?
3. CorePose要保持一致,比如Idle的状态,这时候动画师和技术需要定义好各种类型的CorePose,动捕数据需要大量粘贴CorePose
由于MovementAnisetPro资源实在有限,所以本次实践到这里就打住记录下。下次实践我们从一些开源的动捕数据库着手~
本人水平有限,如果错误或者不妥,欢迎拍砖讨论~