我曾在three.js进阶之骨骼绑定文章中提到了AnimationMixer、AnimationAction等内容,其实这些应该属于Three.js的动画系统,本文就系统的介绍一下动画系统(Animation System)。
前言
一般情况下,我们很少会使用three.js的动画系统去手动创建动画——因为这真的很麻烦,更高效便捷的做法还是直接在建模软件如Blender中完成动画的制作,然后在three.js中进行播放。不过,学习了动画系统对我们还是会有帮助的,下面进入正文。
创建动画涉及三个概念:关键帧Keyframes
,关键帧轨迹KeyframeTrack
和动画剪辑AnimationClip
。
1 关键帧 Keyframes
在动画系统中最低级别的概念就是关键帧,每个关键帧由三个信息组成:时间、属性和值,举个栗子:
- 在第0秒,
position
的取值为(0,0,0)
; - 在第3秒,
scale
的取值为(1,1,1)
; - 在第12秒,
material.color
为红色。
这三个关键帧分别描述特定时间的某些属性的值,不过关键帧并不指定任何特定对象。位置关键帧可用于为具有.location属性的任何对象设置动画,缩放关键帧可为具有.scale属性的任何对象设置动画,依此类推。但是,关键帧确实指定了数据类型。上面的position
和scale
关键帧指定矢量数据,而material.color
关键帧指定颜色数据。目前,动画系统支持五种数据类型。
要创建动画,我们至少需要两个关键帧。最简单的例子是两个数字关键帧,例如,动画材质的不透明度(它的透明度/透视程度):
- 在第0秒,
material.opacity
为0; - 在第3秒,
material.opacity
为1;
不透明度为0意味着完全不可见,不透明度为1意味着完全可见。当我们为某个对象设置了这两个关键帧后,它将会在3秒内逐渐出现。不管对象原本的透明度为多少,关键帧会覆盖其原本的值。
2 关键帧轨迹 KeyframeTrack
在Three.js中并没有表示单个关键帧的类,KeyframeTrack
中包含两个数组——时间数组和取值数组,每一个关键帧就对应时间数组和取值数组中的一个值。另外,KeyframeTrack
只是一个基类,不要直接使用KeyframeTrack
,,针对前面提到的每种数据类型都有对应的子类,你需要根据取值的数据类型选择对应的子类:
- BooleanKeyframeTrack
- ColorKeyframeTrack
- NumberKeyframeTrack
- QuaternionKeyframeTrack
- StringKeyframeTrack
- VectorKeyframeTrack
2.1 NumberKeyframeTrack
使用前面的透明度关键帧的例子:
- 在0秒时,
material.opacity
为0 - 在1秒时,
material.opacity
为1 - 在2秒时,
material.opacity
为0 - 在3秒时,
material.opacity
为1 - 在4秒时,
material.opacity
为0
由于透明度为数值型,因此可以使用NumberKeyframeTrack
类来存储关键帧数据:
import { NumberKeyframeTrack } from "three";
const times = [0, 1, 2, 3, 4];
const values = [0, 1, 0, 1, 0];
const opacityKF = new NumberKeyframeTrack(".material.opacity", times, values);
说明:KeyframeTrack
的构造函数为:
/**
* KeyframeTrack构造函数
* name: 关键帧轨道的名称
* times: 关键帧时间数组,内部转换为Float32Array
* values: 包含与时间数组相关的取值,内部转换为浮点32Array
* interpolation: 要使用的插值类型,默认值为线性插值
*/
KeyframeTrack( name : String, times : Array, values : Array, interpolation : Constant )
2.2 VectorKeyframeTrack
由于NumberKeyframeTrack
在每个时间点都只有一个数值类型的取值,因此times数组和values数组的长度是一样的,那么如果每一帧的数据是一个向量呢?应该如何构造values数组呢?我们使用下面的例子:
- 在第0秒,
position
为(0,0,0)
- 在第3秒,
position
为(2,2,2)
- 在第6秒,
position
为(0,0,0)
这三个关键帧将使对象从场景的中心开始,在三秒内向右、向上和向前移动,然后反转方向并移动回中心。接下来,我们将使用这些关键帧创建矢量轨迹。
import { VectorKeyframeTrack } from "three";
const times = [0, 3, 6];
const values = [0, 0, 0, 2, 2, 2, 0, 0, 0];
const positionKF = new VectorKeyframeTrack(".position", times, values);
需要留意,由于每个时间点的position
数据都是一个Vector3
包含3个数值,而且这些数据是直接平铺开来的,因此values数组的长度是times数组的3倍,对应的映射关系为:
const times = [0, 3, 6];
const values = [
0,
0,
0, // (x, y, z) at t = 0
2,
2,
2, // (x, y, z) at t = 3
0,
0,
0, // (x, y, z) at t = 6
];
3 动画剪辑 AnimationClip
如下图(动态效果可点击这里查看)中跳舞的模型动作非常复杂:双脚旋转,膝盖弯曲,手臂疯狂摆动,头部随着节拍点头。每个单独的动作都存储在一个单独的关键帧轨道中,因此有一个轨道控制舞者左脚的旋转,另一个轨道控制他的右脚的旋转,第三个轨道控制他的脖子的旋转,等等。
事实上,这个舞蹈动画是由53个关键帧轨道制成的,其中52个是控制舞者膝盖、肘部和脚踝等单个关节的四元数轨道。然后,有一个.location轨迹,可以在地板上来回移动图形。
这53个关键帧轨道结合在一起创建出的最终动画,我们称之为动画剪辑。因此,动画剪辑是附加到单个对象的任意数量关键帧的集合,表示剪辑的类是AnimationClip
。动画剪辑可以循环播放,所以,虽然这个舞者的动画只有18秒长,但当它到达终点时,它开始下一轮的循环,因此看起来舞者似乎可以一直跳下去。
下面是AnimationClip
的构造函数:
AnimationClip( name : String, duration : Number, tracks : Array )
从构造函数中能够看出动画剪辑存储三个信息:剪辑的名称、剪辑的长度和组成剪辑的轨道数组。如果我们将长度设置为-1,则轨道数组将用于计算长度。我们创建一个包含前面的单个位置轨迹的剪辑:
import { AnimationClip, VectorKeyframeTrack } from "three";
const times = [0, 3, 6];
const values = [0, 0, 0, 2, 2, 2, 0, 0, 0];
const positionKF = new VectorKeyframeTrack(".position", times, values);
// 当前只有一个关键帧轨道
const tracks = [positionKF];
// 将length设置为-1可以自动从tracks中计算长度,在本例中为6秒
const length = -1;
const clip = new AnimationClip("slowmove", length, tracks);
和关键帧一样,AnimationClip
不会被附着到任何特定的对象上,那么应该如何将动画绑定到模型身上并且控制其进行播放呢?
4 动画混合器 AnimationMixer
为了让物体(如Mesh)接入动画系统并且能够动起来,我们需要将其和动画混合器AnimationMixer
建立联系。场景中的每个动画对象都需要使用一个单独的混合器。混合器负责使模型按照动画剪辑的设定进行状态调整,如移动舞者的脚、手臂和臀部,或者是移动飞鸟的翅膀。
import { Mesh, AnimationMixer } from 'three';
// 创建一个静态的Mesh
const mesh = new Mesh();
// 通过将其连接到混合器,将其变为动画网格
const mixer = new AnimationMixer(mesh);
5 动画动作 AnimationAction
AnimationAction
负责将动画对象连接到动画剪辑AnimationClip
,同时也负责控制动画的暂停、播放、重置等操作。需要注意的是,我们不会直接创建action,而是借助AnimationMixer.clipAction()
函数创建,这样能够有更好的性能,因为mixer会对action进行缓存。
看下面的例子:
import { AnimationClip, AnimationMixer } from "three";
const positionKF = new VectorKeyframeTrack(
".position",
[0, 3, 6],
[0, 0, 0, 2, 2, 2, 0, 0, 0]
);
const opacityKF = new NumberKeyframeTrack(
".material.opacity",
[0, 1, 2, 3, 4, 5, 6],
[0, 1, 0, 1, 0, 1, 0]
);
const moveBlinkClip = new AnimationClip("move-n-blink", -1, [
positionKF,
opacityKF,
]);
const mesh = new Mesh();
const mixer = new AnimationMixer(mesh);
const action = mixer.clipAction(moveBlinkClip);
5.1 多动作控制
假设我们有一个人的模型,并且这个模型可以走路、跑步和跳,每个动画都将在一个单独的剪辑中出现,每个剪辑必须连接到一个动作。因此,就像混合器和模型之间存在一对一的关系一样,动作和动画剪辑之间也存在一对一的关系:
const mixer = new AnimationMixer(humanModel);
const walkAction = mixer.clipAction(walkClip);
const runnAction = mixer.clipAction(runClip);
const jumpAction = mixer.clipAction(jumpClip);
下一步是选择要执行这些操作中的哪一个。你怎么做将取决于你正在建造什么样的场景。例如,如果是游戏,您将将这些操作连接到用户控件,这样当按下相应的按钮时,角色将行走、运行或跳跃。
参考资料
The three.js Animation System