Three.js 實戰專案:3D 互動網站
主題:建立 3D 場景與互動流程
簡介
在現代網頁設計中,3D 互動 已成為提升使用者體驗與品牌辨識度的重要手段。透過 Three.js,開發者只需要少量程式碼,就能在瀏覽器裡呈現高效能、跨平台的立體畫面,無需安裝任何外掛。
本篇文章將帶領讀者從 零基礎 建立一個完整的 3D 場景,並實作常見的互動流程(如滑鼠點擊、拖曳、動畫播放)。內容涵蓋核心概念、實作範例、常見陷阱與最佳實踐,適合 初學者 以及 中階開發者 快速上手並套用於實務專案。
核心概念
1. 基本構件:Scene、Camera、Renderer
Three.js 的渲染流程可抽象為三大核心物件:Scene(場景容器)、Camera(觀察者)與 Renderer(渲染器)。只有把這三者正確組合,才能在畫布上看到 3D 內容。
// 建立場景
const scene = new THREE.Scene();
// 建立相機 (PerspectiveCamera)
const camera = new THREE.PerspectiveCamera(
75, // 視野角度 (FOV)
window.innerWidth / window.innerHeight, // 長寬比
0.1, // 近剪裁面
1000 // 遠剪裁面
);
camera.position.set(0, 1.6, 3); // 將相機提升至人眼高度
// 建立渲染器,綁定至 <canvas>
const renderer = new THREE.WebGLRenderer({ antialias: true, canvas: document.querySelector('#webgl') });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
小技巧:在開發階段可先加入
renderer.setClearColor(0x202020);設定背景色,方便辨識座標系統。
2. 光源與材質
沒有光源,任何 Mesh 都會是全黑的。Three.js 提供多種光源(Ambient、Directional、Point、Spot、Hemisphere),根據場景需求挑選並調整強度、位置與顏色。
// 環境光:為整體提供基礎亮度
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);
// 平行光:模擬太陽光,投射陰影
const directional = new THREE.DirectionalLight(0xffffff, 0.8);
directional.position.set(5, 10, 7);
directional.castShadow = true; // 啟用陰影
directional.shadow.mapSize.width = 1024; // 陰影解析度
directional.shadow.mapSize.height = 1024;
scene.add(directional);
實務建議:若場景中有大量物件,僅保留 AmbientLight + DirectionalLight,可降低 GPU 負載。
3. 載入外部模型(GLTF / GLB)
在實務專案中,通常會使用 3D 建模軟體(Blender、Maya)匯出 GLTF/GLB 檔案,並於 Three.js 中動態載入。
// 建立 GLTFLoader
const loader = new THREE.GLTFLoader();
// 載入模型
loader.load(
'models/room.glb',
(gltf) => {
const model = gltf.scene;
model.traverse((node) => {
if (node.isMesh) {
node.castShadow = true;
node.receiveShadow = true;
}
});
scene.add(model);
},
(xhr) => {
console.log(`模型載入進度: ${(xhr.loaded / xhr.total) * 100}%`);
},
(error) => {
console.error('模型載入失敗', error);
}
);
重點:載入完成後,務必將 mesh 的
castShadow與receiveShadow設為true,否則陰影不會顯示。
4. 使用 OrbitControls 實作相機操作
為了讓使用者能自由旋轉、縮放視角,我們常加入 OrbitControls(官方附屬套件)。
// 引入 OrbitControls
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 平滑慣性
controls.dampingFactor = 0.05;
controls.minDistance = 2; // 最近距離
controls.maxDistance = 10; // 最遠距離
controls.target.set(0, 1, 0); // 目標焦點
controls.update();
小提醒:
controls.update()必須在每一次的渲染迴圈中呼叫,否則慣性效果不會生效。
5. 互動:Raycaster 取得點擊資訊
Raycaster 是 Three.js 處理滑鼠或觸控點擊的核心工具,能將螢幕座標轉換為 3D 世界的射線,進而偵測哪些物件被點擊。
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onPointerDown(event) {
// 正規化螢幕座標 (-1 ~ +1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 設定射線
raycaster.setFromCamera(mouse, camera);
// 取得與射線相交的物件
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
const hit = intersects[0].object;
console.log('點擊物件:', hit.name);
// 範例:改變顏色
hit.material = hit.material.clone();
hit.material.color.setHex(Math.random() * 0xffffff);
}
}
window.addEventListener('pointerdown', onPointerDown);
技巧:若只想偵測特定類別的物件,可先將它們放入
clickableObjects陣列,再傳給raycaster.intersectObjects(clickableObjects, true),可大幅提升效能。
6. 動畫循環與時間控制
Three.js 的渲染循環通常使用 requestAnimationFrame,並結合 Clock 取得每幀的時間差,以實作平滑動畫(例如模型旋轉、移動)。
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta(); // 兩幀之間的秒數
// 示例:讓模型緩緩自轉
if (scene.getObjectByName('RotatingBox')) {
const box = scene.getObjectByName('RotatingBox');
box.rotation.y += delta * 0.5; // 每秒 0.5 rad
}
controls.update(); // 更新 OrbitControls
renderer.render(scene, camera);
}
animate();
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 模型材質消失 | 匯入 GLTF 時未設定 renderer.shadowMap.enabled = true 或光源未開啟陰影。 |
確認 renderer.shadowMap.enabled = true,並為每顆光源設定 castShadow。 |
| 畫面卡頓 | 場景內物件過多、陰影解析度過高、未使用 requestAnimationFrame。 |
使用 Level of Detail (LOD)、降低 shadow.mapSize、適度使用 InstancedMesh。 |
| Raycaster 效能低 | 每幀遍歷全部物件。 | 只在需要時(點擊/觸控)呼叫 raycaster.intersectObjects,並限制搜尋範圍。 |
| 視窗尺寸改變未同步 | 窗口縮放後相機與渲染器尺寸未更新,導致畫面變形。 | 監聽 resize 事件,重新設定 camera.aspect、camera.updateProjectionMatrix()、renderer.setSize()。 |
| 記憶體洩漏 | 動態載入模型但未釋放資源。 | 使用 dispose() 釋放幾何體、材質與紋理,或在切換場景時呼叫 renderer.dispose()。 |
最佳實踐
- 模組化:將場景、相機、控制器、載入器等功能分離成獨立檔案,提升可維護性。
- 使用 ES6+:利用
import/export、async/await讓程式碼更易讀。 - 開發環境:搭配 Vite 或 Webpack,自動熱重載與資源壓縮。
- 資源最佳化:GLTF 模型使用 Draco 壓縮,圖片使用 WebP 或 Basis。
- 測試相容性:在行動裝置上測試觸控操作,必要時加入 TouchControls 或自訂手勢。
實際應用場景
| 領域 | 典型案例 | Three.js 解決方案 |
|---|---|---|
| 產品展示 | 電子產品 3D 旋轉、拆解說明 | 使用 GLTF 載入高細節模型、Raycaster 點擊部件顯示說明文字。 |
| 教育訓練 | 虛擬實驗室、解剖模型 | 結合 dat.GUI 調整參數、動畫播放教學步驟。 |
| 建築視覺化 | 房屋戶型漫遊、光照模擬 | 透過 HDRI 環境光、光源控制即時調整日夜景。 |
| 互動藝術 | 網頁裝置互動裝置、音樂視覺化 | 結合 Web Audio API、ShaderMaterial 動態產生粒子特效。 |
| 行銷活動 | 3D 互動遊戲、抽獎機制 | 加入 GSAP 動畫、狀態機管理互動流程,提升使用者黏著度。 |
總結
本篇從 Three.js 的核心概念出發,示範了建立 3D 場景、配置光源、載入模型、加入相機控制與互動的完整流程。透過 Raycaster、OrbitControls、Clock 等工具,我們可以在瀏覽器內即時呈現流暢且具交互性的 3D 體驗。
同時,我們也列舉了開發過程中常見的陷阱與最佳實踐,讓讀者在實作時能避免效能與穩定性問題。最後,透過表格說明了多種實務應用場景,期望讀者能將所學套用到 產品展示、教育訓練、建築視覺化 等領域,打造出令人印象深刻的 3D 互動網站。
加油!只要掌握了「場景 + 相機 + 渲染器」的三大要素,再加上適當的光源、模型與互動控制,你就能在短時間內完成一個完整的 3D 互動專案。祝開發順利,期待看到你的作品在網路上閃耀!