Three.js 教學:動畫與渲染迴圈 ── GSAP 與動畫控制
簡介
在 3D 網頁應用中,動畫是讓場景活起來的關鍵。單純的模型擺放雖然能呈現靜態效果,但缺乏互動感與視覺衝擊力。Three.js 本身提供了基本的時間驅動渲染迴圈,但若想要製作更精緻、時間軸可控、支援緩動 (easing) 與時間軸控制的動畫,GSAP (GreenSock Animation Platform) 是最常被搭配的工具。
GSAP 具備高效能、易於串接、支援時間軸 (Timeline) 與多種緩動函式的特性,讓開發者可以在 渲染迴圈 中以宣告式的方式描述動畫,而不必自行處理每一幀的插值計算。本篇文章將說明如何在 Three.js 中結合 GSAP,從基礎概念到實作範例,並提供常見陷阱與最佳實踐,幫助你快速上手並在實務專案中運用。
核心概念
1. Three.js 的渲染迴圈
Three.js 的渲染迴圈通常使用 requestAnimationFrame(簡稱 RAF):
function animate() {
requestAnimationFrame(animate);
// 更新物件、相機、控制器等
renderer.render(scene, camera);
}
animate();
- 呼叫頻率:與螢幕刷新率同步(通常 60 FPS)。
- 更新時機:每次
RAF回呼時,必須先更新物件狀態,再執行renderer.render。
2. 為何要引入 GSAP?
- 時間軸控制:可以將多段動畫串成一條時間軸,支援暫停、倒轉、快轉等操作。
- 緩動函式:內建數十種緩動效果,免去自行寫插值函式。
- 高效能:GSAP 內部使用
requestAnimationFrame,與 Three.js 渲染迴圈共享同一幀,避免重複呼叫。 - 可讀性:宣告式語法讓動畫流程更直觀,易於維護。
3. GSAP 基本用法
gsap.to(target, {
duration: 2, // 動畫持續 2 秒
x: 10, // 目標 x 位置
rotation: Math.PI, // 目標旋轉角度
ease: "power2.out", // 緩動函式
});
target可以是 Three.js 物件(如mesh.position、mesh.rotation),也可以是 自訂物件。- GSAP 會自動在每一幀更新目標屬性,不需要手動寫
update函式。
4. 與渲染迴圈結合的技巧
- 一次性初始化:在建立場景後一次性設定 GSAP 動畫,之後只需維持渲染迴圈。
- 使用
onUpdate回呼:若需要在動畫過程中執行額外計算(例如更新光源強度),可使用onUpdate。 - 時間軸 (Timeline) 管理:將多段動畫加入同一
gsap.timeline(),可以同時控制整體播放與暫停。
程式碼範例
範例 1:基本的物件移動與旋轉
// 建立基本場景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 2, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
document.body.appendChild(renderer.domElement);
// 建立一個立方體
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x1565c0 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 加入光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);
// 使用 GSAP 讓立方體在 3 秒內移動到 (2, 1, 0) 並旋轉 180°
gsap.to(cube.position, {
duration: 3,
x: 2,
y: 1,
ease: "power2.out",
});
gsap.to(cube.rotation, {
duration: 3,
y: Math.PI,
ease: "elastic.out(1, 0.5)",
});
// 渲染迴圈
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
重點:只要在
gsap.to內指定cube.position或cube.rotation,GSAP 會自動在每幀更新這些屬性,不需要額外的cube.position.needsUpdate。
範例 2:使用 Timeline 編排多段動畫
// 假設已有 scene、camera、renderer 與一個球體 sphere
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 32, 32),
new THREE.MeshStandardMaterial({ color: 0xff5722 })
);
scene.add(sphere);
// 建立時間軸
const tl = gsap.timeline({ repeat: -1, yoyo: true }); // 無限循環,往返播放
// 1. 往上移動 2 秒
tl.to(sphere.position, { duration: 2, y: 2, ease: "power1.inOut" })
// 2. 同時改變顏色 (使用 onUpdate 改變 material)
.to(
{ color: 0xff5722 }, // 中介物件
{
duration: 2,
color: 0x4caf50,
ease: "none",
onUpdate: function () {
sphere.material.color.set(this.targets()[0].color);
},
},
"-=2" // 與前一段同時開始
)
// 3. 繞 Y 軸旋轉 3 秒
.to(sphere.rotation, { duration: 3, y: Math.PI * 2, ease: "elastic.out(1, 0.3)" });
技巧:時間軸允許 重疊 (
"-=2"表示與前段同時開始),以及 重複 (repeat) 與 往返 (yoyo) 功能,適合製作持續的背景動畫或 UI 效果。
範例 3:在 onUpdate 中同步更新相機與控制器
// 使用 OrbitControls 讓使用者可以拖曳相機
const controls = new THREE.OrbitControls(camera, renderer.domElement);
// 讓相機在 5 秒內從遠距離拉近,並在過程中更新 controls
gsap.to(camera.position, {
duration: 5,
z: 3,
ease: "power3.out",
onUpdate: () => controls.update(),
});
說明:若相機位置是由 GSAP 改變,OrbitControls 需要在每幀重新計算目標,否則會出現「相機卡住」的情況。使用
onUpdate可以確保同步。
範例 4:結合自訂 Uniform 動畫(Shader 範例)
// 建立一個簡單的自訂 ShaderMaterial
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0x2196f3) },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
void main() {
float wave = sin(vUv.x * 10.0 + uTime) * 0.5 + 0.5;
gl_FragColor = vec4(uColor * wave, 1.0);
}
`,
});
const plane = new THREE.Mesh(new THREE.PlaneGeometry(4, 4, 32, 32), material);
scene.add(plane);
// 用 GSAP 動畫 uTime,產生波浪效果
gsap.to(material.uniforms.uTime, {
duration: 4,
value: Math.PI * 2,
repeat: -1,
ease: "none",
});
關鍵:對於 Shader Uniform,必須動畫的屬性是
value(或其他自訂屬性),GSAP 會自動在每幀寫入新值,使得 GPU 能即時渲染。
範例 5:同步多個物件的動畫與事件觸發
// 兩個盒子
const boxA = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 0.5, 0.5),
new THREE.MeshStandardMaterial({ color: 0xe91e63 })
);
boxA.position.set(-1, 0, 0);
scene.add(boxA);
const boxB = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 0.5, 0.5),
new THREE.MeshStandardMaterial({ color: 0x00bcd4 })
);
boxB.position.set(1, 0, 0);
scene.add(boxB);
// 同步動畫:兩盒子向中心靠近,完成後觸發 callback
gsap
.timeline()
.to(boxA.position, { duration: 2, x: 0, ease: "power2.inOut" })
.to(boxB.position, { duration: 2, x: 0, ease: "power2.inOut" }, 0) // 同時開始
.add(() => {
// 動畫結束時的自訂事件
console.log("兩盒子已會合!");
// 例如改變顏色
boxA.material.color.set(0xffeb3b);
boxB.material.color.set(0xffeb3b);
});
實務:利用
timeline.add(callback)可以在特定時間點執行任意程式碼,常用於 觸發聲音、切換場景、產生粒子效果 等。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 / 最佳實踐 |
|---|---|---|
忘記在渲染迴圈中呼叫 controls.update() |
GSAP 改變相機或物件位置後,控制器不會自動重新計算。 | 在 gsap.to(..., { onUpdate: () => controls.update() }) 中加入 onUpdate,或在主 animate() 內統一呼叫 controls.update()。 |
同時使用 Three.js 自帶的 clock.getDelta() 與 GSAP |
兩者都會基於 requestAnimationFrame,若同時計算位移會產生衝突。 |
選擇其一:若以 GSAP 為主,避免在 animate() 中自行做插值;若需要自訂物理運算,僅在 animate() 中使用 clock,GSAP 只負責時間軸控制。 |
Uniform 更新忘記設 needsUpdate |
對於 Texture、CubeTexture 等資源,需要手動設定 needsUpdate = true。 |
在 onUpdate 中加入 texture.needsUpdate = true,或使用 gsap.set 直接觸發。 |
| 大量物件同時使用 GSAP,造成效能瓶頸 | 每個動畫都會產生一個內部 ticker,過多會增加 GC 壓力。 |
批次處理:將相似動畫合併到同一個 timeline,或使用 gsap.quickSetter 直接寫入屬性,降低記憶體分配。 |
時間軸重複 (repeat) 與 onComplete 混用 |
repeat 會重置動畫,onComplete 只在最後一次執行。 |
若需要每次循環都觸發,可改用 onRepeat,或在 timeline 中插入 add(() => ...)。 |
進階最佳實踐
使用
gsap.quickSetter提升大量物件的更新效能const setX = gsap.quickSetter(cube.position, "x"); gsap.to({ progress: 0 }, { duration: 2, progress: 1, onUpdate() { setX(this.targets()[0].progress * 5); // 每幀直接寫入 }, });將時間軸與 UI 互動結合
- 使用
tl.pause()、tl.resume()控制動畫播放/暫停。 - 結合
dat.GUI或自製 HTML 按鈕,讓使用者即時調整動畫參數。
- 使用
避免硬編碼時間
- 把動畫時間抽成變數或配置檔,便於日後調整與多語系支援。
在開發階段使用
gsap.defaults({ overwrite: "auto" })- 防止同一屬性同時被多個動畫寫入,避免競爭條件。
實際應用場景
| 場景 | 目的 | GSAP 的角色 |
|---|---|---|
| 產品展示網站 | 讓模型在進入視口時自動旋轉、放大,並在滑鼠移動時產生微動畫。 | 使用 gsap.fromTo 讓模型從遠距離淡入,同時結合 onHover 觸發 gsap.to 改變材質亮度。 |
| 資料視覺化儀表板 | 隨著數據變化,3D 柱狀圖高度平滑過渡。 | 透過 gsap.to(bar.scale, { y: newValue, duration: 1 }),避免突兀的跳變。 |
| 互動式遊戲 UI | 按鈕點擊時彈跳、彈出說明框。 | gsap.fromTo(button.scale, { x: 0.8 }, { x: 1, ease: "back.out(2)" })。 |
| 沉浸式 AR/VR 體驗 | 進入場景時自動播放序幕動畫。 | 使用 gsap.timeline({ paused: true }),在使用者戴上裝置後 tl.play()。 |
| 教育訓練平台 | 示範機械臂或分子運動的步驟。 | 透過 timeline 分段控制每一步的旋轉與位移,搭配 onComplete 播放說明文字。 |
總結
- GSAP 為 Three.js 提供了高效、易用且功能強大的動畫控制層,讓開發者可以在 渲染迴圈 中以聲明式語法描述動畫,省去手動計算與更新的繁瑣工作。
- 了解 渲染迴圈 的基本結構、GSAP 基礎語法、以及 時間軸 的運用,是在 Three.js 專案中實作流暢動畫的關鍵。
- 本文提供了 5 個實用範例,從簡單的位移、旋轉,到時間軸、Shader Uniform、以及多物件同步動畫,涵蓋大部分日常需求。
- 常見陷阱(如控制器更新、Uniform 需要
needsUpdate、效能問題)與 最佳實踐(quickSetter、配置化時間、UI 結合)能協助你在大型專案中保持可維護性與效能。 - 最後,將 GSAP 應用於 產品展示、資料視覺化、遊戲 UI、AR/VR 等場景,能大幅提升使用者體驗與互動感。
祝你在 Three.js + GSAP 的世界裡玩得開心,創造出令人驚艷的 3D 動畫!