three.js中使用tween.js的chain实现动画依次执行
three.js小白的学习之路。
在three.js中实现一些动画,经常会使用tween.js库,这是一个非常优秀的补间动画库。
1.使用chain的原因和场景
在动画制作时,经常会遇到一些执行上有前后次序的动画。他们之间经常是通过时间互相关联。我们不妨将其想象成一个数组,数组中每一个都是tween.js动画的一个组成:
const tweenList = [{tagert: {},time: 1000,start: () => {},update: () => {},complete: () => {},// …………},{tagert: {},time: 2000,delay: 1000,start: () => {},update: () => {},complete: () => {},// …………},// …………………………
];
数组中每一个都是一个对象,对象内部包含有数个tween.js会用到的参数。
假如将下一个动画的执行都放在上一个动画的 onComplete 方法里面,那么就会出现以前类似于ajax的回掉地狱现象。
假如我们使用setTimeOut来固定时间间隔的一次执行,当然可以,但是假如某一个动画的执行时间发生了变化,那么后续动画的执行开始时间都会发生变化,维护起来会非常麻烦。
使用tween.js中的delay方法也会出现上述问题。
这里给出两个解决方法:
1.使用Promise丰创一个异步函数,在 onComplete 中执行 resolve,并在外面写一个 async await 进行调用。
2.使用tween.js中的chain方法。
本文介绍第二种方法。
2.chain属性介绍
首先要获取tween动画的句柄:
const handle1 = new TWEEN.tween().………………
const handle2 = new TWEEN.tween().………………
const handle3 = new TWEEN.tween().………………
然后通过句柄进行调用,注意,上面的链式操作中,不能使用 start 方法:
handle1.chain(handle2)
handle2.chain(handle3) // 执行顺序 1 -> 2 -> 3handle1.chain(handl2, handl3) // 执行顺序 1 -> 2 和 3handle1.chain(handl2)
handle2.chain(handl1) // 无限循环动画,有点类似于 死锁
chain也会返回一个句柄,是已经被修改的handle1的句柄。
3.例子
按照上面的使用方法,可以实现下面这个例子:
绿色撞向红色,红色延迟一秒后,撞向蓝色,蓝色一半立即运动,一半延迟运动。
整体代码如下(省去基础搭建),DOM中加了一个button来控制执行:
const poi = [[-10, 0, 0],[0, 0, -1],[0, 0, 1],[10, 0, -12],[10, 0, -10],[10, 0, 10],[10, 0, 12],
];
const meshList = [];
const geo = new Three.BoxGeometry(2, 2, 2);
const mat1 = new Three.MeshBasicMaterial({ color: 0x00ff00 });
const mat2 = new Three.MeshBasicMaterial({ color: 0xff0000 });
const mat3 = new Three.MeshBasicMaterial({ color: 0x0000ff });
// 生成基础立方体
poi.forEach((poi, i) => {if (i === 0) meshList.push(new Three.Mesh(geo, mat1));else if (i <= 2) meshList.push(new Three.Mesh(geo, mat2));else meshList.push(new Three.Mesh(geo, mat3));meshList[i].position.set(...poi);scene.add(meshList[i]);
});// 动画轨迹数组
const bodyMovePlan = [{target: {x: -1,y: 0,z: 0,},time: 1000,},{target: {x: 9,y: 0,z: 11,},time: 1000,delay: 1000,},{target: {x: 9,y: 0,z: -11,},time: 1000,delay: 1000,},{target: {x: 19,y: 0,z: -21,},time: 1000,},{target: {x: 19,y: 0,z: -1,},time: 1000,delay: 1000,},{target: {x: 19,y: 0,z: 1,},time: 1000,},{target: {x: 19,y: 0,z: 21,},time: 1000,delay: 1000,},
];const tweenHandle = [];
const bodyMove = () => {// 生成句柄,注意不能调用 startbodyMovePlan.forEach((plan, i) => {const tween = new TWEEN.Tween(meshList[i].position).to(plan.target, plan.time).delay(plan.delay ?? 0);tweenHandle.push(tween);});// 将前后动画使用chain关联起来tweenHandle[0].chain(tweenHandle[1], tweenHandle[2]);tweenHandle[1].chain(tweenHandle[3], tweenHandle[4]);tweenHandle[2].chain(tweenHandle[5], tweenHandle[6]);
};bodyMove();
// 点击按钮开始运动
const start = () => {console.log("start");tweenHandle[0].start();
};const loop = () => {TWEEN.update(); // 统一更新所有的tween动画renderer.render(scene, camera);requestAnimationFrame(loop);
};loop();
动画实现过程很简单,最主要就是对路径的规划,以及chain的调用。
注意1:不能提前调用start,且只需要第一个动画调用start即可。
注意2:loop循环中要更新tween动画的时间。
4.避坑
第二次执行出现跳帧到最后的问题
有时可能会出现一些问题,我遇到的一个问题是,第二次执行的时候出现了跳帧问题,就是直接到了结束帧,然后动画的时间依然保持不变。
我的数据结构是这样的:
const ani = [
{type: '',ani: {from: {},go: {},// …………}
},
// ………………
]
分析原因,结构了一层,是浅拷贝,但是里面并非是简单类型,还是有ani这种对象复杂类型,而tween.js在执行过程中并不会对你传入的对象进行深拷贝(也是为什么我们直接传入camera.position就可以改变camera的位置)。
解决方法也很简单,要么优化一下结构,或者如果层数不多,就将里面那一层也复制一份,像解构,就可以解决问题。