索引
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
调度的时候判定是否有效落在时间内才会对动画做evaluate
clip.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()
: 重新计算duration
addTrack(track)
: 添加Track
shift(timeOffset)
: 将Track
的Times
的所有时间偏移scale(timeScale)
: 将整个Track
的长度进行缩放,包含的Times
的所有时间都会进行缩放scaleTo(duration)
: 将整个Track
的长度进行缩放到目标 duration,包含的Times
的所有时间都会进行缩放clone()
: 克隆当前Clip
,返回新的Clip
对象trim(start, end)
: 截取Track
的只留下start
和end
之间的Times
跟Values
setAdditive(additive)
: 设置Track
是否为additive
模式setDuration(duration)
: 设置Track
的duration
isExclusive()
: 是否为独占模式,如果一个动画是独占模式,则播放时会停下所有同 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
下并且希望驱动其他类型动画的时候才需要指定type
type
本质的意义是,它会影响最终track
下的bindings
以及播放动画时候的目标对象,譬如entity
的话目标就是自身,image
实际上目标是target.getImageState()
tracks
是由一个个track
构成的数组,对于每个track
:exclusive
表示是否独占动画AnimationConfig.exclusive
设置则采用该设置AnimationConfig.exclusive
未设置,则根据additive
来判断。当additive
不是true
,说明这个动画是一个完整动画,那么它默认是独占动画,即exclusive
为true
AnimationConfig 的播放接口可以是下面两种:
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
的书写,具体的定义为:
{
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');