本文 AI 產出,尚未審核

Three.js – 動畫與渲染迴圈

物件旋轉、移動與時間管理


簡介

在 3D 網頁開發中,動畫是讓場景活起來的關鍵。無論是簡單的旋轉立方體、角色在地面上行走,或是複雜的粒子系統,都離不開 渲染迴圈(render loop)時間管理
Three.js 把底層的 WebGL 操作抽象化,使我們只需要關注「物件在什麼時間點該怎麼變化」即可。掌握好這些概念,才能寫出 流暢、可維護且效能良好 的 3D 應用。

本篇文章將從 核心概念實作範例常見陷阱與最佳實踐,一路帶你了解如何在 Three.js 中正確地控制物件的旋轉與移動,並以 時間 為基礎驅動動畫。


核心概念

1. 渲染迴圈的本質

Three.js 的渲染迴圈其實只是一個 requestAnimationFrame 的包裝。它會在瀏覽器每次重繪前呼叫一次你的回呼函式,讓你有機會更新場景、相機或任何需要變動的屬性。

function animate() {
    requestAnimationFrame( animate );   // ← 每次畫面更新前被呼叫
    // 1️⃣ 更新物件狀態
    // 2️⃣ 渲染場景
    renderer.render( scene, camera );
}
animate();   // 啟動渲染迴圈
  • 為什麼使用 requestAnimationFrame 而不是 setInterval
    requestAnimationFrame 會自動配合螢幕刷新率(通常是 60Hz),且在分頁隱藏時會自動暫停,節省資源。

2. 時間管理:Delta Time 與 Elapsed Time

在動畫中,我們常用兩種時間概念:

名稱 取得方式 用途
Delta Time (delta) 前一次迴圈與本次迴圈的時間差(秒) 讓動畫與幀率無關,即使 FPS 降低也能保持相同速度
Elapsed Time (elapsed) 從動畫開始累積的總時間(秒) 用於週期性動畫(如正弦波、循環)

Three.js 官方提供了 THREE.Clock 來輕鬆取得這兩個值:

const clock = new THREE.Clock();   // 建立時鐘

function animate() {
    requestAnimationFrame( animate );

    const delta   = clock.getDelta();    // 上一幀與本幀的時間差
    const elapsed = clock.getElapsedTime(); // 從 start 到現在的總時間

    // 以 delta 為基礎更新物件...
    renderer.render( scene, camera );
}
animate();

小技巧:在需要 固定時間步長(如物理模擬)時,可使用 clock.getDelta() 乘上一個 時間縮放係數timeScale),方便加速或減速整體動畫。

3. 旋轉與移動的座標系

Three.js 使用右手座標系:

  • X:指向右
  • Y:指向上
  • Z:指向螢幕外(遠離觀察者)

3.1 物件旋轉

物件的旋轉屬性是 object.rotation,它是一個 Euler 物件,包含 x、y、z 三個軸向的弧度值。常見的做法是:

// 讓立方體每秒繞 Y 軸旋轉 45 度
const speed = THREE.MathUtils.degToRad(45); // 45° → rad

function animate() {
    requestAnimationFrame( animate );
    const delta = clock.getDelta();

    cube.rotation.y += speed * delta; // 依 delta 調整旋轉量
    renderer.render( scene, camera );
}

注意:直接使用 += 會累積誤差,若需要 精確 的角度(如 0~360° 迴圈),建議使用 THREE.MathUtils.euclideanModulo 取模。

3.2 物件移動

移動最直接的方式是改變 object.position,同樣以 delta 為基礎:

const moveSpeed = 2; // 單位:公尺/秒

function animate() {
    requestAnimationFrame( animate );
    const delta = clock.getDelta();

    // 讓球體沿 X 軸正方向前進
    sphere.position.x += moveSpeed * delta;
    renderer.render( scene, camera );
}

若要在 本地座標系(相對於物件自身)移動,可使用 object.translateX/Y/Z( distance )

// 讓飛船在自身前方推進
ship.translateZ( -moveSpeed * delta ); // Z 正方向指向螢幕內

4. 讓動畫與時間同步:範例彙總

以下提供 五個實用範例,涵蓋旋轉、平移、彈性運動與時間驅動的特效。


範例 1️⃣ 基本旋轉(單軸)

// 旋轉立方體:每秒 30 度繞 X 軸
const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({ color: 0x2194ce })
);
scene.add(cube);

const rotSpeed = THREE.MathUtils.degToRad(30); // 30° → rad

