动画系统

角色动画的类型

  • 赛璐璐动画:手绘动画
  • 刚性层阶式动画:角色由一堆刚性部分建模,这些刚性部分以层阶式彼此约束。最大的问题:角色的身体在关节的位置产生碍眼的“裂缝”。
  • 每顶点动画技术:存储随时间改变的顶点位置和法线。它是数据密集的技术,因为每个顶点随时间改变的动作信息都需要存储下来。因此实时游戏中很少使用。
  • 变形目标动画: 移动网格顶点,仅制作相对少量的固定极端姿势,然后运行时混合姿势,线性差值(LERP),常用于面部动画。
  • 蒙皮动画:含有前两个技术的优点,允许组成网格的三角形变形,同时具有刚性层阶式动画的高效性和内存使用量。

把动画方法视为数据压缩技术,对权衡各种动画技术有所帮助。一般来说,我们选择动画技术的目标是能提供最佳压缩又不会产生不能接受的视觉瑕疵。

骨骼

游戏引擎不在意骨头,只在乎关节。

骨骼层阶结构

骨骼的关节形成层阶结构,即树形结构。选择一个关节为根,其他为它的子孙。每个关节仅有一个父关节,因此,在每个关节中存储父关节的索引,根关节的父关节索引为-1。

内存中表示骨骼

骨骼有细小的顶层数据结构表示,它含有关节数组。关节的存储次序保证每个子关节在父关节之后,则根关节位于数组首位。

每个关节的数据结构包含下面信息:

  • 关节名字:字符串或32位字符串散列标识符。
  • 骨骼中的父节点索引。
  • 关节的绑定姿势的逆变换。关节的绑定姿势指蒙皮网格顶点绑定至骨骼时,关节的位置、定向及缩放。

骨骼的数据结构:

struct Joint
{
    Matrix4x3   m_invBindPose;//绑定姿势之逆变换
    const char* m_name;       //关节名字
    U8          m_iParent;    //父索引,或0xFF代表根关节
};

struct Skeleton
{
    U32       m_jointCount;//关节数目
    Joint*    m_aJoint;    //关节数组
};

姿势

绑定姿势

这是三维网格绑定至骨骼之前的姿势,即把网格当做正常、未蒙皮、完全不涉及骨骼的三角形网格渲染的姿势。又称为参考姿势、放松姿势、T姿势。

局部姿势

关节姿势最常见的是相对于父关节来指定。相对父关节的姿势能令关节自然的移动。有时用局部姿势描述相对父的姿势。局部姿势几乎都是存为SQT格式。

数学上,关节姿势就是一个仿射变换。

  第j个关节姿势Pj可由平移矩阵Tj、放缩矩阵Sj、旋转矩阵Rj构成;整个骨骼姿势Pskel写成所有姿势Pj的集合。

关节缩放

有的引擎不允许缩放,Sj就是单位矩阵;有的引擎只允许统一缩放,Sj就是一个标量。统一缩放有利于平截头体剔除和碰撞检测。

struct JointPose
{
    Quaternion  m_rot;    //Q
    Vector3     m_trans;  //T
    F32         m_scale;  //S(仅为统一缩放)
};

struct JointPose
{
    Quaternion  m_rot;    //Q
    Vector3     m_trans;  //T
    Vector3     m_scale;  //S
    U8          m_padding[8];
};

struct SkeletonPose
{
    Skeleton*  m_pSkeleton;//骨骼 + 关节数量
    JointPose* m_aLocalPose;//多个局部关节姿势,动态分配
};

当把关节姿势变换Pj施于以关节j坐标系表示的点或矢量时,其变换结果是以父关节空间表示的点和矢量。这种从子关节空间变换到父关节空间的变换矩阵写成(PC->P)j或者Pj->p(j)

全局姿势

把关节姿势表示为模型空间或世界空间。这称为全局姿势。某关节的模型空间姿势(j->M),可通过从该关节遍历到根关节时,在每个关节乘上其局部姿势(j->p(j))算出。

任何关节j的全局姿势可写成:

struct SkeletonPose
{
    Skeleton*  m_pSkeleton;//骨骼 + 关节数量
    JointPose* m_aLocalPose;//多个局部关节姿势,动态分配
    Matrix44*  m_aGlobalPose;//多个全局关节姿势
};

动画片段

游戏角色的移动必须拆分成大量小粒度的动作。这些动作为动画片段。一般一个角色动作会拆成上千个片段,但是当角色进入游戏的非互动部分是例外。这部分称为非交互连续镜头(NIS)或全动视频(FMV)。

局部时间线

每个动画片段各自有一条局部时间线,使用自变量t表示时间线。t的每个值为时间索引。

片段中一些时间点上有一些重要的姿势,这些姿势称为关键姿势关键帧,然后计算机会采用线性或基于曲线的插值计算中间的姿势。动画片段的时间是连续的,在计算机中时间变量是实数(浮点数)而非整数。这样可以有足够的分辨率度量时间,而且可以计算帧之间的结果或改变动画播放速率。而且它的比例是可变的。典型的帧持续时间是1/30s或1/60s。

动画片段中的一个时间点称为一个采样。采样的数目和帧数目的关系:

  • 若片段非循环,N个帧的动画有N+1个采样;
  • 若片段是循环,N个帧的动画有N个采样,最后一个采样是冗余的。

归一化时间有时称为动画的相位。归一化的时间单位u的范围始终是[0,1];当动画在循环时,u有如正弦波的相位。当要同步两个或以上的动画片段,而它们的持续时间有不同,归一化时间就很适合。

全局时间线

游戏中每个角色的全局时间线是从角色在游戏世界中诞生开始。播放一个动画可以看成动画的局部时间映射到角色的全局时间中。可以通过天正时间比例,把片段播放的更快或更慢。只需要把片段映射到全局时间线之前缩放它的比例即可。这称为播放速率R(片段缩小一半时,R = 2;R = -1时,片段倒转播放)。

动画片段映射到全局时间线和下面的相关:

  • 全局起始时间τstart
  • 播放速率R
  • 持续时间T
  • 循环次数N

局部时间t和全局时间τ的映射关系:t = R(τ - τstart)    τ = τstart + t/R

  • 若动画是非循环的(N = 1),应该吧t裁剪到合法的范围[0,T]中:t = clamp[R(τ - τstart)]|0T
  • 若动画是无限循环(N = ∞),则  t = (R(τ - τstart))modT
  • 若动画是有限循环(1 < N < ∞),则  t = (clamp[R(τ - τstart)]|0NT)modT

若要使用局部时间同步动画,必须保证在完全相同的游戏时间播放它们。实际上这是很困难的,因为动画播放命令可能来自多个不同的子系统,要保证它们的完全同步非常困难。

若使用全局时间同步动画,只需要片段的全局开始时间数值上相同,它们的播放就是完全同步的,若片段的播放速率相同,则会已知同步下去。

动画数据格式

一些关节姿势通常存储为SQT格式。动画的表示方法:

struct JointPose{...};//前面已定义了SQT

struct AnimationSample
{
    JointPose* m_aJointPose;//关节姿势数组
};

struct AnimationClip
{
    Skeleton*        m_pSkeleton;//由于每个动画片段是为特定的骨骼而设计的,这个可能是骨骼标识,而不是指针
    F32              m_framesPerSecond;
    U32              m_frameCount;
    AnimationSample* m_aSamples;//采样数组
    bool             m_isLooping;
};

连续通道函数

动画的采样可看做随时间而改变的连续函数;对于采样间的值,许多游戏引擎采用线性插值,这实际上是对连续进行分段线性逼近

许多游戏允许在动画中加入额外的“元通道”数据。常见的是在多个时间点上存储事件触发器,当动画的局部时间索引进过这些触发器,触发器的事件就会交到游戏引擎,游戏引擎处理这些事件。事件触发器常用于记录在动画的某个时间点播放音效(脚触地时播放脚步声),或粒子效果。另一种是名为定位器的特殊关节,它和骨骼关节一起设置动画。典型的可以吧摄像机和定位器绑定,这样就能得到位置和角度。

蒙皮及生成矩阵调色板

把三维网格顶点联系至骨骼的过称称为蒙皮。蒙皮用的网格是通过其顶点联系上骨骼的。每个顶点可绑定到一个或多个关节。若绑定至多个顶点,该顶点的位置为把它绑定到单一关节后的位置,再求其加权平均。

每个顶点需要信息:

  • 该顶点绑定的(一个或多个)关节的索引;
  • 对于绑定的每个关节,提供一个权重因子。

游戏引擎会限制每个顶点能绑定的关节数目,典型的限制为每顶点4个关节。
原因如下:首先,4个8位关节索引能方便地包裹为一个32位字,此外,每顶点使用2个、3个及4个关节所产生的质量很容易区分,但大多数人并不能分辨出每个顶点4个关节以上的质量差别。

