2023-03-07 16:55

[ThreeJS] 單次動畫

/* 單次動畫 */
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();

0 回應: