React Three Fiber 最佳實踐
基於 2025/5 知識。版本基準:three ^0.170、@react-three/fiber ^9、@react-three/drei ^10。
安裝
pnpm add three @react-three/fiber @react-three/drei
pnpm add -D @types/three
基本 Canvas
import { Canvas } from '@react-three/fiber'
import { OrbitControls, Environment } from '@react-three/drei'
<Canvas camera={{ position: [5, 5, 5], fov: 50 }} dpr={[1, 2]} shadows>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 10, 5]} castShadow />
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
<OrbitControls />
</Canvas>
核心 Hooks
useFrame((state, delta) => {
// 每幀執行,不要 setState!用 ref 直接改 mesh
ref.current.rotation.y += delta
})
const { camera, gl, scene, size } = useThree()
drei 常用
OrbitControls/TrackballControls:相機控制PerspectiveCamera makeDefault:自訂主相機Environment preset="city":HDR 環境光Bounds:自動 fit 物件入鏡Html:3D 裡嵌 DOM( tooltip/label)useGLTF('/model.glb'):載 GLTF,記得useGLTF.preload()Instances/Instance:大量重複物件用 instancing
互動事件
<mesh
=> { e.stopPropagation(); /* e.point, e.face, e.object */ }}
=> setHover(true)}
=> setSelected(null)}
>
事件會冒泡,要 stopPropagation() 阻止穿透。onPointerMissed 在點空白處觸發(適合取消選取)。
群組旋轉(魔術方塊關鍵)
// 把 9 個 cubie 暫時 attach 到一個 Group,旋轉 Group,動畫完再 detach
const group = useRef<THREE.Group>(null!)
const pivot = useRef<THREE.Object3D>(new THREE.Object3D())
function rotateFace(cubies: THREE.Object3D[], axis: 'x'|'y'|'z', angle: number) {
cubies.forEach(c => pivot.current.attach(c))
// useFrame 內 lerp pivot.rotation[axis] 到 angle
// 完成後:cubies.forEach(c => scene.attach(c)) 把世界變換烘回各 cubie
}
關鍵:attach()(非 add())會保留世界座標。
效能陷阱
- useFrame 裡 setState:會觸發 React render,每幀 60 次 → 災難。用 ref 直接操作。
- 每幀 new 物件:
new Vector3()放元件頂層或 useMemo,不要 useFrame 內建。 - 同 geometry/material 重複建:extract 成常數或 useMemo。
- shadow 開太多光源:每個 castShadow 光源一次深度 pass。
- dpr 固定 2:改
dpr={[1, 2]}讓 R3F 自適應。 - 大 scene 不用 instancing:>100 重複物件請用
<Instances>。 - HMR after geometry change:有時要 key prop 強制 remount。
動畫
- 簡單 lerp:useFrame +
THREE.MathUtils.lerp - 複雜:
@react-spring/three(declarative)或framer-motion-3d - Tween:自己 state machine + useFrame
TypeScript
R3F v9 以後 primitive 元素型別在 @react-three/fiber 的 ThreeElements 已正確。若要擴充自訂元素:
import { extend } from '@react-three/fiber'
import { MyMaterial } from './myMaterial'
extend({ MyMaterial })
declare module '@react-three/fiber' {
interface ThreeElements { myMaterial: ThreeElement<typeof MyMaterial> }
}
測試策略
R3F 元件難 unit test → 把 狀態/邏輯 拆出純函式測(如魔術方塊的 rotate 邏輯),元件只負責渲染。必要時用 @react-three/test-renderer。