典型的蒙皮顶点数据结构

蒙皮矩阵

以矩阵Bj->M表示关节j在模型空间的绑定姿势,即此矩阵把点或矢量从关节j的空间变换至模型空间;

在绑定姿势时,该顶点的模型空间位置为VMB,则把此顶点变换至关节j的空间:VjVMBBM->jVMB(BM->j)-1

蒙皮过称要计算的该顶点在当前姿势的模型空间的位置VMC = VjCj->MVMB(BM->j)-1Cj->M

Kj = 绑定姿势矩阵的逆矩阵 * 当前姿势矩阵 = (BM->j)-1Cj->M

矩阵调色板(Matrix palette)

我们须计算一组蒙皮矩阵Kj,当中每个矩阵对应第j个关节。此数组称为矩阵调色板。

当要渲染一个蒙皮网格时,矩阵调色板便要传送至渲染引擎。渲染器会为每个顶点查找调色板中合适的关节蒙皮矩阵,并用该矩阵把顶点从绑定姿势变换至当前姿势。

假设角色的姿势随时间改变,其当前姿势矩阵便需要每帧更新。然而,绑定姿势逆矩阵在整个游戏中都是常量,因为骨骼的绑定姿势是模型创建时确定下来的。因此绑定姿势逆矩阵通常会缓存于骨骼,并不需要在运行时计算。

每个顶点最终会由模型空间变换至世界空间。因此有些引擎会把蒙皮矩阵调色板预先乘以物体的模型-世界变换。这是个很有用的优化。(Kj)W = (BM->j)-1Cj->MMM->W

顶点蒙皮至多个关节,求加权平均(wij表示权重):

动画混合(animation blending)

指能令一个以上的动画片段对角色最终姿势起作用的技术。混合是把两个或更多的输入姿势结合,产生骨骼的输出姿势。比如,通过混合负伤及无负伤的步行动画,我们可以生成二者之间不同负伤程度的步行动画。动画混合可用于对面部表情、身体站姿、运动模式等的极端姿势之间插值。

动画混合也可以用于求出不同时间点的两个已知姿势之间的姿势。通过在短时间段内把来源动画逐渐混合至目标动画,就能把某动画圆滑地过渡至另一动画。

线性混合插值

将两个骨骼姿势,通过线性插值找到它们中间姿势

则两个姿势中每个关节的局部姿势线性插值: 当整个骨骼插值后的姿势:

其中β为混合百分比混合因子;β∈[0,1]。至于对每个关节姿势的矩阵插值的处理方法,可以对它的SQT格式进行插值,这在前面数学基础中讲到。

线性插值混合的应用

时间性混合,即β的值和时间相关,例如要求Δt和2Δt之间的2.18Δt的时间处的姿势,可以求β = 0.18的线性插值。因此,要求时间点t1和t2之间的某个姿势采样,β可以这样求得:

动作连续性,当动画从一个片段过渡到另一个片段经常出现跳帧的问题。

为了在不同的动画片段之间平滑过渡,这里有三种级别的动画连续性:

  • 位置连续 C0连续(continuity):骨骼中每个关节移动时描绘的三维路径不含突然的“跳跃”。
  • 速度连续(位置的导数) C1连续,速度及动量的连续性:路径的第一导数也是连续的。
  • 加速度连续(速度的导数)C2连续

若使用更高阶的连续性,角色的动作会显得更佳及更真实。但是,通常难以达到严格数学上的C1或以上的连续性。通常使用线性插值(LERP)达到C0动作连续性就可以了。LERP混合即成为淡入/淡出。

两种常见的淡入淡出过渡方法

  • 圆滑过渡(smooth transition):播放片段A和B的同时把β从0增至1。这种做法要求两片段都是循环动画,且两片段同步到手脚位置大致匹配。
  • 冻结过渡(frozen transition):片段A的局部时钟停顿于片段B开始播放时。它适合混合两个不相关且不能再时间上同步的片段。

β 线性差值中的混合因子

为达到圆滑的过渡,我们可以令β按时间的三次函数变化,例如用一维贝塞尔曲线。当把这些曲线应用正在淡出的当前片段时,该曲线就称为缓出曲线(ease-out curve);当应用到正在淡入的新片段时,称为缓入曲线(ease-in curve)。

Bezier曲线如下:βstart为混合之始tstart的混合因子,βend为时间tend的最终混合因子,参数u是tstart和tend之间的归一化时间,v = 1 - u。

无需混合就能产生连续动作的方法:

