动画是Web3D程序的灵魂,总体上,动画的本质是目标物体随着时间的流逝而发生一系列的变化过程,在计算机上实现这些动画的底层思路都是将他们转换成基于时间的关键帧动画,实践中又可以有多种为形式表现,比如:
- 目标变形动画:为模型的每一个顶点指定若干个MorphTargets顶点,并通过morphTargetInfluences数组来定义这些顶点对原始顶点的影响因子,来计算顶点在当前帧的最终坐标,至于这些帧以什么样的速度去播放,一般由程序自由控制。
- 几何变形动画:这种动画的思想非常简单,直接通过改变顶点的坐标实现动画,比如海浪动画、表情动画等。
- 骨骼动画(蒙皮动画):骨骼动画由一系列的层级对象组成,父对象的变化会带动子对象的变化,这种父子关系可以有很多层级,通过本地矩阵和世界矩阵,每一个子类都可以很方便的计算出来自己的世界坐标,典型的骨骼动画是人物动画,比如跑步、跳跃等等,注意骨骼动画要求模型必须是单网格的。
当然,动画的表现形式也可以是颜色、比例、透明度等等,其实现原理都是相同的,在Web3D应用程序中,最常见的动画形式仍然是基于位置变化和旋转角度变化的关键帧动画,threejs通过AnimationClip、AnimationMixer等类来实现这种动画,但他们都被设计的过于复杂了,需要事先准备大量复杂的动画数据,如果需要控制动画的运行,还需要编写事件监听程序。
本章我们介绍两个用于创建关键帧动画的类PositionAnimation、RotationAnimation,并把他们整理至脚本文件KeyFrameAnimation.js中(也可以分开保存),利用这两个类可以很简洁、很直观地部署关键帧动画。
目录
2.1.1 位移动画类(PositionAnimation)
2.1.1.6 设置动画时长(setAnimationTime)
2.1.2 旋转动画类(RotationAnimation)
2.1.2.7 设置动画时长(setAnimationTime)
2.1.3 导出PositionAnimation和RotationAnimation
2.1 关键帧动画类
关键帧动画的核心是如何合理的生成中间的插值帧(过渡帧),插值可以是线性的、也可以是非线性的,但通用的动画过程一般都是线性的。
由于动画制作过程中需要用到欧拉角和向量运算,我们把PositionAnimation、RotationAnimation两个类整理至一个脚本文件KeyFrameAnimation.js中,这样就只需要做一次导入操作就可以了。
import { Euler, Vector3 } from 'three';
import { ThreeBase } from './ThreeBase.js';
//产生一个唯一的标识符
function generateUniqueId() {
// 使用36进制转换,避免负号出现
let timestamp = Date.now().toString(36);
// 获取随机数的一部分,并转换为36进制
let randomPart = Math.random().toString(36).substr(2, 9);
return timestamp + randomPart;
}
函数generateUniqueId用于生成一个唯一的标识符(近似于“m4gn1ij9jrow53f1t”的一个字符串),用于唯一的标识一个关键帧动画,可以使用任何其他等效的函数。
2.1.1 位移动画类(PositionAnimation)
2.1.1.1 构造函数(constructor)
class PositionAnimation extends ThreeBase {
constructor(object, points, duration, loop) {
super();
this.UUID = generateUniqueId();
this.object = object; //动画对象
this.points = points; //位置坐标数组
this.frames = this.points.length; //帧数
this.duration = duration || 1; //动画总时长
this.loop = loop == undefined ? true : loop; //是否循环(默认为循环播放)
this.frameTime = this.duration / this.frames; //每帧时长
this.object.position.copy(this.points[0]); //准备动画
this.running = false; //动画是否正在运行
}
…//其他方法
}
参数points传递的是所有位移点的三维坐标数组,这个数组可以使用threejs的CatmullRomCurve3类来生成,一般方法是先给定几个关键点(至少3个以上,这样才能形成一条封闭曲线),根据这些关键点生成一条平滑曲线,然后从曲线上密集的取若干连续点的坐标(比如50个点),如下所示。
let vectors = [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(5, 0, 0),
new THREE.Vector3(5, 5, 0)
];
let curve = new THREE.CatmullRomCurve3(vectors);
let points = curve.getPoints(50);
let geometry = new THREE.BufferGeometry().setFromPoints(points);
let material = new THREE.LineBasicMaterial({color: 0xffff00});
let line = new THREE.Line(geometry, material);
这段代码将生成如下图所示的一条曲线,变量points即可用于构造函数的参数。
2.1.1.2 开始动画(start)
start() {
this.frameElapsedTime = 0.0; //当前帧流逝时间
this.animationElapsedTime = 0.0; //动画总流逝时间
this.curPos = -1; //当前帧
this.nextPos = 0; //下一帧
this.loopCycles = 0; //循环次数
this.enabled = true; //是否生效(当loop设置为false时,循环一次后enabled自动变为false)
this.running = true; //动画是否正在运行
}
其中,this.frameElapsedTime表示当前帧的流逝时间,他在每一帧结束的时候都会清零,只有当这个时间大于this.frameTime(每帧时长)的时候,动画才会走到下一帧去;this.nextPos属性可用于lookAt指向,实现巡线效果。
2.1.1.3 暂停动画(pause)
pause() {
this.enabled = false;
this.running = false;
}
2.1.1.4 播放动画(play)
play() {
this.enabled = true;
this.running = true;
}
2.1.1.5 重新开始动画(reStart)
reStart() {
this.object.position.copy(this.points[0]);
this.start();
}
2.1.1.6 设置动画时长(setAnimationTime)
setAnimationTime(v) {
this.duration = v;
this.frameTime = this.duration / this.frames;
}
2.1.1.7 设置是否循环(setLoop)
setLoop(v) {
this.loop = v;
this.enabled = true;
this.animationElapsedTime = 0.0;
}
2.1.1.8 更新动画(update)
update(delta) {
if(!this.enabled) {
this.running = false;
return;
}
this.frameElapsedTime += delta;
this.animationElapsedTime += delta;
if(this.frameElapsedTime >= this.frameTime) {
this.running = true;
if(this.curPos == (this.frames - 1)) { //走到最后了
this.loopCycles++; //统计动画循环次数,始终不断增长
this.publish(
'loopCyles', //固定消息,v[0]
this.UUID, //唯一标识,v[1]
this.loopCycles //循环次数,v[2]
);
if(!this.loop) { //只播放一次
this.enabled = false;
this.running = false;
return;
}
}
this.curPos = (this.curPos + 1) % this.frames;
this.nextPos = (this.nextPos + 1) % this.frames;
this.object.position.copy(this.points[this.curPos]);
this.object.lookAt(this.points[this.nextPos]);
this.frameElapsedTime = 0; //重新开始一帧的计时
} else
this.running = false;
}
动画每循环一次,发布一个“loopCycles”消息,同时发布UUID和循环次数,这个消息可用于控制连续关键帧动画的播放。
2.1.2 旋转动画类(RotationAnimation)
旋转动画类的工作原理与位移动画类相似,主要的区别在于参数不同,RotationAnimation只接收一个具有2个元素的旋转数组,分别是起始旋转角度、结尾旋转角度,中间的过渡帧则由程序按线性插值的方式自动生成。
2.1.2.1 构造函数(constructor)
class RotationAnimation extends ThreeBase {
constructor(object, rotations, duration, loop) {
super();
this.UUID = genera