function animate() {
    requestAnimationFrame( animate );
    const delta = clock.getDelta();

    cube.rotation.x += rotSpeed * delta; // 使用 delta 防止幀率影響
    renderer.render( scene, camera );
}
animate();

重點rotSpeed 用弧度表示,每秒 旋轉 30°。


範例 2️⃣ 同時多軸旋轉(使用 elapsed time)

// 讓球體同時繞 Y、Z 軸做週期性旋轉
const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 32, 32),
    new THREE.MeshStandardMaterial({ color: 0xf54291 })
);
scene.add(sphere);

function animate() {
    requestAnimationFrame( animate );
    const elapsed = clock.getElapsedTime();

    // 以正弦波產生平滑的角速度變化
    sphere.rotation.y = Math.sin( elapsed ) * Math.PI;   // -π ~ +π
    sphere.rotation.z = Math.cos( elapsed ) * Math.PI;   // -π ~ +π

    renderer.render( scene, camera );
}
animate();

說明Math.sin/Math.cos 讓旋轉速度呈現 自然的加速與減速,適合製作「漂浮」或「擺動」的效果。


範例 3️⃣ 直線移動 + 邊界迴圈

// 讓方塊在 X 軸往返移動,超過邊界時回到起點
const box = new THREE.Mesh(
    new THREE.BoxGeometry(0.8, 0.8, 0.8),
    new THREE.MeshStandardMaterial({ color: 0x7ed321 })
);
scene.add(box);

const moveSpeed = 3;          // m/s
const limit = 5;              // 邊界座標 (+/- limit)

function animate() {
    requestAnimationFrame( animate );
    const delta = clock.getDelta();

    box.position.x += moveSpeed * delta;
    // 超過右邊界則回到左邊
    if (box.position.x > limit) box.position.x = -limit;

    renderer.render( scene, camera );
}
animate();

技巧:使用 模組運算 (THREE.MathUtils.euclideanModulo) 也能寫出無縫迴圈:

box.position.x = THREE.MathUtils.euclideanModulo(
    box.position.x + moveSpeed * delta + limit,   // 先平移再加上 offset
    limit * 2
) - limit;   // 重新映射回 -limit ~ +limit

範例 4️⃣ 本地座標系的前進與轉向(FPS 風格)

// 建立一個簡易的「第一人稱」相機控制器
const player = new THREE.Object3D();   // 作為玩家的容器
scene.add(player);
player.add(camera);                    // 相機掛在玩家上

const forwardSpeed = 5;   // m/s
const turnSpeed    = THREE.MathUtils.degToRad(120); // 120°/s

// 假設有鍵盤事件控制 forward / left / right
let moveForward = false, turnLeft = false, turnRight = false;

// 監聽鍵盤(此處僅示範)
window.addEventListener('keydown', e => {
    if (e.code === 'KeyW') moveForward = true;
    if (e.code === 'KeyA') turnLeft    = true;
    if (e.code === 'KeyD') turnRight   = true;
});
window.addEventListener('keyup', e => {
    if (e.code === 'KeyW') moveForward = false;
    if (e.code === 'KeyA') turnLeft    = false;
    if (e.code === 'KeyD') turnRight   = false;
});

function animate() {
    requestAnimationFrame( animate );
    const delta = clock.getDelta();

    // 轉向
    if (turnLeft)  player.rotation.y += turnSpeed * delta;
    if (turnRight) player.rotation.y -= turnSpeed * delta;

    // 前進(沿本地 Z 軸負方向)
    if (moveForward) player.translateZ( -forwardSpeed * delta );

    renderer.render( scene, camera );
}
animate();

關鍵translateZ 會自動考慮物件的 旋轉,因此玩家在轉向後仍能向前推進。


範例 5️⃣ 時間縮放(Slow‑motion / Fast‑forward)

// 讓旋轉立方體可以透過滑桿調整速度(0.1x ~ 3x)
const cube2 = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({ color: 0xffc107 })
);
scene.add(cube2);

let timeScale = 1; // 預設正常速度

// 建立 HTML 滑桿(簡易示意)
const slider = document.createElement('input');
slider.type = 'range';
slider.min = 0.1;
slider.max = 3;
slider.step = 0.1;
slider.value = 1;
document.body.appendChild(slider);
slider.addEventListener('input', () => {
    timeScale = parseFloat(slider.value);
});

const baseSpeed = THREE.MathUtils.degToRad(60); // 60°/s

function animate() {
    requestAnimationFrame( animate );
    const delta = clock.getDelta() * timeScale; // 套用時間縮放

    cube2.rotation.y += baseSpeed * delta;
    renderer.render( scene, camera );
}
animate();