动画师确保其片段的最后姿势能匹配后续片段的首个姿势。这种姿势称为核心姿势。具体的做法,创作一段圆滑的动画,然后把它切为两个或两个以上的动画片段。

方向性运动

  • 靶向移动:(有助于理解unity中的混合树(Blend Tree))

动画师制作3中不同的循环动画片段,包括向前、向左和向右移动,这些称为方向性运动片段。把它们排在半圆周上,分别对应0o、90o、-90o。角色面向0o方向,然后选择要求的角度的两个相邻的片段使用LERP方式混合,β是移动角度和相邻片段的角度求得。

  • 轴转移动

简单的播放向前运动的动画片段,同时以垂直轴旋转整个角色。

复杂的线性插值混合

泛化的一维线性插值混合中,可以定义一个参数b,它的范围任意,但是所有片段对应于该参数的某点上。(上面靶向移动的混合因子就是他的一个特例,b∈[-90o,90o])这样就可以通过b求出β:

简单的二维线性插值混合中,b变成了二维混合矢量b = [bx  by]。若b位于4个片段包含的正方形中:

  • 利用水平混合因子bx求出两个中间姿势,一个位于顶边的两个片段之间,一个位于底边的两个片段之间。它们可以通过一维LERP混合求得。
  • 然后利用垂直混合因子by,把两个中间姿势用一维LERP求得最终姿势。

若是3个片段,可以简单的使用三个权重:α、β、γ;α + β + γ = 1。

通过下面的公式求得它的姿势:

使用Delaunay三角剖分(Delaunay triangulation)可以得到泛化的二维线性插值混合。详细:http://en.wikipedia.org/wiki/Delaunay_trianglulation

骨骼分部混合

人可独立控制身体不同部位。例如,可以在步行时挥动右臂,并同时令左臂指着某物。在游戏中实现这种动作的方法之一是,使用名为骨骼分部混合(partial-skeleton blending)的技术。

混合遮罩(blend mask)可以把某些关节的混合百分比设为0,来掩盖那些关节。

现实中,在跑步中挥手时,挥手动作比站立时更“晃动”及不受控制,骨骼分部混合无法实现这样的真实性。另一种更自然的技术:加法混合。

加法混合(additive blending)

区别片段(difference clip)- 代表两段正常动画的区别。
考虑两个输入片段 来源片段(source clip S)、参考片段(reference clip R);概念上,区别片段 D = S – R
若区别片段D加进原来的参考片段,我们就会得到来源片段。只需要把某百分比的D加进R,我们也可以产生介于R和S之间的动画。就像线性插值。加法混合技术之美在于:制作一个区别片段之后,可以把该片段加进其他不相关的片段,而不仅限于原来的参考片段。

实际的数学公式,关节j的区别片段:   产生新的姿势Aj

仅当输入片段S和R的持续时间相同,才能得到它们的区别动画。也可以加上加法混合百分比,修改两者的权重:

加法混合的局限

  • 在参考片段中,尽量减少髋关节的旋转;
  • 在参考片段中,肩及肘关节应该一直维持中性姿势;
  • 动画师应为每个核心姿势(站立、蹲下、躺下等)创建新的区别动画。

加法混合的应用

  • 站姿变化
  • 移动噪声
  • 瞄准及注视

动画后期处理(animation post-processing)

通过上面的混合之后,通常在渲染前还要修改姿势,这称为动画后期处理。

  • 程序式动画:指任何在运行时生成的动画,这些动画并非由动画工具导出的数据驱动。
  • 逆运动学:与一般的从一组局部姿势推出一个全局姿势相反,逆运动学的输入是某关节想要的全局姿势,此输入称为末端受动器,要求出其他关节的局部姿势。
  • 布娃娃物理:它是由一组物理模拟的刚体,这些刚体彼此受限于角色的关节位置,这些受限方式要设置成能产生自然的“无生气”身体移动。

压缩技术

通道省略:省略无关的通道。多数角色不需要非统一放缩,因此三个放缩通道减少成一个;人形角色的骨头不能伸缩,因此关节的平移通道也可以省略。

量化:动画片段经常不需要32为浮点数的精度,16位编码的精度足够。因此,将32位浮点数的量化成整数表示法,或者把整数解码为浮点数(损失精度)。

