three.js进阶之动画系统

我曾在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属性的任何对象设置动画,依此类推。但是,关键帧确实指定了数据类型。上面的positionscale关键帧指定矢量数据,而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

猜你喜欢

转载自blog.csdn.net/qq_26822029/article/details/130441062