實務應用:在遊戲或教學影片中常需要 慢動作(slow‑motion)或 加速(fast‑forward),只要把 delta 乘上縮放係數即可,不需要改變原始速度變數


常見陷阱與最佳實踐

陷阱 說明 解決方案 / 最佳實踐
直接使用 object.rotation.x += angle 而忽略 delta 幀率不穩定時會導致動畫快慢不一致。 使用 clock.getDelta() 乘上每秒角速度。
弧度與角度混用 THREE.Euler 只接受弧度,若傳入度數會出現奇怪的旋轉。 統一使用弧度,或利用 THREE.MathUtils.degToRad() / radToDeg() 轉換。
忘記重置 clock 在切換場景或暫停時,累積的 elapsedTime 可能造成不預期的行為。 在需要「重新計時」的時機呼叫 clock.start()clock.elapsedTime = 0
使用 setIntervalsetTimeout 來驅動動畫 會與瀏覽器的刷新率脫節,且在頁面隱藏時仍持續執行,浪費資源。 永遠使用 requestAnimationFrame
不考慮物件的父子階層 子物件的座標是相對於父物件的,直接改變 position 可能產生意外的移動。 明確了解 本地座標系世界座標系,有需求時使用 object.getWorldPosition()object.worldToLocal()
忘記釋放資源 產生大量幾何體或材質卻未呼叫 dispose(),會導致記憶體泄漏。 在不再使用時,呼叫 geometry.dispose()material.dispose(),並從場景移除。

小技巧

  1. 封裝動畫更新
    把所有需要每幀更新的行為放入一個陣列 updaters,在 animate 中一次迭代。這樣可以輕鬆 啟用 / 停用 某段動畫。

    const updaters = [];
    
    // 例:加入旋轉 updater
    updaters.push( (delta) => {
        cube.rotation.y += rotSpeed * delta;
    });
    
    function animate() {
        requestAnimationFrame( animate );
        const delta = clock.getDelta();
    
        updaters.forEach( fn => fn(delta) );
    
        renderer.render( scene, camera );
    }
    
  2. 使用 THREE.MathUtils.lerp 產生平滑過渡(如相機跟隨)。

  3. 避免在渲染迴圈內做大量計算(例如:即時生成大量幾何體),改用 Worker預先緩存


實際應用場景

場景 需求 主要使用的概念
3D 產品展示 讓模型自動旋轉、使用者滑鼠拖曳時停止自轉 requestAnimationFrame + delta + object.rotation
第一人稱射擊遊戲 (FPS) 玩家在鍵盤/滑鼠控制下前進、轉向、射擊 translateZ、本地座標系、clock.getDelta()、時間縮放
資料視覺化(Bar Chart 旋轉動畫) 依資料值逐步伸長,完成後緩慢環繞旋轉 elapsedTimeMath.lerpTHREE.Easing
教育互動模擬(太陽系) 行星依不同軌道速度繞太陽公轉、同時自轉 多個 Clock不同的 speed 乘上 delta
AR/VR 體驗 依使用者頭部姿態即時更新相機,物件保持相對位置 object.position + camera.matrixWorldInverse、時間同步

案例說明:在太陽系模擬中,地球的自轉速度約為 360°/24h,公轉速度約 360°/365d。把它們分別轉換成 rad/s,再乘上 delta,就能保證不論使用者的裝置 FPS 為多少,動畫都保持正確的比例。


總結

  • 渲染迴圈 是 Three.js 動畫的核心,必須使用 requestAnimationFrame 取得與螢幕刷新同步的更新時機。
  • 時間管理deltaelapsed)讓動畫 與幀率無關,確保在不同裝置上都有一致的速度與感受。
  • 物件的 旋轉移動 皆以 弧度座標系 為基礎,配合 clock.getDelta()translateX/Y/ZTHREE.MathUtils 等工具,可寫出流暢且易維護的程式碼。
  • 常見的陷阱多與 幀率不穩、單位混淆、資源釋放 有關,遵守最佳實踐(如使用 delta、統一弧度、適時 dispose())即可避免大多數問題。
  • 透過 封裝更新函式時間縮放本地座標系,你可以快速擴展到 遊戲、互動展示、教育模擬 等各種實務情境。

掌握了 物件旋轉、移動與時間管理,你就能在 Three.js 中構建出 生動、效能優化且可擴展 的 3D 互動體驗。祝你玩得開心,創作無限! 🚀