游戏引擎的动画技术基础
June 25, 2023About 14 min
游戏引擎的动画技术基础
动画的发展史
人类在远古就有通过一系列动态来表达动画的方式。
通过图像变化来显示动画的原理是基于视觉残留。
- 视觉残留是影像会在眼里残留1/21秒,这个是游戏动画的理论支撑.

- 游戏动画的理论工具都是来源于电影行业


- 游戏中的动画技术发展
- 左图是2D动画技术变化,右图是3D动画技术变化



实现游戏动画的三大挑战
- 游戏中的动画需要接收不同的用户输入,且受环境影响 ------------ 受不同Gameplay的影响
- 游戏中的所有东西都是实时的,动画数据很大 ------------ 用于动画计算的Budget有限
- 游戏中角色的真实感和要求很高, 更多的细节和物理表现 ------------ 用于动画计算的Budget有限
游戏中的2D动画技术

- 精灵动画:把角色动画按帧记录下来,在游戏中逐帧播放。

- 早期 Doom 中的3D动画是通过2D动画实现的
- 基于各个视角采样了一系列的动作,再通过相机视角播放不同的sprite动画

- 现代游戏中精灵动画也应用于粒子效果的Texture变化中


- Live2D把一个角色上的所有元素变成小的图元,然后拼接到一起成为一个大图元。
通过各种平移,旋转等变化组合成新的动画。



- Live2D中的所有图元通过设置深度来控制Visible
- Live2D生成每个图片元的控制mesh,加入随机控制点来控制图元变化
- Live2D定义关键帧(key frame)进行插值
游戏中的3D动画技术

- DOF: 表示自由度,有多少DOF就表示可以在多少个维度变化
- 刚体在三位空间的运动自由度就是6DOF
- 平移三个自由度
- 旋转三个自由度
- 动画简单来看就是基于刚体自由度的表达

- 游戏中的3D动画,最基础的就是基于层次结构的刚体变化,比如皮影戏。
- 其实就是把角色每个关节(joint)设置为可动的节点
关节本身有树状结构,所以叫做刚体的层次动画

- 顶点动画也是游戏中常用技术
- 右上的旗子动画就是通过顶点动画实现的, 把顶点位置随时间的变化存到两张Texture中.
- Vertex Animation Texture(VAT)
- 第一张Texture的横轴表示每个顶点,纵轴表示动画每帧的顶点偏移
- 第二章Texture把顶点变化那帧的法线存储起来, 同样的横轴表示每个顶点
- 这些数据可通过物理引擎离线模拟好后存储起来
- 流水动画(右下): 也是通过顶点动画实现的

- 变形动画也是顶点动画,不同的是它是通过顶点影响权重来控制变化的
- 它会创建几种不同的Key Poses, 然后进行关键帧插值


- 蒙皮动画的原理都相同
- 每个顶点受多根骨骼影响来避免骨头穿插的问题

- 布娃娃 Ragdoll 用于模拟受击死亡, 是需要特别设计的动画, 还要展示掉落感
- 物理模拟—衣料和流体是游戏里常见的应用
- IK—反向动力学, 用于通过目标推断出当前的Pose
蒙皮动画的实现

- 刷蒙皮简单来说就是设置顶点受那些骨骼影响, 也就是设置顶点受骨骼影响的权重
- 一般动画会在Vertex shader里面去做

- 动画计算都是在各自的空间里, 通过6DOF进行不同空间的变换
- 需要注意的是Local Space是指模型上每个骨骼的局部坐标系


- 生物骨骼是由一个个有层次的节点组成,其中有一个是根节点,其他所有节点都有父亲节点。
- 类人生物的骨骼一般叫做biped(二足生物), 根节点一般是人的尾椎骨的地方。
非人四肢动物的骨骼一般叫做quadruped(四足生物),根节点也设置在Pelvis joint 尾椎骨。

- 游戏引擎里面表达骨骼是通过Joint(关节)
- 两个joint之间定义了一根实际的Bone

- 除了标准骨骼外还会根据情况添加其他骨骼
- 添加额外的骨骼支持表情和眼睛动画
添加额外的骨骼支持各种变形比如斗篷
这些都会加到角色标准骨骼上

- 骑乘和拿武器都是绑定到特定的Joint上

- 人型的Root节点会定义到两脚之间, 方便处理角色的移动和跳跃
- 比如人物手办支架到台座的点,也是Root节点。

- 绑定动画到载具时, 通过Mount Joint卡到一起,这个joint影响骑乘者的坐标变换和旋转变换
- 人和马的动画分别播放


- 每个静止的状态叫一个Pose
- APose提供更自然的站立,也避免Tpos的肩膀挤压问题
- 需要表达一个Pose时有9个自由度,包含缩放
3D旋转算法


