索引
HT动画系统由三部分内容组成:
onUpdate等回调控制具体逻辑Timer或TargetedAnimation到Timeline中,按照时间顺序播放其中Timer和Timeline可以点击链接进入专门的文档查看,本文档专门描述TargetedAnimation。
TargetedAnimation是HT动画系统中最常用的一个动画,大部分动画都可以通过它来实现的。它本身由Clip剪辑和Target目标组成。Clip剪辑包含多组Track轨道,每个Track轨道描述了一组属性的关键帧。
Track轨道是对一个属性根据关键帧进行动画的元件,其中关键帧包含了时间序列 Times跟对应的值序列 Values。
在动画播放的时候,要计算某个精确时间点状态时候,它会对时间序列进行二分查找,找到前后的两个关键帧时间点,然后根据时间点的值,调用对应插值函数进行插值。
它的构造函数为:
Track(name, times, values, easings, extraInfo)
name: 属性名称,本身是一个Binding。举例来说:binding的fallback规则,即会判断 getPosition/setPosition 是否存在,存在则使用,不存在则 .positionTrack插值出来的value传入,target是后续这个Track播放时的最终目标times: 时间序列,表示关键帧分别的时间,形如 [ 0, 0.5, 0.8 ]values: 值序列,根据时间序列,表示关键帧分别的值,形如 [ 1, 2, 3 ],或者 [ Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9) ]easings: 缓动函数序列,形如 [ 'easeIn', 'easeOut' ],默认 undefined 则表示 linear,具体参考Easing手册。注意 easings 数量比times要少一个,因为它表示的是两个times之间的插值extraInfo: 额外控制信息,目前包含信息:type: 默认轨道的类型可以自动推定,不需要传入。但是用户自己也可以强制指定类型,举个例子对于#FFAAFF,它即可以说一组颜色,也可以是普通字符串,需要用户自己指定举例:
new Track('width', [ 0, 1 ], [ 100, 200 ], 'linear')
new Track('style@shape.gradient.pack', [ 0, 2, 3.5, 5 ],
[
['radial', 0.5, 0.5, 0.5, 0, '#4CAF50', 1, 'white'], // 不同关键帧的 gradient pack 数据
['radial', 0.5, 0.5, 0.5, 0, '#2980B9', 1, '#D6EAF8'],
['radial', 0.5, 0.5, 0.5, 0, '#A9A9A9', 1, '#F5F5F5'],
['radial', 0.5, 0.5, 0.5, 0, '#F57C00', 1, 'white']
], undefined, { type : 'gradient' });
Track 包含的方法有:
add(time, value, easing): 添加一个关键帧。更多情况是 new Track 时候就已传入而不需要 addremoveKeyAtTime(time): 删除指定时间的关键帧clearKeys(): 清除所有关键帧getMaxTime(): 获取最大时间evaluate(time): 根据时间计算插值结果evaluateTo(time, array, offset): 根据时间计算插值结果,结果写入array的offset位置shift(timeOffset): 将Track的Times的所有时间偏移scale(timeScale): 将整个Track的长度进行缩放,包含的Times的所有时间都会进行缩放clone(): 克隆当前Track,返回新的Track对象trim(start, end): 截取Track的只留下start和end之间的Times跟Values这个例子中,用track.trim方法截取了Track,外围的Track中的scale和gradient两个属性的动画都只有完整动画的局部。一般如果要截取动画的局部的话,有两种方式可以达成:
timeline的特性,在add(animatable, childStart, childDuration, timelinePosition, timelineDuration)传入childStart和childDuration,那最终在timeline调度的时候,实际上这个子动画就发生了截取,不同于clip.trim和track.trim,它其实是逻辑上的裁剪,timeline调度的时候判定是否有效落在时间内才会对动画做evaluateclip.trim和track.trim,这两个方法是完整的对track数据进行了截取,它会扔掉track中times和values中start和end之外的数据,并且偏移时间轴到start位置作为0点即起点默认的Track的type包含有(下面以Times为 [0 - 1],Value为[a, b]举例):
discrete: 离散,不插值,对应从 0 到 0.999.. 的值为 a,到目标时间 1 的值为 blinear: 线性。即 a + (b - a) * 比例quaternion: 四元数插值,将使用Quaternion.slerp进行插值,得到球面平滑的正确的旋转插值euler: 欧拉角插值,因为欧拉的特性,对每个分量进行线性插值并不会得到球面平滑的动画,所以它会被切割形成一系列四元数以完成插值,这些发生在内部,用户不需要关心gradient or color: 颜色插值,包含HT内部的渐变色类型path3d: 3D 的路径插值,会修改目标的位置跟朝向path2d: 2D 的路径插值,会修改目标的位置跟朝向默认情况下,会自动根据值的情况猜测该Track的类型,如果无法猜测,则默认为linear,此时如果要强制指定可以在 new Track 的时候传入extraInfo中指定type。
如果上述类型均无法满足,用户需要完全自定义插值,可以使用ht.Default.setTrackType(type)方式注册:
ht.Default.setTrackType(type, { interpolation, valuesChangeHandler, stride })
interpolation:插值函数,必须提供,格式形如:function(out, outIndex, a, aIndex, b, bIndex, t, stride) { ... };表示out的outIndex位置的值,等于a的aIndex位置的值和b的bIndex位置的值根据t进行插值,stride表示值的步长valuesChangeHandler:Track的times和values变化时的回调,可选;用于关注输入发生变化做一些额外的用户态的自定义缓存等动作stride:值的步长,可选;默认为Values长度除以Times长度,后续会用它控制输出的buffer长度这里需要注意的是,对于高频计算的Track来说,我们为了让它GC(Garbage Collection)的开销降到最低,内部都是通过array数组来存放过程跟结果,并且对于同一个Track来说数组是复用的,所以自定义Track的时候,虽然也可以玩完全接管走interpolation的插值函数并自行计算,但最终的output如果是对象的话推荐使用array并提供对应的stride指定单个output的数据步长
Track的additive模式,表示是否动画是增量的。默认情况下,动画是非增量的,覆盖设置的,即Track设置的值会覆盖目标的值。
但是对于增量动画,Track设置的值会累加到目标的值上。
(注意,Additive由Clip来驱动来结算,Track的additive属性只起一个标记作用,需要使用clip.setAdditive(true)来设置)
它意味着,对动画设置 additive 为 true 后,HT内部会计算跟Track起始帧的值的差值,差值的逻辑为:
discrete类型,不计算差值。一般是string,bool这些无法做插值的元素quaternion类型,差值为两个四元数旋转之间的增量旋转euler类型,实质上会变成quaternion类型,即最终同样是增量的旋转linear类型,简单的做对应的数值差discrete处理,比如颜色渐变等增量动画应用到目标上的时候,如果目标有非增量类型的其他动画在播放,则增量直接累积到目标上。
如果此时目标上只有纯增量的动画,则会通过动画的track的bingding(name字段)的getter先获取当前目标的值作为初始值,再累加
这个例子里面用其中一个node创建它的移动跟旋转动画,然后设置为增量动画后复用给其他所有node。另外,按照每个node同中心的距离来作为动画的delay,并增加了根据高度来表现颜色的材质,以增加动画的表现效果
Clip剪辑是Track的集合,一组剪辑用于描述一套动作。例如人物的行走动画,即一个Clip剪辑,它一般包含多组不同骨骼部件的Track,每个Track描述不同的位移旋转等。
它的构造函数为:Clip(name, tracks, duration, isAdditive)
name: 剪辑名称,主要用于编辑器呈现或者其他用户的区分用途tracks: Track集合,数组类型,剪辑包含的轨道的列表duration: 剪辑时长,如果未设置,则会自动根据Tracks的Times来计算isAdditive: 是否为增量动画,具体参见Additive它包含的接口:
resetDuration(): 重新计算durationaddTrack(track): 添加Trackshift(timeOffset): 将Track的Times的所有时间偏移scale(timeScale): 将整个Track的长度进行缩放,包含的Times的所有时间都会进行缩放scaleTo(duration): 将整个Track的长度进行缩放到目标 duration,包含的Times的所有时间都会进行缩放clone(): 克隆当前Clip,返回新的Clip对象trim(start, end): 截取Track的只留下start和end之间的Times跟ValuessetAdditive(additive): 设置Track是否为additive模式setDuration(duration): 设置Track的durationisExclusive(): 是否为独占模式,如果一个动画是独占模式,则播放时会停下所有同 target 的其他独占动画,目前HT的所有FBX/GLTF的骨骼动画默认解析后都设置为独占,例如一个Robot有两个动画:idle和walk,当播放idle时,walk会自动停下,反之亦然setExclusive(exclusive): 设置Track是否为独占模式isPhaseMatching(): 是否为相位匹配模式setPhaseMatching(phaseMatching): 设置Track是否为相位匹配模式PhaseMatching 只有在动画切换并且target.playAnimation(animationName, speed, start, loop, fadeTime)设置了fadeTime的时候才会生效
我们举个例子:当前正在播放的动画叫做 A1,新播放的动画叫做 A2,fadeTime 0.8 秒
为什么会有相位匹配?
原因在于,譬如人物这种精细的模型,如果处在任意动画阶段就无条件混合的话,二者会有抽搐混合。所以一般会设定一定的相位,譬如人脚掌着地,认为脚掌着地是人行走的固定相位,那么在切换行走动画的时候,会先播放到脚掌着地的相位,再做混合相对就会融合的更好
(我们现在简单的认定融合的相位点是动画结尾位)
将一组Clip剪辑应用到具体的目标或者目标群上的时候,并设定了播放速度、起始时间、循环模式等预期, 就构成了TargetedAnimation。
它首先继承自Timer,所以具备Timer的全部功能。构造函数为:
new ht.Animation.TargetedAnimation(clip, target, defaults)
clip: 动画剪辑target: 目标,或者目标数组defaults: 动画的配置信息,该参数同Timer,具体可以参考Timer它额外包含的接口:
getTarget(): 获取目标setTarget(target): 设置目标getRootTarget(): 获取根目标getClip(): 获取动画剪辑setClip(clip): 设置动画剪辑play(speed, start, loop, delay, fadeInTime): 同Timer和Timeline不同的是,这里多了个参数fadeInTime,表示淡入时间,淡入时间,在切换两个动画的时候可以指定isExclusive(): 它的clip是否为独占模式,,如果一个动画是独占模式,则播放时会停下所有同 target 的其他独占动画这个例子中有一些信息特别说明一下:
idle walk 和 run 是独占动画,它们直接是互斥的。其他 agree headShake 等都是非独占动画,它们可以同时播放,并且设置了additive,作为增量附加的动画idle 动画没有设置 phaseMatching,即它跟其他动画作weight blend时候都是直接切换,我们以 1 秒 blend 的时间为例,从idle切换到run的时候idle的动画权重从 1 变为 0,run的动画权重从 0 变为 1,动画按照权重进行插值idle的动画权重为 0 并停止,run的动画权重为 1 继续播放walk 跟 run 动画设置了 phaseMatching,即它跟其他动画作weight blend时候都是相位匹配,我们以 1 秒 blend 的时间为例,从walk切换到run的时候walk到结束的时间,因为设定的phase默认是结束点位,先继续播放到一轮结束walk的动画权重从 1 变为 0,run的动画权重从 0 变为 1,动画按照权重进行插值run和 walk的时长不同,其中run是0.7秒,walk是0.966秒,所以二者的速度会做渐变以期望二者逐渐同频。具体说,walk起始速度为 1,那么run起始速度为 0.724。而结束的时候run的速度会到 1,所以walk最终速度会来到 1.38walk的动画权重为 0 并停止,run的动画权重为 1 继续播放agree headShake sad_pose sneak_pose 都是非独占动画,它们可以同时播放,并且设置了additive,作为增量附加的动画,根据权重进行跟主动画的叠加sad_pose sneak_pose 并不需要完整动画,它们故意只使用了最后一帧的数据,animation.additive = true; animation.start = animation.duration; animation.duration = 0.0;,譬如它们的动作最后一帧是弯腰的,那么我们只是用到弯腰的各个部件的Transform增量(包含translation、rotation、scale),根据权重决定了弯腰的具体幅度为了方便创建动画,我们提供一些必要的工具函数便于快速手写动画
AnimationConfig 包含动画的配置信息,它主要是用于:
new Clip和new Track操作,可以快速创建node或image图标或3D图标的animations属性中,后续可以使用playAnimation进行播放其数据定义如下:
{
type : 'entity', // 可选,动画类型,model3d 表示驱动目标上的模型动画,image 表示驱动目标上的image动画,entity表示驱动目标自身动画
default : false, // 可选,是否是默认动画,target.playAnimation(undefined) 没有带动画名的时候会查找所有动画中标记了 default : true 的进行播放
duration : 1, // 可选,动画时长,如果未设置,则按照 1 秒计算
name : 'walk', // 动画名,后续 playAnimation 的动画名
tracks : [
{ name : 'robotHips.position', times : [ 0, 9 ], values : [ 0, 0, 0, 500, 500, 500 ], easings : [ 'Bezier' ], type : 'linear' }, // track 定义
{ name : 'robotHips.quaternion', times : [ 0, 9 ], values : [ 0, 0, 0, 1, -0.3829, 0, 0, 0.92388 ], type : 'quaternion' },
],
additive : false, // 可选,是否为增量的相对动画,具体参见[Additive](#ref_additive)
exclusive : undefined, // 可选,是否为独占动画,如果一个动画是独占模式,则播放时会停下所有同 target 的其他独占动画
phaseMatching : false, // 可选,是否为相位匹配模式
}
type默认不需要指定AnimationConfig定义在entity(例如node,graphView等)的animations下的则为entity,如果定义在图标上的animations下则为image,如果定义在模型上则为model3d。只有在AnimationConfig定义在entity下并且希望驱动其他类型动画的时候才需要指定typetype本质的意义是,它会影响最终track下的bindings以及播放动画时候的目标对象,譬如entity的话目标就是自身,image实际上目标是target.getImageState()tracks是由一个个track构成的数组,对于每个track:exclusive表示是否独占动画AnimationConfig.exclusive设置则采用该设置AnimationConfig.exclusive未设置,则根据additive来判断。当additive不是true,说明这个动画是一个完整动画,那么它默认是独占动画,即exclusive为trueAnimationConfig 的播放接口可以是下面两种:
entity.playAnimation(animationConfig, speed, start, loop, fadeTime),其中 entity 可以是 node / graphView / graph3dView 等name的animationConfig注册动画,后续entity.playAnimation(name)播放动画entity自身注册通过entity.registerAnimation(animationConfig)2D image图标注册动画通过image.animations = [ animationConfig1, animationConfig2 .. ]在图标上3D 模型注册动画通过model3d.animations = [ animationConfig1, animationConfig2 ]在模型3D图标上为了更方便快速的创建常用 Entity 动画(目前的 Entity 动画包含node / graphView / graph3dView),我们提供了一些语法糖,进一步简化AnimationConfig的书写,标记为Tracks如果未定义的话则尝试使用这种方式定义。具体的定义为:
{
duration : 1, // 可选,动画时长,如果未设置则认为 1
// type, name, default, additive, exclusive, phaseMatching 等属性见 AnimationConfig
// 非上述关键字的,均被当作 track 的 name 处理
'state@position' : { values : [ 1, 1, 1, 5, 5, 5 ], easings : [ 'Bezier' ], },
'state@position' : { to : [ 5, 5, 5], easing : 'Bezier' }, // to 模式的话,用当前的 position 跟 to 的值,构成 values
'state@position' : [ 5, 5, 5 ], // 等同于 { to : [ 5, 5, 5 ] }
}
举个详细的例子,可以简单的进行快速创建动画:
node.playAnimation({
// duration : 3,
'p3' : [ 5, 5, 5 ],
'scale3d' : { to : [ 2, 2, 2 ], easing : 'Spring' },
'rotationY' : node.getRotationY() + Math.PI * 2
})
// or
node.registerAnimation({
name : 'myCustomAnimation',
// duration : 3,
'p3' : [ 5, 5, 5 ],
'scale3d' : { to : [ 2, 2, 2 ], easing : 'Spring' },
'rotationY' : node.getRotationY() + Math.PI * 2
});
node.playAnimation('myCustomAnimation');