Lyra技术分析:角色动画
前言
本文记录一下笔者分析并模仿制作Lyra动画系统的过程和心得, 由于系统非常庞大, 所以会逐步更新记录
下面所有图片均来自自制工程(自己做一遍才能更好的了解当中的细节)
- 2023.11.16 包含8方向位移, Turn In Place,Jump, Aim
动画框架
首先要围绕几个动画类来展开(名称可能有出入)
- ABP_CharacterBase : 基础角色动画蓝图, 模型上指定的就是他
- ALI_AnimLayer: 动画Link接口, 挂载到ABP_CharacterBase 和其他Link动画蓝图类
- ABP_LinkBase: 实现Link逻辑的基类
- ABP_Link_Pistol : 派生的用于配置手枪的link类
- ABP_Link_Rifle: 同上类推
所以, 基础动画蓝图类里面是搭建好基础的状态机, 但是几乎不配置动画资源
而LInk基类里去处理每一层的逻辑
但是动画资源都是通过变量配置
然后每一个派生的Link类就负责配置这些变量
此举比较好的解决了动画蓝图不容易解耦和协作开发的缺点, 同时也非常方便后期的扩展
还需要强调的一个技术点
Lyra的动画蓝图更新都放在线程安全函数里, 所有依赖的函数都是线程安全函数(不会出现以前在极端情况下的游戏线程的属性与动画线程的竞争), 同时使用了大量的动画节点回调函数和动态参数绑定技术
基础移动部分
首先关注关键的一点, Lyra的8方向位移不依赖动画混合空间
然后需要开启目前5.3还处于beta的移动库插件
重点: 使用的是4个方向的动画序列, 动画的选择来自方向枚举的判定
比如start的动画选择在动画序列的On Become阶段
start和stop都是Sequence Evaluator, cycle是Sequence player
Lyra的start和stop动画只会选择一次, cycle动画会持续更换, 注意这个就需要用到惯性混合
这里就非常容易忘记加上Inertialization节点, 为了防止出现问题无从查找(笔者找了很长时间就是没发现为什么循环动画跳帧), 那么就在一开始就在主动画蓝图的最后面加上这个节点
归纳一下start/cycle/stop 3个动画的机制
- start
- setup : 根据方向确认动画, 设置动画时间为0
- update: 驱动Distance Matching
- cycle
- update: 根据方向刷新动画, 根据速度缩放动画速率(Distance Matching)
- stop
- setup: 同start
- stop: 预测停止距离并根据需求进行Distance Matching或者推进动画
脚步适配
一切的运动适配都是为了解决滑步的问题, 从起步到循环到停步都是如此
这里建议先看一下UE官方讲解高级动画移动技术的文档
或者笔者旧版本的笔记
因为很多地方都有点晦涩难懂, 所以还是建议在有一定了解以后再来看Lyra的这部分内容
这套技术依赖Root Motion, 意味着所有动画都必须是Root Motion动画, 包括所有的循环动画
起步和停步动画必须生成曲线信息, 在插件中已经提供了动画修改器
产生的曲线是如下图的
有几点注意
- 起步都是从0开始到最终的位移距离
- 停步都是从负值到0
这样做的目的是也是为了方便计算
起步
因为使用的是Evaluator, 时间需要我们自己推进, 所以初始化时一定不要忘记将时间设置为0(其他类似的类似)
我们需要计算出两帧之间的距离偏移, 然后将这个关键参数输入给AdvanceTimeByDistanceMatching
函数
因为推进的是时间, 所以归根结底改动的是速率, 这一步没有对步伐进行调整
而这个距离计算比较简单, 就是比较当前帧数与上一帧的位移偏差
这里为什么自己计算而不用移动组件里的信息? 因为要考虑两边的时序问题和多线程问题
然后在外面使用现在已经正式版本的两个适配节点, 用于计算身体旋转和步伐大小适配
前者后面讲, 后者就是用来弥补只有动画速率调整带来的问题,
所有Lyra的思路就是 在一定缩放动画速率的基础上加上步伐的适配, 两者结合来让脚步更好的表现
循环
循环与起步没太大的区别, 动画切换上面也提到过了
停步
停步相对复杂, 因为我们需要计算出我们停止的位置, 在插件库里封装了一个比较好用的函数(实现方式在移动组件内, 以前需要自己抄一遍出来)
原理: 根据预测的停止距离, 找到合适的动画曲线上的时间点, 将动画直接推进到这个时间点开始播放
比如剩余距离是45, 那么就在上图的时间点开始播放余下动画
这一步有个前提, 你停止的距离要小于等于动画的最大移动距离, 所以停止动画适合做的比较长一点
补充一下, 默认的第三人称角色的移动几乎是瞬间停止的, 需要调整下图几个参数保证有一定的刹车过程
效果
旋转适配
旋转适配就是之前提到的节点 Orientation Warping做的事情, 目的是解决前后左右中间的4个角度问题
因为8方向混合空间一定会存在脚步穿帮现象,
ALS的方案是为左右移动的动画各准备了2个动画分别对应左脚在前和右脚在前(无法根本上解决脚步穿模问题)
使命召唤这类大作的方案是在ALS的基础上再准备一个扭胯的过渡动画处理脚步混合问题
而Orientation Warping
做的其实就是控制中轴线的骨骼的旋转来让下半身进行一定的旋转, 讲向前动画匹配到左右45度, 向后动画匹配两个135度
这个角度计算通过已有接口就可以直接获取到, 取值范围是[-180,180]
看一下效果
回转运动
这个是比较特殊的一个机制, 很多案例里并没有这个机制的存在,
先看一下回转运动即Pivot的动画资源
上图是往右边的pivot运动, 即朝向右边运动然后急刹车往左边运动
看Distance曲线是从负值到正直的变化曲线
具体解释一下
回转运动即往一个方向运动时立马超对立方向给与加速度, 大白话就是按A向左运动的过程中立马切换成按D向右运动的短时间运动
很多时候我们不处理这个过程也无伤大雅, 毕竟没有谁一直会来回按A/D或者W/S
对比一下两者的视频
- 有pivot
- 没有pivot
下图是回转运动的状态机图
不得不提现在UE5的状态机确实非常优雅, Alias节点用的好可以避免很多蜘蛛网的产生
我们只在start和cycle状态会进入到pivot运动, 所以alias节点这样选择
至于条件, 那么就是当前速度与加速度相反的时候
然后是回转运动的状态机
这里就要解释一下了, 为什么要两个一模一样的状态机来回跳转
这个有点像有些案例里面的开火状态机, 你在这个pivot状态还没有结束的时候立马再触发一次pivot, 那么原有状态机是不能满足立马重新开始并很好的融合的
所以比较取巧的方法是用2个一样的pivot状态来回切换
重复触发的条件有两个
- 速度与加速度相反
- 当前加速度与上一次加速度相反
然后看状态机内部的实现, 初始化的时候选择动画序列, 并记录当前的加速度
刷新的逻辑稍微多一点
文字描述
- 在速度与加速度相反的时候即pivot运动的时候, 用stop的方式做Distance Matching
- 使用的预测终点的方法改成了
Predict Ground Movement Pivot Location
- 在速度与加速度同方向以后改成常规的类似start的方式处理Distance Matching
原地转身
原地转身即Turn In Place
Lyra也使用了常规手法, 即角色朝着Controller面朝方向转动, 模型用Rotation Root Bone做反向补偿, 造成角色转动而模型不转的假象
核心就是计算一个动态的RootYawOffset
变量
先看一下每一帧更新的计算逻辑
不得不提这个枚举, 一共三个成员
1 | UENUM(BlueprintType) |
解释一下3个状态的作用
- Accumulate : 累计状态, 即需要转身的状态, 下半身要进行反补的状态, 根据你鼠标的转向得出插值, 反补给模型
- Hold On: 意图是保持住, 这里没有用到, 应该是在一些情况下固定在一定角度然后再跟随鼠标移动的状态
- Blend Out: 淡出状态, 身体慢慢插值到鼠标面朝方向的状态, 即Turn In Place播放转身动画时的状态
核心就是RootYawOffset
处于Accumulate的时候的计算
首先只要是Idle状态机没有处于Blend Out的时候, 都要把状态设置成Accumulate 并且刷新RootYawOffset
的值
看实现是围绕着两条曲线进行
- RemainingTurnYaw: 旋转的角度变化
- TurnYawWeight: 转身状态的权重
曲线生成依赖Lyra项目的TurnYawAnimModifier
修改器, 需要的话要把这个修改器导出来到自己项目
角度除权重得到曲线值, 非零的时候将每一帧的变化值对RootYawOffset
进行修改
这里有个问题, 为什么是除法? 而不是乘法
然后就要看状态机
在面朝方向与角色朝向插值达到一定角度以后开始进入Turn In Place Rotation状态机
Rotation状态机与下面的Recovery状态机是同一个动画资源, 只不过上面的播放的时间决定下面的起始时间,
用 Turn In Place Rotation Time变量记录
Turn In Place Rotation状态机主要更新了这个变量
在重复达到触发转身动画的条件时会重复进入Rotation状态机
这里只有动画表现, 这个不是很难理解
看效果
瞄准
瞄准Lyra用了单独的一个叠加层来处理
计算非常简单
因为前面已经计算了为了原地转身的RootYawOffset
值, 这个反一下刚好是Aim的Yaw应该有的角度
跳跃
起跳区分了手动起跳和被动掉落
凭证是Z方向的速度
唯一需要重点看一下的是Land的状态
Land的动画也需要Distance Matching来适配动作, 但是方向不同, 他是Z方向的距离
计算得到的距离地面的距离来决定播放动画的位置
这个距离比较特殊, 用到的是非线程安全的函数, 原因是需要用到移动组件的数据, 看来也是没办法
Lyra用的方法如下
直接从移动组件拿数据, 不过如果你不想用代码, 蓝图又访问不到这个数据, 那么蓝图也有一个接口可以得到这个距离, 如下
到此跳跃自己的状态机都可以了, 但是还需要一个落地的叠加状态
重点就是落地时候的叠加动画
根据落地之前的计时来决定混合的权重, 意味着轻轻的一跳就叠加一点点, 反之更多
效果