本文 AI 產出,尚未審核

Three.js 與 React 整合(react‑three‑fiber)── 在 React 中建立 Canvas

簡介

在 Web 前端開發中,Three.js 已成為製作 3D 互動內容的事實標準,而 React 則是現代 UI 開發的主要框架。當兩者結合時,我們可以在 React 元件樹中直接管理 3D 場景,讓 UI 與 3D 渲染保持同步,開發流程更自然、更易維護。

react-three-fiber(簡稱 R3F)正是為此而生的 React renderer,它把 Three.js 的 API 包裝成 React 元件,使得 在 JSX 中宣告 Canvas、相機、光源、模型等 成為可能。掌握如何在 React 中正確建立 Canvas,便是開啟 R3F 世界的第一步,也是後續動畫、互動、效能優化的基礎。

本文將從 Canvas 的基本使用 出發,說明核心概念、提供多個實作範例,並分享常見陷阱與最佳實踐,讓你能快速在 React 專案中嵌入 Three.js 3D 場景。


核心概念

1. <Canvas> 元件:R3F 的根容器

  • <Canvas> 取代了原生的 <canvas> 元素,負責建立 Three.js 的渲染器、相機、渲染循環等。
  • 它是一個 React 元件,可以接受許多屬性(props)來客製化渲染器設定,例如 shadows, camera, gl 等。
  • 任何放在 <Canvas> 內的子元件,都會自動成為 Three.js 場景的子節點。
import { Canvas } from '@react-three/fiber';

function App() {
  return (
    <Canvas>
      {/* 3D 內容放這裡 */}
    </Canvas>
  );
}

2. 內建相機與自訂相機

  • 預設情況下,R3F 會在 <Canvas> 內建立一個 PerspectiveCamera,位置為 (0,0,5)
  • 若需要特殊的相機參數(如 FOV、近遠剪裁面),可以透過 camera 屬性傳入設定物件,或自行宣告 <PerspectiveCamera> 元件。
<Canvas camera={{ position: [0, 2, 10], fov: 60 }}>
  {/* 你的場景 */}
</Canvas>

3. 事件與渲染循環

  • R3F 自動將渲染循環(requestAnimationFrame)掛在 <Canvas>,你只需要在子元件中使用 useFrame Hook 取得每幀回調。
  • 事件(如 onClickonPointerMove)會直接映射到 Three.js 物件,且支援 Raycaster 內建的命中檢測。
import { useFrame } from '@react-three/fiber';

function RotatingBox() {
  const meshRef = useRef();
  useFrame((state, delta) => {
    meshRef.current.rotation.x += delta;
    meshRef.current.rotation.y += delta;
  });
  return (
    <mesh ref={meshRef} onClick={() => console.log('Box clicked!')}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color="orange" />
    </mesh>
  );
}

4. 結合 React 生命週期

  • 因為 R3F 完全遵循 React 的渲染模型,props 改變會自動觸發 Three.js 物件更新
  • 例如改變顏色、尺寸或位置,只要在 JSX 中更新對應的屬性即可,R3F 會在底層呼叫 object3D.position.setmaterial.color.set 等。
function ColorfulSphere({ color = 'royalblue' }) {
  return (
    <mesh>
      <sphereGeometry args={[1, 32, 32]} />
      <meshStandardMaterial color={color} />
    </mesh>
  );
}

程式碼範例

範例 1:最小可運行的 Canvas

import React from 'react';
import { Canvas } from '@react-three/fiber';

export default function SimpleScene() {
  return (
    <Canvas style={{ height: '400px', background: '#272727' }}>
      {/* 簡單的環境光 */}
      <ambientLight intensity={0.5} />
      {/* 方向光 */}
      <directionalLight position={[5, 5, 5]} intensity={1} />
      {/* 立方體 */}
      <mesh rotation={[0.4, 0.2, 0]}>
        <boxGeometry args={[1, 1, 1]} />
        <meshStandardMaterial color="tomato" />
      </mesh>
    </Canvas>
  );
}

說明:此範例只使用 <Canvas>、光源與一個立方體,展示 R3F 的「宣告式」寫法。style 用於調整 Canvas 大小與背景色。