- 二维空间的旋转轴只有1个, 三维空间的旋转轴有3个
- 三维空间里绕任意轴旋转可以用绕3个轴旋转来表达


- 欧拉角用基于三个轴的旋转角度用来描述旋转
- 欧拉角的问题
- 欧拉角依赖于旋转变换的先后顺序,即换一种绕不同轴旋转的顺序会导致最终旋转结果不同。所以一般规定必须xyz顺序。
- 欧拉角存在“退化”问题,即当固定绕一个轴旋转时,其他两个轴就退化成同一个内容了,比如绕z轴转动时,x和y的地位就相同了,此时DoF实际上只有1度了。
- 插值困难。给定两组旋转角度时,很难计算它们之间的插值结果,不能简单线性插值。
- 很难绕其他任意特定轴旋转。
四元数

- 在笛卡尔坐标系,对二维空间的任意一个旋转可以通过一个归一化的复数值 a + bi (右上)来表达。




- 上图中q是旋转的四元数描述,v’是v通过q旋转后的结果

- 利用四元数可以表示很多三维空间的旋转,包括反向旋转、旋转的叠加以及从当前方向u到指定方向v的旋转函数计算等

- 基于任意轴旋转的四元数
关节姿势的属性信息

- 旋转变化


- 平移变化


- 缩放变化用于特殊地方,比如面部缩放

- 记录为仿射矩阵(Affine Matrix)


- 动画一般存储在Local Space里面,保存了相对于Parent节点的变化
- 局部坐标系插值是球面插值,模型空间是线性插值会出现插值错误
单关节影响的蒙皮原理

- 假设某个顶点绑定到某一个Joint(只受这个骨骼影响),
无论Joint怎么动,这个顶点的新位置和Joint的相对空间关系是不变的。


- mesh中的顶点在模型坐标系下的初始坐标经过时间t后的变换矩阵Skinning Matrix K。

- 游戏引擎中会先计算好一个SkInning Matrix Palette表,并计算好各个mesh点原始坐标,
从而可以快速利用GPU在shader中计算出当前时刻 t 的mesh坐标。
Skinning Matrix K还会再在左乘世界坐标系的转换矩阵完成到世界坐标系的转换。

- 在引擎实现时会在数据结构中提前把矩阵的逆存储好
基于多关节影响的蒙皮原理

- 通过加权平均来处理顶点受到多个Joint影响
- 顶点受到Joint影响的上限一般不超过4个

- 顶点插值必须在模型空间完成,因为存在多个Joint的Local SPace影响


- 插值让动画平滑过渡




- 通过NLerp计算动画插值时需要通过点乘大于0来选择最短路径插值
- 需要注意NLERP的插值在时间不是均匀的


- SLerp 通过夹角去插值完成平滑效果
因为有反三角函数计算开销大
当夹角很小的时候会导致插值不稳定 - 当夹角小的时候就用NLerp, 大的时候切换到SLerp
简单的动画运行时管线

- 获取到大量动画的Clips
- 找到Clip的当前帧和下一帧,通过插值计算出骨架应该在的Pose
- 存储到Model Space里面
- 每根骨骼转换表达为它的Skinning Matrix Palette,这里就从CPU计算转换到GPU计算
- 随着硬件的进步,前面的一部分计算也逐渐放到GPU中进行
动画压缩 Animation Compression


- 可以看到动画要存储的数据非常大



- 动画变化时并不会变换所有的DOF, 通过锁定基本不变换的DOF,可以降低存储数据的量


- 通过只记录关键帧缩小动画数据总量


- 通过曲线来逼近旋转变换,只需要4个点进行多项值计算
- 这个计算是离线的


- 浮点需要占据4个字节,可以通过定点数来模拟
- 很多时候不需要32位浮点数精度,所以可以将数值压缩到0-1之间然后乘以65535用16位整数去表示。

- 具体的,针对四位数Quaternion,利用其特性(四元可以用其他三个较小的值去表示较大值,因而实际可以这样存储:


- 通过压缩后的数据变化

- 压缩虽然好用,但会带来误差累积问题。所以需要检测误差是不是被控制在可接受范围内。



- 最实际可用的误差是visual error视觉误差,但如果对比压缩前后每顶点的误差则计算量太大,所以实际中针对每一个节点joint进行估计:


- Error Compensation:在多帧误差累积后反向进行偏移来补偿。其问题就是这一帧和上一帧会很不连贯。
- 对节点估计两个互相垂直方向上的点,距离设置为offset(如果精度敏感则offset大一点,如果不敏感或小骨骼的话就offset小一点),这样只需要比较两个点在压缩前后的error即可.
动画制作过程







