动画系统简介
动画系统的构成
Three.js的最基础的动画系统,由以下几部分组成:
KeyFrameTrackAnimationClipAnimitionMixerAnimationAction
AnimationClip
在建模软件制作好动画之后,可以导出模型文件;通过Loader将模型文件加载之后,建模软件制作的动画就会被保存在animations中。这个animations就是一个AnimationClip的数组。
例如,常见的对人进行建模时,通常会包含一些动作动画:走路、跑步、挥手等。导出模型后,被Loader载入之后,每一个动作都会变成一个AnimationClip对象。
KeyFrameTrack
对于每个AnimationClip(可以理解为每个动作),都包含了很多对象属性的变化。例如,跑步这个动作包含:左胳膊、右胳膊、左腿、右腿等对象的属性变化。对于每一个属性,可以通过关键帧(KeyFrameTrack)来描述它的变化。
手动创建一个动画
为了简化代码,这里只用一个“点”来进行说明,先看一下这个动画的效果:
上面这个“动作”主要包含了两个属性的变化:position和color。因此,我们需要创建一个AnimitionClip,它包含了两个KeyFrameTrack,分别用来控制两个属性的变化。
//#region init scene
const scene = new THREE.Scene();
const pointGeometry = new THREE.BufferGeometry();
pointGeometry.setAttribute('position', new THREE.Float32BufferAttribute([-1, -1, 0], 3));
const pointMaterial = new THREE.PointsMaterial();
const point = new THREE.Points(pointGeometry, pointMaterial);
point.name = 'point';
scene.add(point);
//#endregion
//#region config animation
const positionTimes = [0, 2]; // 在0s时,有一个关键帧;在1s时,有一个关键帧
const positionVecs = [
-1, -1, 0, // times[0]对应的关键帧
1, 1, 0 // times[1]对应的关键帧
]
const positionKeyFrameTrack = new THREE.VectorKeyframeTrack('point.position', positionTimes, positionVecs);
const colorTimes = [0, 0.5, 1, 1.5, 2];
const colorVecs = [
1, 0, 0,
0, 1, 0,
0, 0, 1,
0, 1, 0,
1, 0, 0
];
const colorKeyFrameTrack = new THREE.ColorKeyframeTrack('point.material.color', colorTimes, colorVecs);
const animationClip = new THREE.AnimationClip('positionAnimation', 2, [positionKeyFrameTrack, colorKeyFrameTrack]);
//#endregion
动画要修改的属性,是通过KeyFrameTrack.name确定的,因此,在创建KeyFrameTrack时,第一个name参数不是随便起的。关于这个name的规则,需要参考官方文档:
KeyframeTrack#name – three.js docs (threejs.org)
如果不是做比较底层的工作,一般情况下不会接触到这个东西,只需要知道有这回事就行。
上述代码写完之后,只能看到一个静态的“白点”,并没有动画效果;那是因为,我们还没有播放这个动画。
通过AnimationMixer和AnimationAction播放动画
在上面,我们手动创建了一个动画(在实际的开发中,动画一般都会通过建模软件进行制作,导入模型文件后,就会解析成AnimationClip对象)。想要播放AnimationClip就需要通过另两个API来进行控制。
AnimationMixer,可以理解成一个总控制台,它的作用,就是用来控制“时刻” 。可以形象的理解为是一个动画播放的进度条。
AnimationAction,通过AnimationMixer.clipAction()进行创建。这个动作可以理解为把AnimationClip放进控制台进行播放。AnimationAction提供了一系列的API来控制对应的AnimationClip的播放,最基础的作用就是映射AnimationMixer的时刻。
举个例子:AnimationMixer的时间一直在向前推进,我们想让一个动画在第n秒播放,就可以通过AnimationClip.startAt()将动画延迟到第n秒播放。换句话说就是:将AnimationMixer的第n秒映射为AnimationClip的第0秒。
在上面的例子中,只有一个对象,所以这里通过AnimationMixer和AnimationAction进行控制显得有些多余。但是,一个场景往往不是只有一个对象,如果有很多对象都包含了动画,每个对象播放动画的时刻都不相同,这时,AnimationAction就非常必要了。
现在,可以播放我们上面手动创建的动画了,先要创建一个总的控制台:
const mixer = new THREE.AnimationMixer(scene); // 要控制的对象是scene的后代
然后,需要在动画循环中”推进时间“。AnimationMixer.update()就是用来推进时间的方法,它接收一个delta参数,时间的变化量,即,推进”多少“
Three也提供了配套的工具Clock,用来在动画循环中获取两次动画循环之间的时间间隔,也就是mixer要推进的时间。
// 初始化的时候
const clock = new THREE.Clock()
// 下面这句要放在动画循环中
mixer.update(delta)
mixer创建完毕后,需要用它来控制AnimationClip的播放:
const animationAction = mixer.clipAction(animationClip);
animationAction.play();
注意,这里的play()并不是平时生活中理解的播放。先看一段文档中的话:
Tells the mixer to activate the action. This method can be chained.
Note: Activating this action doesn’t necessarily mean that the animation starts immediately: If the action had already finished before (by reaching the end of its last loop), or if a time for a delayed start has been set (via startAt), a reset must be executed first. Some other settings (paused=true, enabled=false, weight=0, timeScale=0) can prevent the animation from playing, too.
文档对play()的说明是:告诉mixer激活action。
这并不意味着立刻执行这个动画,因为有以下几种情况:
- 这个动画已经播放完毕了,再次
play()不会起作用 - 设置了
startAt延迟播放 - ...
AnimationAction的工作方式是一个状态机,通过一系列方法设置一些参数(状态) ,在设置完毕后,通过play()告诉mixer,我的参数设置完了,你可以激活播放了。
总结
本文旨在介绍Three.js中的动画系统的基本使用,对动画更复杂的控制的API(主要集中在AnimationAction)没有进行过多的介绍。希望这篇文章可以帮助读者朋友建立一个动画系统的框架,搞清楚每一部分的作用,这样,遇到问题之后再看文档的API就会更加有针对性。