-
- 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);
-
-
- return new Promise((resolve) => {
- const finished = function () {
- mixer.removeEventListener('finished', finished);
- resolve();
- };
- mixer.addEventListener('finished', finished);
-
- action.play();
- });
- }
/* 單次動畫 */
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));
-
-
- 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);
-
-
- 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();
/* 初始化渲染器 */
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();