/* 單次動畫 */
function animateOnce(mixer, prop, dur, values) {
/* 停止 & 清除 先前的動畫 */
mixer.stopAllAction();
mixer.uncacheRoot(mixer.getRoot());
/* 增加動畫片段 */
const track = new THREE.KeyframeTrack(prop, [0, dur], values);
const clip = new THREE.AnimationClip('move', dur, [track]);
const action = mixer.clipAction(clip);
action.clampWhenFinished = true;
action.setLoop(THREE.LoopOnce);
/* Promise 來處理完成事件,這樣就可以用 await */
return new Promise((resolve) => {
const finished = function () {
mixer.removeEventListener('finished', finished);
resolve();
};
mixer.addEventListener('finished', finished);
action.play(); /* 播放動畫 */
});
}
想要能像 SVG 一樣簡單的控制模型的移動,ThreeJS 並沒有直接提供我想要的模式,試了很久終於成功了。
mixer 中的 action 是不斷添加的,這會混亂播放動畫,所以在播放新的動畫前必須[停止]且[清除]先前的動畫。
為了可以簡單串接不同模型的動畫,所以就想用 await 來達成,所以加上 Promise 的包裝。
完整程式
/* 初始化渲染器 */
const renderer = new THREE.WebGLRenderer({ antialias: true });
document.getElementById('Container').appendChild(renderer.domElement);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
/* 初始化場景 */
const scene = new THREE.Scene();
scene.background = new THREE.Color('#000'); /* 背景顏色 */
scene.add(new THREE.AmbientLight('#FFF', 0.5)); /* 加入環境光 */
scene.add(new THREE.AxesHelper(50)); /* 3D 軸標示 */
/* 初始化鏡頭 */
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 300);
camera.position.set(0, 4, 12);
/* 初始化軌道控制,鏡頭的移動 */
const orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
orbitControls.update();
/* 動畫刷新器 */
const mixers = [];
function newMixer(target) {
const mixer = new THREE.AnimationMixer(target);
mixers.push(mixer);
return mixer;
}
/* 動畫計時器 */
const clock = new THREE.Clock();
/* 渲染週期 */
function renderCycle() {
const delta = clock.getDelta();
mixers.forEach(x => x.update(delta));
renderer.render(scene, camera);
requestAnimationFrame(renderCycle);
}
renderCycle();
/*---------------------------------------------------------------*/
/* 單次動畫 */
function animateOnce(mixer, prop, dur, values) {
/* 停止 & 清除 先前的動畫 */
mixer.stopAllAction();
mixer.uncacheRoot(mixer.getRoot());
/* 增加動畫片段 */
const track = new THREE.KeyframeTrack(prop, [0, dur], values);
const clip = new THREE.AnimationClip('move', dur, [track]);
const action = mixer.clipAction(clip);
action.clampWhenFinished = true;
action.setLoop(THREE.LoopOnce);
/* Promise 來處理完成事件,這樣就可以用 await */
return new Promise((resolve) => {
const finished = function () {
mixer.removeEventListener('finished', finished);
resolve();
};
mixer.addEventListener('finished', finished);
action.play(); /* 播放動畫 */
});
}
const cubeA = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial( {color: '#0F0'} )
);
const mixerA = newMixer(cubeA);
scene.add(cubeA);
const cubeB = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial( {color: '#00F'} )
);
const mixerB = newMixer(cubeB);
scene.add(cubeB);
async function run() {
await animateOnce(mixerA, '.position[x]', 3, [0, 3]);
await animateOnce(mixerB, '.position[y]', 3, [0, 2]);
await animateOnce(mixerA, '.position[x]', 3, [3, 0]);
await animateOnce(mixerB, '.position[y]', 3, [2, 0]);
}
run();