本文 AI 產出,尚未審核

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 內建 AudioListenerAudioPositionalAudio,讓聲音能隨相機或物件位置變化,營造 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 負擔。 只在需要偵測時(如 pointermovepointerdown)才呼叫,或使用 節流 (throttle)
音效延遲播放 AudioLoader 仍在載入時即呼叫 play(),會產生卡頓或無聲。 先確保 buffer 已載入,或使用 Promise 方式等待。
特效過度使用 多個 Pass 同時啟用會降低 FPS,特別在行動裝置。 依裝置檢測性能 (navigator.hardwareConcurrencyWebGLRenderer.capabilities) 動態調整特效開關。
粒子記憶體泄漏 每次產生粒子未移除,導致場景累積大量 Points 在粒子生命結束時 scene.remove(points),並釋放幾何體與材質 (geometry.dispose()material.dispose())。
相機與音源同步失誤 AudioListener 沒加到相機上,會出現聲音不隨視角移動的問題。 確認 listener 已掛在相機,且 PositionalAudio 的位置正確。

最佳實踐

  1. 模組化:將互動、音效、特效分別封裝成 class(例如 InteractionManagerSoundManagerEffectManager),提升可維護性。
  2. 資源預載:使用 THREE.LoadingManager 統一管理模型、貼圖、音檔的載入,確保所有資源完成後再進入主場景。
  3. 自適應解析度:在視窗變更時同時調整 renderer.setSizecomposer.setSizecamera.aspect,避免畫面拉伸。
  4. 效能檢測:利用 stats.jsGPUStat 觀測 FPS、渲染時間,針對瓶頸進行優化(例如降低粒子數量、使用 InstancedMesh)。

實際應用場景

場景 需求 實作要點
品牌產品展示 3D 商品可旋轉、點擊播放說明音效、點擊觸發特效 使用 OrbitControls + Raycaster,商品模型加入 PositionalAudio,特效使用 Bloom + Particle 強調互動。
線上藝術裝置 使用者走動時產生回音、光斑隨腳步閃爍 透過 PointerLockControls 讓使用者「走」在虛擬空間,腳步位置觸發 PositionalAudioUnrealBloomPass
教育互動課程 3D 物理實驗、點擊觸發粒子爆炸、配合解說音檔 每個實驗元件設置 RaycasterAudio,粒子系統模擬爆炸或化學反應,使用 GUI 調整參數以適應不同年齡層。
遊戲式網站 闖關式導覽、完成任務播放背景音樂、觸發特效獎勵 EffectComposer 結合 GlitchPassFilmPass 製造過關動畫,任務完成時播放 PositionalAudio 並顯示 Particle 爆炸。

總結

本篇從 互動事件、3D 音效、後期特效、粒子系統 四大核心技術出發,示範了在 Three.js 中打造 富有沉浸感的 3D 互動網站 所需的實作步驟與注意事項。透過 RaycasterPointer 事件,我們可以精準捕捉使用者操作;利用 AudioListener + PositionalAudio,聲音會隨空間移動而變化,增添真實感;EffectComposer 與各式 Pass 為畫面帶來光暈、閃爍等視覺衝擊;最後的粒子系統則讓每一次互動都留下「火花」般的回饋。

在開發過程中,模組化、資源預載與效能監控 是確保專案易於維護、兼容多平台的關鍵。只要遵循本文的 最佳實踐,即使是剛入門的開發者,也能快速上手,為客戶或個人作品交付一個 視覺、聽覺、觸覺 三位一體的完整 3D 體驗。祝你玩得開心、創作順利! 🚀