Three.js 實戰專案:3D 互動網站 – 加入互動、音效與特效
簡介
在現代的網頁設計中,3D 互動體驗已成為提升使用者黏著度與品牌形象的重要手段。透過 Three.js,我們可以在瀏覽器裡直接渲染高效能的 3D 场景,並結合滑鼠、觸控、鍵盤等輸入,打造出令人驚豔的互動網站。
除了視覺上的互動,音效與特效(例如粒子、後期濾鏡)更能加強情境沉浸感,讓使用者彷彿身歷其境。本篇文章將從 基礎概念、實作範例、常見陷阱 以及 最佳實踐 四個面向,帶領你一步步為 Three.js 專案加入互動、音效與特效,讓你的 3D 網站從「好看」升級為「好玩」與「好聽」的完整體驗。
核心概念
1. 事件驅動的互動模型
Three.js 本身不提供 DOM 事件的偵測,需要自行將 Raycaster 與瀏覽器的事件系統結合。Raycaster 會把滑鼠或觸控座標投射成射線,檢測與場景中物件的相交情況,從而觸發點選、拖曳或 hover 效果。
1.1 基本 Raycaster 設定
// 建立 Raycaster 與滑鼠向量
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// 監聽滑鼠移動事件
window.addEventListener('pointermove', (event) => {
// 正規化螢幕座標 (-1 ~ +1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
重點:
pointermove能同時支援滑鼠與觸控裝置,建議統一使用。
1.2 點選物件
function onClick(event) {
// 更新 Raycaster
raycaster.setFromCamera(mouse, camera);
// 只檢測可互動的物件 (例如 objects 陣列)
const intersects = raycaster.intersectObjects(interactiveObjects, true);
if (intersects.length > 0) {
const selected = intersects[0].object;
// 變更顏色以示選取
selected.material.emissive.setHex(0xff0000);
// 播放點選音效 (見第 2 節)
playSound('click');
}
}
window.addEventListener('pointerdown', onClick);
2. 音效的整合 – 使用 Web Audio API
Three.js 內建 AudioListener、Audio 與 PositionalAudio,讓聲音能隨相機或物件位置變化,營造 3D 空間音效。以下示範如何在場景中加入背景音樂與點選音效。
2.1 初始化 AudioListener
// 把 listener 加到相機上,聲音會跟隨相機移動
const listener = new THREE.AudioListener();
camera.add(listener);
2.2 播放背景音樂
const bgMusic = new THREE.Audio(listener);
const audioLoader = new THREE.AudioLoader();
audioLoader.load('assets/audio/bg-music.mp3', (buffer) => {
bgMusic.setBuffer(buffer);
bgMusic.setLoop(true);
bgMusic.setVolume(0.5);
bgMusic.play();
});
2.3 位置音效 (PositionalAudio)
// 假設有一個可點選的立方體 cube
const cube = new THREE.Mesh(geom, mat);
scene.add(cube);
// 為 cube 加入 3D 音效
const sound = new THREE.PositionalAudio(listener);
audioLoader.load('assets/audio/click.wav', (buffer) => {
sound.setBuffer(buffer);
sound.setRefDistance(20);
});
cube.add(sound);
// 在點選時觸發
function playSound(type) {
if (type === 'click') sound.play();
}
小技巧:
setRefDistance控制音量衰減的距離,調整可讓音效更自然。
3. 特效與後期處理 (Post‑Processing)
Three.js 提供 EffectComposer 讓我們在渲染管線最後加入 Bloom、Glitch、Film Grain 等特效。這些特效不僅提升視覺衝擊,也能在互動時產生即時回饋。
3.1 建立 Composer
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
// Bloom 效果設定
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, // strength
0.4, // radius
0.85 // threshold
);
composer.addPass(bloomPass);
3.2 在動畫迴圈中使用 Composer
function animate() {
requestAnimationFrame(animate);
// 更新控制器、動畫等
controls.update();
// 使用 composer 替代 renderer.render()
composer.render();
}
animate();
3.3 動態切換特效
function toggleBloom(enabled) {
bloomPass.enabled = enabled;
}
// 例如在使用者點擊某個物件時開啟 Bloom
function onSpecialClick() {
toggleBloom(true);
setTimeout(() => toggleBloom(false), 800); // 0.8 秒後自動關閉
}
4. 粒子系統 – 為互動加上「火花」效果
粒子是製作 爆炸、光斑、煙霧 等視覺特效的常見手段。Three.js 的 Points + PointsMaterial 搭配 BufferGeometry 能高效產生上千顆粒子。
4.1 建立簡易火花粒子
function createSpark(position) {
const count = 100;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const velocities = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
// 初始位置在中心
positions[i * 3] = position.x;
positions[i * 3 + 1] = position.y;
positions[i * 3 + 2] = position.z;
// 隨機速度向外散開
const speed = 0.5 + Math.random() * 0.5;
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
velocities[i * 3] = speed * Math.sin(phi) * Math.cos(theta);
velocities[i * 3 + 1] = speed * Math.cos(phi);
velocities[i * 3 + 2] = speed * Math.sin(phi) * Math.sin(theta);
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
const material = new THREE.PointsMaterial({
color: 0xffaa00,
size: 0.05,
transparent: true,
opacity: 0.9,
depthWrite: false
});
const points = new THREE.Points(geometry, material);
scene.add(points);
// 更新粒子位置
const life = 60; // 幀數
let frame = 0;
function update() {
if (frame++ > life) {
scene.remove(points);
return;
}
const pos = points.geometry.attributes.position;
const vel = points.geometry.attributes.velocity;
for (let i = 0; i < count; i++) {
pos.array[i * 3] += vel.array[i * 3] * 0.02;
pos.array[i * 3 + 1] += vel.array[i * 3 + 1] * 0.02 - 0.01; // 重力
pos.array[i * 3 + 2] += vel.array[i * 3 + 2] * 0.02;
}
pos.needsUpdate = true;
requestAnimationFrame(update);
}
update();
}
使用情境:在
onSpecialClick()內呼叫createSpark(intersects[0].point),即可在點擊位置產生火花。
5. 結合 GUI 參數調整
開發過程中,即時調整參數 能大幅提升迭代速度。dat.GUI(或 lil-gui)提供直覺的滑桿與勾選框,讓開發者與設計師即時看到效果。
import GUI from 'lil-gui';
const gui = new GUI();
const params = {
bloomStrength: 1.5,
bloomRadius: 0.4,
bloomThreshold: 0.85,
soundVolume: 0.5,
enableParticles: true,
};
gui.add(params, 'bloomStrength', 0, 3).onChange(v => bloomPass.strength = v);
gui.add(params, 'bloomRadius', 0, 1).onChange(v => bloomPass.radius = v);
gui.add(params, 'bloomThreshold', 0, 1).onChange(v => bloomPass.threshold = v);
gui.add(params, 'soundVolume', 0, 1).onChange(v => bgMusic.setVolume(v));
gui.add(params, 'enableParticles').onChange(v => {
// 開關粒子系統
});
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| Raycaster 每幀呼叫過多 | 若在 animate 中每幀都執行 intersectObjects,會造成 CPU 負擔。 |
只在需要偵測時(如 pointermove、pointerdown)才呼叫,或使用 節流 (throttle)。 |
| 音效延遲播放 | AudioLoader 仍在載入時即呼叫 play(),會產生卡頓或無聲。 |
先確保 buffer 已載入,或使用 Promise 方式等待。 |
| 特效過度使用 | 多個 Pass 同時啟用會降低 FPS,特別在行動裝置。 |
依裝置檢測性能 (navigator.hardwareConcurrency、WebGLRenderer.capabilities) 動態調整特效開關。 |
| 粒子記憶體泄漏 | 每次產生粒子未移除,導致場景累積大量 Points。 |
在粒子生命結束時 scene.remove(points),並釋放幾何體與材質 (geometry.dispose()、material.dispose())。 |
| 相機與音源同步失誤 | AudioListener 沒加到相機上,會出現聲音不隨視角移動的問題。 |
確認 listener 已掛在相機,且 PositionalAudio 的位置正確。 |
最佳實踐:
- 模組化:將互動、音效、特效分別封裝成 class(例如
InteractionManager、SoundManager、EffectManager),提升可維護性。 - 資源預載:使用
THREE.LoadingManager統一管理模型、貼圖、音檔的載入,確保所有資源完成後再進入主場景。 - 自適應解析度:在視窗變更時同時調整
renderer.setSize、composer.setSize與camera.aspect,避免畫面拉伸。 - 效能檢測:利用
stats.js或GPUStat觀測 FPS、渲染時間,針對瓶頸進行優化(例如降低粒子數量、使用InstancedMesh)。
實際應用場景
| 場景 | 需求 | 實作要點 |
|---|---|---|
| 品牌產品展示 | 3D 商品可旋轉、點擊播放說明音效、點擊觸發特效 | 使用 OrbitControls + Raycaster,商品模型加入 PositionalAudio,特效使用 Bloom + Particle 強調互動。 |
| 線上藝術裝置 | 使用者走動時產生回音、光斑隨腳步閃爍 | 透過 PointerLockControls 讓使用者「走」在虛擬空間,腳步位置觸發 PositionalAudio 與 UnrealBloomPass。 |
| 教育互動課程 | 3D 物理實驗、點擊觸發粒子爆炸、配合解說音檔 | 每個實驗元件設置 Raycaster、Audio,粒子系統模擬爆炸或化學反應,使用 GUI 調整參數以適應不同年齡層。 |
| 遊戲式網站 | 闖關式導覽、完成任務播放背景音樂、觸發特效獎勵 | 以 EffectComposer 結合 GlitchPass、FilmPass 製造過關動畫,任務完成時播放 PositionalAudio 並顯示 Particle 爆炸。 |
總結
本篇從 互動事件、3D 音效、後期特效、粒子系統 四大核心技術出發,示範了在 Three.js 中打造 富有沉浸感的 3D 互動網站 所需的實作步驟與注意事項。透過 Raycaster 與 Pointer 事件,我們可以精準捕捉使用者操作;利用 AudioListener + PositionalAudio,聲音會隨空間移動而變化,增添真實感;EffectComposer 與各式 Pass 為畫面帶來光暈、閃爍等視覺衝擊;最後的粒子系統則讓每一次互動都留下「火花」般的回饋。
在開發過程中,模組化、資源預載與效能監控 是確保專案易於維護、兼容多平台的關鍵。只要遵循本文的 最佳實踐,即使是剛入門的開發者,也能快速上手,為客戶或個人作品交付一個 視覺、聽覺、觸覺 三位一體的完整 3D 體驗。祝你玩得開心、創作順利! 🚀