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: 同上类推

所以, 基础动画蓝图类里面是搭建好基础的状态机, 但是几乎不配置动画资源

image-20231116160139071

image-20231116160146096

而LInk基类里去处理每一层的逻辑

image-20231116160215057

但是动画资源都是通过变量配置

然后每一个派生的Link类就负责配置这些变量

image-20231116160450502

此举比较好的解决了动画蓝图不容易解耦和协作开发的缺点, 同时也非常方便后期的扩展

还需要强调的一个技术点

Lyra的动画蓝图更新都放在线程安全函数里, 所有依赖的函数都是线程安全函数(不会出现以前在极端情况下的游戏线程的属性与动画线程的竞争), 同时使用了大量的动画节点回调函数和动态参数绑定技术

image-20231116163929946

image-20231116163830409

基础移动部分

首先关注关键的一点, Lyra的8方向位移不依赖动画混合空间

然后需要开启目前5.3还处于beta的移动库插件

image-20231116163538283

重点: 使用的是4个方向的动画序列, 动画的选择来自方向枚举的判定

比如start的动画选择在动画序列的On Become阶段

start和stop都是Sequence Evaluator, cycle是Sequence player

image-20231116162006754

image-20231116161955623

Lyra的start和stop动画只会选择一次, cycle动画会持续更换, 注意这个就需要用到惯性混合

image-20231116162134218

这里就非常容易忘记加上Inertialization节点, 为了防止出现问题无从查找(笔者找了很长时间就是没发现为什么循环动画跳帧), 那么就在一开始就在主动画蓝图的最后面加上这个节点

image-20231116162300701

归纳一下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动画, 包括所有的循环动画

起步和停步动画必须生成曲线信息, 在插件中已经提供了动画修改器

image-20231116170601868

产生的曲线是如下图的

image-20231116170642359

有几点注意

  • 起步都是从0开始到最终的位移距离
  • 停步都是从负值到0

这样做的目的是也是为了方便计算

起步

因为使用的是Evaluator, 时间需要我们自己推进, 所以初始化时一定不要忘记将时间设置为0(其他类似的类似)

我们需要计算出两帧之间的距离偏移, 然后将这个关键参数输入给AdvanceTimeByDistanceMatching函数

image-20231116171608419

因为推进的是时间, 所以归根结底改动的是速率, 这一步没有对步伐进行调整

而这个距离计算比较简单, 就是比较当前帧数与上一帧的位移偏差

image-20231116171354361

这里为什么自己计算而不用移动组件里的信息? 因为要考虑两边的时序问题和多线程问题

image-20231116171412579

然后在外面使用现在已经正式版本的两个适配节点, 用于计算身体旋转和步伐大小适配

前者后面讲, 后者就是用来弥补只有动画速率调整带来的问题,

所有Lyra的思路就是 在一定缩放动画速率的基础上加上步伐的适配, 两者结合来让脚步更好的表现

循环

image-20231116171642367

image-20231116171637252

循环与起步没太大的区别, 动画切换上面也提到过了

停步

停步相对复杂, 因为我们需要计算出我们停止的位置, 在插件库里封装了一个比较好用的函数(实现方式在移动组件内, 以前需要自己抄一遍出来)

image-20231116171915158

原理: 根据预测的停止距离, 找到合适的动画曲线上的时间点, 将动画直接推进到这个时间点开始播放

image-20231116172241223

比如剩余距离是45, 那么就在上图的时间点开始播放余下动画

这一步有个前提, 你停止的距离要小于等于动画的最大移动距离, 所以停止动画适合做的比较长一点

补充一下, 默认的第三人称角色的移动几乎是瞬间停止的, 需要调整下图几个参数保证有一定的刹车过程

image-20231116172537227

效果

stop1

旋转适配

image-20231116173938294

旋转适配就是之前提到的节点 Orientation Warping做的事情, 目的是解决前后左右中间的4个角度问题

因为8方向混合空间一定会存在脚步穿帮现象,

ALS的方案是为左右移动的动画各准备了2个动画分别对应左脚在前和右脚在前(无法根本上解决脚步穿模问题)

使命召唤这类大作的方案是在ALS的基础上再准备一个扭胯的过渡动画处理脚步混合问题

Orientation Warping做的其实就是控制中轴线的骨骼的旋转来让下半身进行一定的旋转, 讲向前动画匹配到左右45度, 向后动画匹配两个135度

这个角度计算通过已有接口就可以直接获取到, 取值范围是[-180,180]

image-20231116173852513

看一下效果

8dire

回转运动

这个是比较特殊的一个机制, 很多案例里并没有这个机制的存在,

先看一下回转运动即Pivot的动画资源

pivot_seq

上图是往右边的pivot运动, 即朝向右边运动然后急刹车往左边运动

image-20231116194200228

看Distance曲线是从负值到正直的变化曲线

具体解释一下

回转运动即往一个方向运动时立马超对立方向给与加速度, 大白话就是按A向左运动的过程中立马切换成按D向右运动的短时间运动

