本文 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 —— useFrameuseLoader —— 分別負責 每幀更新資源載入。掌握它們的用法,能讓你在 React 生態系中快速構建互動式 3D 應用、資料視覺化或 AR/VR 原型,且程式碼更具可讀性與可維護性。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶領讀者一步步熟悉這兩個 Hook,並提供實務應用的參考方向。


核心概念

1. useFrame – 每幀執行的回呼

useFrame 是 R3F 提供的「渲染迴圈 Hook」,類似 Three.js 中的 renderer.setAnimationLooprequestAnimationFrame

  • 簽名useFrame((state, delta) => void, priority?)
    • state:包含 camera、scene、gl、mouse、clock 等資訊的 R3F 狀態物件。
    • delta:自上一幀的時間差(秒),適合做時間基礎的動畫。
    • priority(可選):數字越小表示越早執行,可用於控制多個 Hook 的執行順序。

重點useFrame 只在組件掛載時註冊一次,React 的重新渲染不會重新建立回呼,除非你在 Hook 內使用 useEffectuseCallback 重新定義。

範例 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;
}

輸出結果會是 firstsecond,因為優先權數字越小越先執行。


2. useLoader – 方便的資源載入

在 Three.js 中,我們通常使用 TextureLoaderGLTFLoader 等類別手動載入資源,並在載入完成後設定到材質或模型上。R3F 把這個流程包裝成 useLoader,讓 資源的載入、快取、錯誤處理 都變得像 React 的 Hook 一樣直覺。

  • 簽名useLoader(LoaderClass, url, extensions?)
    • LoaderClass:Three.js 的 Loader,例如 THREE.TextureLoaderGLTFLoader
    • 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.currentnull,會拋出錯誤。 使用 if (ref.current) { … }useRef(null!)(在 TypeScript 中)保護程式。
useLoader 中使用變數作為 URL,且變數頻繁變動 會觸發重新載入,產生閃爍或不必要的網路請求。 把 URL 放在 useMemouseState 中,確保只有在真正需要時才改變。
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 只負責 useLoaderModelViewer 只負責 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,讓每幀的更新可以直接寫在元件內,配合 refuseThree,即可操作相機、物件或全局狀態。
  • useLoader資源載入 包裝成快取友好的 Hook,支援單一或多重載入,與 @react-three/drei 的輔助 Hook 結合,能快速完成貼圖、模型、環境光的載入與錯誤處理。
  • 正確使用 ref、避免在 useFrame 中直接觸發 React state、善用 priority 與快取機制,是提升效能與可維護性的關鍵。

掌握這兩個核心 Hook,您就能在 React 生態系 中自如地開發 3D 應用,從簡單的旋轉方塊到完整的產品展示、資料視覺化或 WebXR 體驗,都能以熟悉的 React 思維快速落地。祝您玩得開心,創造出令人驚艷的 Three.js + React 作品!