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>,你只需要在子元件中使用useFrameHook 取得每幀回調。 - 事件(如
onClick、onPointerMove)會直接映射到 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.set、material.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提供的OrbitControls與PerspectiveCamera讓相機操作變得非常簡單,只要加上相應元件即可。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>
);
}
說明:
useGLTF是drei提供的便利 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 |
把不會變動的幾何體、材質抽離成獨立元件,使用 useMemo 或 React.memo |
| 事件無法觸發 | 物件不在相機視野、或光線投射 (raycaster) 被遮擋 | 確認相機與物件位置,或在 <Canvas> 加上 raycaster={{ computeOffsets: (event) => event.offsetX }} 以修正座標系統 |
其他實務建議
- 分離渲染與 UI:將 UI(如表單、按鈕)放在 Canvas 之外,避免 React 重新渲染導致不必要的 Three.js 重繪。
- 使用
drei套件:它提供許多常用的抽象元件(OrbitControls,Html,Environment),大幅減少樣板程式碼。 - 開啟
shadowMap:如果需要陰影,必須在<Canvas>加上shadows,且光源與物件都要設定castShadow/receiveShadow。 - 自訂渲染器:對於特殊需求(如 WebGL2、抗鋸齒),可以透過
<Canvas gl={{ antialias: true, alpha: false }} />直接傳遞渲染器參數。 - 資源釋放:使用
useLoader或useGLTF時,React 會自動在 component unmount 時釋放緩衝區,但若自行創建Texture、BufferGeometry,務必在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-fiber 把 Three.js 的渲染抽象成 React 元件,讓我們在 JSX 中直接宣告 Canvas、相機、光源與模型。
本文從最基礎的 <Canvas> 建立開始,說明了相機設定、渲染循環、事件綁定等核心概念,並提供 5 個實用範例,涵蓋靜態模型、動畫、GLTF 載入、狀態共享等情境。
在實務開發時,記得:
- 為 Canvas 明確設定尺寸與背景。
- 使用
drei來簡化控制器與輔助元件。 - 依賴 React 的 props 與 state 來驅動 3D 物件的變化,避免直接在渲染迴圈裡手動更新。
- 注意效能:
useMemo、React.memo、InstancedMesh 等技巧能顯著降低 CPU/GPU 負擔。
掌握了 在 React 中建立 Canvas 的技巧後,你就能將 3D 互動自然地融合於現代 Web 應用,為使用者帶來更豐富、更沉浸的體驗。祝你玩得開心,創造出令人驚嘆的 Three.js + React 作品! 🚀