很多时候我们不处理这个过程也无伤大雅, 毕竟没有谁一直会来回按A/D或者W/S

对比一下两者的视频

  • 有pivot

pivot

  • 没有pivot

pivot_no

下图是回转运动的状态机图

image-20231116192318701

不得不提现在UE5的状态机确实非常优雅, Alias节点用的好可以避免很多蜘蛛网的产生

我们只在start和cycle状态会进入到pivot运动, 所以alias节点这样选择

image-20231116192412289

至于条件, 那么就是当前速度与加速度相反的时候

然后是回转运动的状态机

image-20231116192504953

这里就要解释一下了, 为什么要两个一模一样的状态机来回跳转

这个有点像有些案例里面的开火状态机, 你在这个pivot状态还没有结束的时候立马再触发一次pivot, 那么原有状态机是不能满足立马重新开始并很好的融合的

所以比较取巧的方法是用2个一样的pivot状态来回切换

重复触发的条件有两个

  • 速度与加速度相反
  • 当前加速度与上一次加速度相反

然后看状态机内部的实现, 初始化的时候选择动画序列, 并记录当前的加速度

image-20231116192752428

刷新的逻辑稍微多一点

image-20231116192824998

文字描述

  • 在速度与加速度相反的时候即pivot运动的时候, 用stop的方式做Distance Matching
  • 使用的预测终点的方法改成了 Predict Ground Movement Pivot Location
  • 在速度与加速度同方向以后改成常规的类似start的方式处理Distance Matching

原地转身

原地转身即Turn In Place

Lyra也使用了常规手法, 即角色朝着Controller面朝方向转动, 模型用Rotation Root Bone做反向补偿, 造成角色转动而模型不转的假象

核心就是计算一个动态的RootYawOffset变量

先看一下每一帧更新的计算逻辑

image-20231116195419064

不得不提这个枚举, 一共三个成员

1
2
3
4
5
6
7
UENUM(BlueprintType)
enum class ERsAnimTurnInPlaceRootOffsetMode: uint8
{
Accumulate = 0,
HoldOn,
BlendOut
};

解释一下3个状态的作用

  • Accumulate : 累计状态, 即需要转身的状态, 下半身要进行反补的状态, 根据你鼠标的转向得出插值, 反补给模型
  • Hold On: 意图是保持住, 这里没有用到, 应该是在一些情况下固定在一定角度然后再跟随鼠标移动的状态
  • Blend Out: 淡出状态, 身体慢慢插值到鼠标面朝方向的状态, 即Turn In Place播放转身动画时的状态

核心就是RootYawOffset处于Accumulate的时候的计算

首先只要是Idle状态机没有处于Blend Out的时候, 都要把状态设置成Accumulate 并且刷新RootYawOffset的值

image-20231116200935967

看实现是围绕着两条曲线进行

image-20231116202126967

  • RemainingTurnYaw: 旋转的角度变化
  • TurnYawWeight: 转身状态的权重

曲线生成依赖Lyra项目的TurnYawAnimModifier修改器, 需要的话要把这个修改器导出来到自己项目

角度除权重得到曲线值, 非零的时候将每一帧的变化值对RootYawOffset进行修改

这里有个问题, 为什么是除法? 而不是乘法

然后就要看状态机

image-20231116200001186

在面朝方向与角色朝向插值达到一定角度以后开始进入Turn In Place Rotation状态机

Rotation状态机与下面的Recovery状态机是同一个动画资源, 只不过上面的播放的时间决定下面的起始时间,

用 Turn In Place Rotation Time变量记录

image-20231116200610293

image-20231116200106900

Turn In Place Rotation状态机主要更新了这个变量

在重复达到触发转身动画的条件时会重复进入Rotation状态机

这里只有动画表现, 这个不是很难理解

看效果turn

瞄准

image-20231116203706745

瞄准Lyra用了单独的一个叠加层来处理

计算非常简单

image-20231116203752264

因为前面已经计算了为了原地转身的RootYawOffset值, 这个反一下刚好是Aim的Yaw应该有的角度

跳跃

image-20231116204604030

起跳区分了手动起跳和被动掉落

凭证是Z方向的速度

唯一需要重点看一下的是Land的状态

Land的动画也需要Distance Matching来适配动作, 但是方向不同, 他是Z方向的距离

计算得到的距离地面的距离来决定播放动画的位置

image-20231116204817259

这个距离比较特殊, 用到的是非线程安全的函数, 原因是需要用到移动组件的数据, 看来也是没办法

Lyra用的方法如下

image-20231116204949461

直接从移动组件拿数据, 不过如果你不想用代码, 蓝图又访问不到这个数据, 那么蓝图也有一个接口可以得到这个距离, 如下

image-20231116204830867

到此跳跃自己的状态机都可以了, 但是还需要一个落地的叠加状态

image-20231116205137143

image-20231116205145246

重点就是落地时候的叠加动画

image-20231116205243423

image-20231116205417818

根据落地之前的计时来决定混合的权重, 意味着轻轻的一跳就叠加一点点, 反之更多

效果

jump