範例 2:自訂相機與軌道控制(OrbitControls)

import React from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls, PerspectiveCamera } from '@react-three/drei';

export default function CameraDemo() {
  return (
    <Canvas>
      {/* 透過 drei 套件引入的相機 */}
      <PerspectiveCamera makeDefault position={[0, 2, 8]} fov={70} />
      {/* 讓使用者可以拖曳、縮放視角 */}
      <OrbitControls enablePan={false} maxPolarAngle={Math.PI / 2} />
      <ambientLight intensity={0.3} />
      <directionalLight position={[5, 5, 5]} intensity={0.8} />
      {/* 多個幾何體示範 */}
      <mesh position={[-2, 0, 0]}>
        <boxGeometry args={[1, 1, 1]} />
        <meshStandardMaterial color="skyblue" />
      </mesh>
      <mesh position={[2, 0, 0]}>
        <sphereGeometry args={[0.7, 32, 32]} />
        <meshStandardMaterial color="goldenrod" />
      </mesh>
    </Canvas>
  );
}

說明@react-three/drei 提供的 OrbitControlsPerspectiveCamera 讓相機操作變得非常簡單,只要加上相應元件即可。makeDefault 讓此相機成為全局預設相機。


範例 3:使用 useFrame 產生持續動畫

import React, { useRef } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';

function SpinningTorus() {
  const torusRef = useRef();

  // 每一幀執行一次
  useFrame((state, delta) => {
    torusRef.current.rotation.x += delta * 0.5;
    torusRef.current.rotation.y += delta * 0.3;
  });

  return (
    <mesh ref={torusRef} position={[0, 0, 0]}>
      <torusGeometry args={[1, 0.4, 16, 100]} />
      <meshStandardMaterial color="#ff69b4" metalness={0.6} roughness={0.2} />
    </mesh>
  );
}

export default function AnimatedScene() {
  return (
    <Canvas>
      <ambientLight intensity={0.4} />
      <directionalLight position={[3, 5, 2]} intensity={1} />
      <SpinningTorus />
    </Canvas>
  );
}

說明useFrame 是 R3F 提供的 Hook,類似於 Three.js 中的 animate 迴圈。透過 delta(上一幀與本幀的時間差)可讓動畫保持時間一致性。


範例 4:動態載入 GLTF 模型與 Suspense

import React, { Suspense } from 'react';
import { Canvas } from '@react-three/fiber';
import { useGLTF, Html } from '@react-three/drei';

function Model({ url }) {
  const { scene } = useGLTF(url);
  return <primitive object={scene} scale={0.5} />;
}

export default function ModelScene() {
  return (
    <Canvas>
      <ambientLight intensity={0.6} />
      <directionalLight position={[10, 10, 5]} intensity={1} />
      {/* 使用 Suspense 讓模型載入時顯示 Loading */}
      <Suspense fallback={<Html center>Loading…</Html>}>
        <Model url="/models/damaged_helmet.gltf" />
      </Suspense>
    </Canvas>
  );
}

說明useGLTFdrei 提供的便利 Hook,會自動快取模型。配合 React 的 Suspense,可以在模型載入期間顯示自訂的 loading UI,提升使用者體驗。


範例 5:在多個 Canvas 中共享相同的渲染狀態

import React, { useState } from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';

function SharedBox({ color }) {
  return (
    <mesh>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={color} />
    </mesh>
  );
}

export default function DualCanvas() {
  const [boxColor, setBoxColor] = useState('teal');

  return (
    <>
      <button onClick={() => setBoxColor('orange')}>改變顏色</button>

      {/* 第一個 Canvas */}
      <Canvas style={{ width: '45%', height: '400px', display: 'inline-block' }}>
        <ambientLight intensity={0.5} />
        <OrbitControls />
        <SharedBox color={boxColor} />
      </Canvas>

      {/* 第二個 Canvas */}
      <Canvas style={{ width: '45%', height: '400px', display: 'inline-block', marginLeft: '5%' }}>
        <ambientLight intensity={0.5} />
        <OrbitControls />
        <SharedBox color={boxColor} />
      </Canvas>
    </>
  );
}

