动画系统数组
角色动画的类型缓存
把动画方法视为数据压缩技术,对权衡各类动画技术有所帮助。通常来讲,咱们选择动画技术的目标是能提供最佳压缩又不会产生不能接受的视觉瑕疵。网络
骨骼数据结构
游戏引擎不在乎骨头,只在意关节。架构
骨骼层阶结构app
骨骼的关节造成层阶结构,即树形结构。选择一个关节为根,其余为它的子孙。每一个关节仅有一个父关节,所以,在每一个关节中存储父关节的索引,根关节的父关节索引为-1。函数
内存中表示骨骼工具
骨骼有细小的顶层数据结构表示,它含有关节数组。关节的存储次序保证每一个子关节在父关节以后,则根关节位于数组首位。oop
每一个关节的数据结构包含下面信息:post
骨骼的数据结构:
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。
动画片断中的一个时间点称为一个采样。采样的数目和帧数目的关系:
归一化时间有时称为动画的相位。归一化的时间单位u的范围始终是[0,1];当动画在循环时,u有如正弦波的相位。当要同步两个或以上的动画片断,而它们的持续时间有不一样,归一化时间就很适合。
全局时间线
游戏中每一个角色的全局时间线是从角色在游戏世界中诞生开始。播放一个动画能够当作动画的局部时间映射到角色的全局时间中。能够经过天正时间比例,把片断播放的更快或更慢。只须要把片断映射到全局时间线以前缩放它的比例便可。这称为播放速率R(片断缩小一半时,R = 2;R = -1时,片断倒转播放)。
动画片断映射到全局时间线和下面的相关:
局部时间t和全局时间τ的映射关系:t = R(τ - τstart) τ = τstart + t/R
若要使用局部时间同步动画,必须保证在彻底相同的游戏时间播放它们。实际上这是很困难的,由于动画播放命令可能来自多个不一样的子系统,要保证它们的彻底同步很是困难。
若使用全局时间同步动画,只须要片断的全局开始时间数值上相同,它们的播放就是彻底同步的,若片断的播放速率相同,则会已知同步下去。
动画数据格式
一些关节姿式一般存储为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的空间:Vj = VMBBM->j = VMB(BM->j)-1
蒙皮过称要计算的该顶点在当前姿式的模型空间的位置VMC = VjCj->M = VMB(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之间的某个姿式采样,β能够这样求得:
动做连续性,当动画从一个片断过渡到另外一个片断常常出现跳帧的问题。
为了在不一样的动画片断之间平滑过渡,这里有三种级别的动画连续性:
若使用更高阶的连续性,角色的动做会显得更佳及更真实。可是,一般难以达到严格数学上的C1或以上的连续性。一般使用线性插值(LERP)达到C0动做连续性就能够了。LERP混合即成为淡入/淡出。
两种常见的淡入淡出过渡方法
β 线性差值中的混合因子
为达到圆滑的过渡,咱们能够令β按时间的三次函数变化,例如用一维贝塞尔曲线。当把这些曲线应用正在淡出的当前片断时,该曲线就称为缓出曲线(ease-out curve);当应用到正在淡入的新片断时,称为缓入曲线(ease-in curve)。
Bezier曲线以下:βstart为混合之始tstart的混合因子,βend为时间tend的最终混合因子,参数u是tstart和tend之间的归一化时间,v = 1 - u。
无需混合就能产生连续动做的方法:
动画师确保其片断的最后姿式能匹配后续片断的首个姿式。这种姿式称为核心姿式。具体的作法,创做一段圆滑的动画,而后把它切为两个或两个以上的动画片断。
方向性运动
动画师制做3中不一样的循环动画片断,包括向前、向左和向右移动,这些称为方向性运动片断。把它们排在半圆周上,分别对应0o、90o、-90o。角色面向0o方向,而后选择要求的角度的两个相邻的片断使用LERP方式混合,β是移动角度和相邻片断的角度求得。
简单的播放向前运动的动画片断,同时以垂直轴旋转整个角色。
复杂的线性插值混合
泛化的一维线性插值混合中,能够定义一个参数b,它的范围任意,可是全部片断对应于该参数的某点上。(上面靶向移动的混合因子就是他的一个特例,b∈[-90o,90o])这样就能够经过b求出β:
简单的二维线性插值混合中,b变成了二维混合矢量b = [bx by]。若b位于4个片断包含的正方形中:
如果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); }
下降总体的采样率和省略一些样本也是压缩数据的方法
多数游戏不须要全部动画同时载入内存,只会在游戏开始时载入一组核心动画片断。
动画系统架构
动画管道
它包含下面的阶段:
游戏中每一个个别角色或物体有其每实体数据结构,但相同类型的角色或物体会共享一组资源数据。
尚未统一个方法表示每实例数据,可是几乎全部动画引擎都有下面记录:
扁平加权平均混合表示法
N维的加权平均,淡入淡出时,比较复杂。细节省略。
混合树
将片断桉树形结果混合,淡入淡出时,很简单。细节省略。
动做状态机
动画状态
ASM中每一个状态对应一个任意复杂的动画片断混合。在混合树的架构中,每一个状态对应至某个预先定义的混合树。在扁平加权平均架构中,一个状态表明一组片断及一组相对权重
过渡
ASM中状态的过渡,须要这些信息:
状态层
控制参数
约束