本文 AI 產出,尚未審核

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。

最佳實踐

  1. 使用 useRef 儲存 Three.js 物件

    • ref 是唯一能直接操作底層物件的方式,避免不必要的 React re‑render。
  2. 把動畫邏輯寫在 useFrame,把資料變更寫在 React state

    • 如需根據使用者輸入改變動畫參數,先把參數存到 useRef,在 useFrame 中讀取。
  3. 資源快取

    • useLoader 內建快取,若需要手動預載(preload),可呼叫 useLoader.preload(Loader, url)。
  4. 分離關注點

    • 把「載入」與「渲染」拆成不同元件,例如 ModelLoader 只負責 useLoader,ModelViewer 只負責 useFrame。
  5. 使用 drei 提供的輔助 Hook

    • useGLTF, 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 作品!