說明:React 的 state 可以跨 Canvas 共享,只要把需要變動的屬性(如 color)作為 props 傳入即可。這在 多視角同步分屏渲染 時非常有用。


常見陷阱與最佳實踐

常見問題 可能原因 解決方案 / Best Practice
Canvas 沒有顯示 父容器高度為 0、或未設定 CSS <Canvas> 加上明確的 width / height,或使用 style={{ flex: 1 }}
模型載入失敗 路徑錯誤、未設定 public 靜態目錄 確認檔案位於 public 或透過 Webpack import,並在 useGLTF 中使用正確 URL
相機位置不正確 預設相機被其他元件覆寫 使用 makeDefault 明確指定相機,或在 <Canvas>camera props 中設定
渲染效能低 每幀大量重建物件、未使用 React.memo 把不會變動的幾何體、材質抽離成獨立元件,使用 useMemoReact.memo
事件無法觸發 物件不在相機視野、或光線投射 (raycaster) 被遮擋 確認相機與物件位置,或在 <Canvas> 加上 raycaster={{ computeOffsets: (event) => event.offsetX }} 以修正座標系統

其他實務建議

  1. 分離渲染與 UI:將 UI(如表單、按鈕)放在 Canvas 之外,避免 React 重新渲染導致不必要的 Three.js 重繪。
  2. 使用 drei 套件:它提供許多常用的抽象元件(OrbitControls, Html, Environment),大幅減少樣板程式碼。
  3. 開啟 shadowMap:如果需要陰影,必須在 <Canvas> 加上 shadows,且光源與物件都要設定 castShadow / receiveShadow
  4. 自訂渲染器:對於特殊需求(如 WebGL2、抗鋸齒),可以透過 <Canvas gl={{ antialias: true, alpha: false }} /> 直接傳遞渲染器參數。
  5. 資源釋放:使用 useLoaderuseGLTF 時,React 會自動在 component unmount 時釋放緩衝區,但若自行創建 TextureBufferGeometry,務必在 useEffect 的 cleanup 中呼叫 dispose()

實際應用場景

場景 為何使用 <Canvas> + R3F 範例簡述
產品 3D 展示 需要即時旋轉、縮放、光影效果;React 可同時管理商品資訊的 UI。 商品頁面左側嵌入 <Canvas>,右側顯示規格表,透過 state 變更顏色或材質。
資料視覺化 大量點雲或三維圖表,渲染效能高;React 負責過濾、時間序列切換。 使用 InstancedMesh 渲染上千筆資料點,React 控制時間滑桿更新 instanceMatrix
互動式教學 需要結合表單、文字說明與 3D 示範,保持 UI/3D 同步。 語言學習 APP 中,點擊選項改變 3D 角色的表情與姿勢。
遊戲或 AR/VR 雛形 React 的狀態管理方便實作遊戲邏輯,R3F 提供 3D 渲染基礎。 小型迷宮遊戲:使用 useKeyboardControls 捕捉鍵盤輸入,更新相機位置。
儀表板 (Dashboard) 需要在同一頁面展示 2D UI 與 3D KPI 模型。 在管理平台的右上角放置一個旋轉的 3D 齒輪指示系統負載。

總結

react-three-fiberThree.js 的渲染抽象成 React 元件,讓我們在 JSX 中直接宣告 Canvas、相機、光源與模型。
本文從最基礎的 <Canvas> 建立開始,說明了相機設定、渲染循環、事件綁定等核心概念,並提供 5 個實用範例,涵蓋靜態模型、動畫、GLTF 載入、狀態共享等情境。

在實務開發時,記得:

  1. 為 Canvas 明確設定尺寸與背景。
  2. 使用 drei 來簡化控制器與輔助元件。
  3. 依賴 React 的 propsstate 來驅動 3D 物件的變化,避免直接在渲染迴圈裡手動更新。
  4. 注意效能:useMemoReact.memo、InstancedMesh 等技巧能顯著降低 CPU/GPU 負擔。

掌握了 在 React 中建立 Canvas 的技巧後,你就能將 3D 互動自然地融合於現代 Web 應用,為使用者帶來更豐富、更沉浸的體驗。祝你玩得開心,創造出令人驚嘆的 Three.js + React 作品! 🚀