U32 CompressUnitFloatRL(F32 unitFloat, U32 nBits)
{
    //基于要求的输出位数,判断区间数量
    U32 nIntervals = 1u << nBits;
    
    //把输入值从[0,1]范围放缩至[0,nIntervals - 1]范围
    //这里需要减一是由于希望最大的输出值能存储在nBits个位内
    F32 scaled = unitFloat * (F32)(nIntervals - 1u);
    
    //最后,我们需要加0.5f,在四舍五入至最近的区间中点
    //然后,把该值截尾,取得区间索引(通过转型至U32)
    U32 rounded = (U32)(scaled + 0.5f);
    
    //为无效输入值做出保护
    if (rounded > nIntervals - 1u)
        rounded = nIntervals - 1u;
    
    return rounded;
}

F32 DecompressUnitFloatRL(U32 quantized, U32 nBits)
{
    //基于编码时的位数,判断区间数量
    U32 nIntervals = 1u << nBits;
    
    //解码时只需简单的把U32转为F32,并按区间大小缩放
    F32 intervalSize = 1.0f / (F32)(nIntervals - 1u);
    
    F32 approxUnitFloat = (F32)quantized * intervalSize;
    
    return approxUnitFloat;
}

//处理任意在[min,max]范围内的输入值,可以使用下面的函数
U32 CompressFloatRL(F32 value, F32 min, F32 max, U32 nBits)
{
    F32 unitFloat = (value - min) / (max - min);
    U32 quantized = CompressUnitFloatRL(unitFloat, nBits);
    return quantized;
}

F32 DecompressFloatRL(U32 quantized, F32 min, F32 max, U32 nBits)
{
    F32 unitFloat = DecompressUnitFloatRL(quantized, nBits);
    F32 value = min + (unitFloat * (max - min));
    return value;
}

如果要把32位压缩为16位

inline U16 CompressRotationChannel(F32 qx)
{
    return (U16)CompressFloatRL(qx, -1.0f, 1.0f, 16u);
}

inline F32 CompressRotationChannel(U16 qx)
{
    return (U16)DecompressFloatRL((U32)qx, -1.0f, 1.0f, 16u);
}

降低整体的采样率和省略一些样本也是压缩数据的方法

多数游戏不需要所有动画同时载入内存,只会在游戏开始时载入一组核心动画片段。

动画系统架构

  • 动画管道 animation pipeline
  • 动作状态机 action state machine, ASM:游戏角色的动作通常建模为有限状态机,这称为动作状态机
  • 动画控制器 animation controller:许多引擎中,玩家和非玩家角色的行为最终由动画控制器所组成的高级系统控制。

动画管道

它包含下面的阶段:

  • 片段解压及姿势提取
  • 姿势混合
  • 全局姿势生成
  • 后期处理
  • 重新计算全局姿势
  • 矩阵调色板生成

游戏中每个个别角色或物体有其每实体数据结构,但相同类型的角色或物体会共享一组资源数据。

  • 骨骼
  • 蒙皮网络
  • 动画片段

还没有统一个方法表示每实例数据,但是几乎所有动画引擎都有下面记录:

  • 片段状态
    • 局部时钟
    • 播放速率
  • 混合规格:描述哪些动画片段正在播放,以及这些片段如何混合在一起。
  • 分部骨骼关节权重
  • 局部姿势
  • 全局姿势
  • 矩阵调色盘

扁平加权平均混合表示法

N维的加权平均,淡入淡出时,比较复杂。细节省略。

混合树

将片段桉树形结果混合,淡入淡出时,很简单。细节省略。

动作状态机

动画状态

ASM中每个状态对应一个任意复杂的动画片段混合。在混合树的架构中,每个状态对应至某个预先定义的混合树。在扁平加权平均架构中,一个状态代表一组片段及一组相对权重

过渡

ASM中状态的过渡,需要这些信息:

  • 来源及目标状态
  • 过渡类型:即时过渡、淡入淡出、其他
  • 持续时间
  • 缓入/缓出曲线类型
  • 过渡窗口:某些过渡只能在来源动画的局部时间位于某个窗口内才能进行。

状态层

控制参数

约束

  • 依附
  • 参考定位器
  • 抓取及手部IK
  • 动作提取及脚部IK
  • 注视(look-at):角色能注视环境中的兴趣点。角色可以仅用眼睛注视,又或者同时用眼和头,又或加入上半身的扭动。注视约束有时候是以IK或程序式关节偏移实现的,但更自然的观感可用加法混合实现。
  • 掩护对准(cover registration):角色在掩护时要和掩护物完美对齐,这通常是用参考定位器技术来实现。
  • 进入及离开掩护
  • 通行协助