Three.js 與 React 整合(react‑three‑fiber)
主題:使用 R3F Hooks(useFrame / useLoader)
簡介
在傳統的 Three.js 開發中,我們必須自行管理渲染迴圈、資源載入與生命週期。將 Three.js 與 React 結合後,react‑three‑fiber(簡稱 R3F)提供了一套 React Hook 風格的 API,讓 3D 場景的建立與更新可以像寫普通的 React 元件一樣自然。
其中最常用的兩個 Hook —— useFrame 與 useLoader —— 分別負責 每幀更新 與 資源載入。掌握它們的用法,能讓你在 React 生態系中快速構建互動式 3D 應用、資料視覺化或 AR/VR 原型,且程式碼更具可讀性與可維護性。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶領讀者一步步熟悉這兩個 Hook,並提供實務應用的參考方向。
核心概念
1. useFrame – 每幀執行的回呼
useFrame 是 R3F 提供的「渲染迴圈 Hook」,類似 Three.js 中的 renderer.setAnimationLoop 或 requestAnimationFrame。
- 簽名:
useFrame((state, delta) => void, priority?)state:包含camera、scene、gl、mouse、clock等資訊的 R3F 狀態物件。delta:自上一幀的時間差(秒),適合做時間基礎的動畫。priority(可選):數字越小表示越早執行,可用於控制多個 Hook 的執行順序。
重點:
useFrame只在組件掛載時註冊一次,React 的重新渲染不會重新建立回呼,除非你在 Hook 內使用useEffect或useCallback重新定義。
範例 1:簡單的旋轉方塊
import { Canvas, useFrame } from '@react-three/fiber';
import { Box } from '@react-three/drei';
function RotatingBox() {
// ref 用來取得 Three.js 物件
const meshRef = useRef();
// 每幀把方塊繞 Y 軸旋轉
useFrame((state, delta) => {
if (meshRef.current) {
meshRef.current.rotation.y += delta; // 依 delta 變化,速度恆定
}
});
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
);
}
export default function App() {
return (
<Canvas>
<ambientLight />
<RotatingBox />
</Canvas>
);
}
此範例示範 ref 結合
useFrame,讓方塊在每幀自動旋轉。
範例 2:使用 state.camera 控制相機
function CameraOrbit() {
const { camera } = useThree(); // 取得 R3F 狀態中的 camera
useFrame(({ clock }) => {
const t = clock.getElapsedTime();
// 繞 Y 軸做圓形軌道
camera.position.x = Math.sin(t) * 5;
camera.position.z = Math.cos(t) * 5;
camera.lookAt(0, 0, 0);
});
return null; // 這個 Hook 不需要渲染任何 JSX
}
透過
useThree取得全局狀態,讓相機在場景中「環繞」目標物件。
範例 3:多個 Hook 的執行順序
function First() {
useFrame(() => console.log('first'), 1); // priority 1
return null;
}
function Second() {
useFrame(() => console.log('second'), 2); // priority 2
return null;
}
輸出結果會是
first→second,因為優先權數字越小越先執行。
2. useLoader – 方便的資源載入
在 Three.js 中,我們通常使用 TextureLoader、GLTFLoader 等類別手動載入資源,並在載入完成後設定到材質或模型上。R3F 把這個流程包裝成 useLoader,讓 資源的載入、快取、錯誤處理 都變得像 React 的 Hook 一樣直覺。
- 簽名:
useLoader(LoaderClass, url, extensions?)LoaderClass:Three.js 的 Loader,例如THREE.TextureLoader、GLTFLoader。url:字串或字串陣列(支援多重載入)。extensions(可選):載入前要套用的擴充函式,例如useLoader.preload。
useLoader 會自動把載入的結果 快取 起來,若相同 URL 再次呼叫,會直接返回已緩存的物件,避免重複下載。
範例 4:載入貼圖並套用到平面
import { useLoader } from '@react-three/fiber';
import { TextureLoader } from 'three/src/loaders/TextureLoader';
function TexturedPlane() {
// 直接傳入 Loader class 與 URL
const texture = useLoader(TextureLoader, '/textures/wood.jpg');
return (
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[5, 5]} />
<meshStandardMaterial map={texture} />
</mesh>
);
}
範例 5:載入 GLTF 模型 + 進度條
import { useLoader, useProgress } from '@react-three/drei';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
function Model() {
const gltf = useLoader(GLTFLoader, '/models/house.glb');
return <primitive object={gltf.scene} />;
}
// 進度條元件
function LoadingBar() {
const { active, progress, errors, item, loaded, total } = useProgress();
return active ? (
<div style={{
position: 'absolute', top: 20, left: 20,
padding: '4px 8px', background: 'rgba(0,0,0,0.6)', color: '#fff'
}}>
Loading {item}: {Math.round(progress)}%
</div>
) : null;
}
useProgress為@react-three/drei提供的 Hook,能即時取得載入狀態,配合useLoader完成完整的 loading UI。
範例 6:一次載入多張貼圖(陣列寫法)
function MultiTextureBox() {
// 傳入陣列,返回相同順序的貼圖陣列
const [diffuse, normal] = useLoader(TextureLoader, [
'/textures/brick_diffuse.jpg',
'/textures/brick_normal.jpg',
]);
return (
<mesh>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial map={diffuse} normalMap={normal} />
</mesh>
);
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
Hook 內部直接使用 new THREE.Loader() |
每次渲染都會重新建立 Loader,導致快取失效、效能下降。 | 只在 Hook 呼叫時傳入 Loader Class(如 TextureLoader),R3F 內部會自動重用實例。 |
忘記在 useFrame 中檢查 ref.current |
若物件尚未掛載,ref.current 為 null,會拋出錯誤。 |
使用 if (ref.current) { … } 或 useRef(null!)(在 TypeScript 中)保護程式。 |
在 useLoader 中使用變數作為 URL,且變數頻繁變動 |
會觸發重新載入,產生閃爍或不必要的網路請求。 | 把 URL 放在 useMemo 或 useState 中,確保只有在真正需要時才改變。 |
在 useFrame 中直接改變 React state |
會造成每幀一次的重新渲染,嚴重拖慢效能。 | 只在必要時(如 UI 需要)使用 setState,或改用 useRef 暫存值。 |
| 忘記清除副作用 | 某些自訂動畫(如外部庫的 Tween)需要在組件卸載時手動取消。 | 在 useEffect 中回傳清除函式,或在 useFrame 裡檢查 mounted flag。 |
最佳實踐
使用
useRef儲存 Three.js 物件ref是唯一能直接操作底層物件的方式,避免不必要的 React re‑render。
把動畫邏輯寫在
useFrame,把資料變更寫在 React state- 如需根據使用者輸入改變動畫參數,先把參數存到
useRef,在useFrame中讀取。
- 如需根據使用者輸入改變動畫參數,先把參數存到
資源快取
useLoader內建快取,若需要手動預載(preload),可呼叫useLoader.preload(Loader, url)。
分離關注點
- 把「載入」與「渲染」拆成不同元件,例如
ModelLoader只負責useLoader,ModelViewer只負責useFrame。
- 把「載入」與「渲染」拆成不同元件,例如
使用
drei提供的輔助 HookuseGLTF,useTexture,useProgress等,都基於useLoader包裝,語意更清晰。
實際應用場景
| 場景 | 可能使用的 Hook | 範例說明 |
|---|---|---|
| 資料視覺化(3D 圖表) | useFrame 讓圖表隨時間旋轉、放大;useLoader 載入自訂材質或字型貼圖。 |
投資平台的 3D 柱狀圖,使用 useFrame 讓柱子根據即時資料滑動。 |
| 產品展示(e‑commerce) | useLoader 載入 GLTF、HDR 環境貼圖;useFrame 控制模型自動旋轉或根據滑鼠拖曳調整視角。 |
手機模型在網頁上可自行旋轉、點擊切換顏色貼圖。 |
| 遊戲原型 | useFrame 處理角色移動、碰撞偵測;useLoader 快速載入多個關卡的貼圖與音效。 |
以 R3F 為基礎的 3D 迷宮遊戲,使用 useFrame 追蹤玩家位置。 |
| AR/VR 互動 | useFrame 與 XR API 結合更新頭部追蹤;useLoader 載入 360° 環境圖。 |
WebXR 頁面中,使用 useFrame 讓虛擬物件隨使用者視角移動。 |
| 動態 UI(3D 按鈕、控制面板) | useFrame 製作 hover 動畫;useLoader 載入圖示貼圖。 |
3D 控制面板的滑桿在滑動時使用 useFrame 平滑過渡。 |
總結
useFrame把 渲染迴圈 變成 React Hook,讓每幀的更新可以直接寫在元件內,配合ref、useThree,即可操作相機、物件或全局狀態。useLoader把 資源載入 包裝成快取友好的 Hook,支援單一或多重載入,與@react-three/drei的輔助 Hook 結合,能快速完成貼圖、模型、環境光的載入與錯誤處理。- 正確使用
ref、避免在useFrame中直接觸發 React state、善用priority與快取機制,是提升效能與可維護性的關鍵。
掌握這兩個核心 Hook,您就能在 React 生態系 中自如地開發 3D 應用,從簡單的旋轉方塊到完整的產品展示、資料視覺化或 WebXR 體驗,都能以熟悉的 React 思維快速落地。祝您玩得開心,創造出令人驚艷的 Three.js + React 作品!