フック
R3F が提供する主要フックは 4つ。useFrame / useThree /
useLoader / useGraph。これと React 標準の useRef/useEffect/useState
さえあれば 9 割の処理が書ける。すべて Canvas の中でしか動かないのが最重要ルール。
R3F のフックは <Canvas> が提供するコンテキストに依存する。
Canvas の外のコンポーネントから使うとエラー。
Canvas に渡すコンポーネント(<Canvas><Foo /></Canvas> の Foo)の中なら OK。
useFrame
毎フレーム呼ばれるコールバックを登録する。これが Three.js の setAnimationLoop 相当。
コールバックは (state, delta, xrFrame) => void。
state— Canvas のグローバル状態({ camera, scene, gl, clock, mouse, ... })delta— 前フレームから経過した秒数(フレームレート非依存の計算に使う)xrFrame— WebXR セッション時のみ
function SpinningBox() {
const ref = useRef<THREE.Mesh>(null!)
useFrame((_, dt) => {
ref.current.rotation.x += dt * 0.5
ref.current.rotation.y += dt * 0.8
})
return (
<mesh ref={ref}>
<boxGeometry />
<meshStandardMaterial color="#7dd3fc" />
</mesh>
)
}
setState しない
useFrame は60fpsで呼ばれる。中で setState すると毎フレーム
React の再レンダーが走り、即座にパフォーマンスが死ぬ。
値を保持したいだけならuseRef に書くか、Three.js のオブジェクトを直接
mutate するのが正解。
state の中身
| キー | 意味 |
|---|---|
gl | WebGLRenderer |
scene | ルート Scene |
camera | 現在のカメラ |
raycaster | 共有 Raycaster |
clock | THREE.Clock。経過時間など |
mouse / pointer | NDC(-1..1)の現在ポインタ位置 |
size | { width, height, top, left } 物理サイズ |
viewport | ワールド座標サイズ + DPR |
events | イベントマネージャ |
set(partial) | state の更新(カメラ差し替え等) |
get() | 最新の state を同期取得(zustand 由来) |
invalidate() | 1フレーム描画を要求(demand モード時) |
advance(time, runGlobalEffects?) | 手動でフレームを進める |
performance.regress() | 性能落ちを通知(drei の AdaptiveDpr 等が反応) |
実行順を制御する(priority)
第2引数に number を渡すと、レンダリングの前後や、複数の useFrame の順序を制御できる。
- priority = 0(既定) — デフォルトのレンダリングが走る前に呼ばれる
- priority > 0 — レンダリングの後に呼ばれる。複数あるなら昇順
- priority < 0 — R3F の自動レンダリングを無効化。自分で
gl.render()しないと描画されない(multi-pass などに)
useFrame(updateA, 0) // レンダー前
useFrame(updateB, 1) // レンダー後(昇順なので C より先)
useFrame(updateC, 2) // レンダー後
// 自動レンダーを止めて、自前で2回 render
useFrame(({ gl, scene, camera }) => {
gl.autoClear = true
gl.render(scene, camera)
gl.autoClear = false
gl.render(overlayScene, camera)
}, 1)
// priority < 0 にすると 1 回目の自動レンダーも止まる
useFrame((state) => { /* setup before */ }, -1)
パフォーマンス: ムダなアロケーションを避ける
毎フレーム new Vector3() しないこと。useRef で使い回すのが定石。
const tmp = useRef(new THREE.Vector3()).current
useFrame((state) => {
tmp.set(state.mouse.x, state.mouse.y, 0).unproject(state.camera)
// tmp を使って計算
})
// 複数必要なら useMemo にまとめる
const helpers = useMemo(() => ({
tmp: new THREE.Vector3(),
matrix: new THREE.Matrix4(),
quat: new THREE.Quaternion(),
}), [])
条件付き有効化
useFrame 自体に条件を付けることはできないが、コールバック内で早期 returnするのが定石。
useFrame(() => {
if (!enabled) return
// 重い処理
})
useFrame をフックに切り出す
function useSpin(ref: RefObject<THREE.Object3D>, speed = 1) {
useFrame((_, dt) => {
if (ref.current) ref.current.rotation.y += dt * speed
})
}
function Box() {
const ref = useRef<THREE.Mesh>(null!)
useSpin(ref, 0.5)
return <mesh ref={ref}>...</mesh>
}
useThree
Canvas のグローバル状態にアクセスする。中身は useFrame の state と同じ。
レンダー時に必要なら useThree、フレーム駆動なら useFrameと覚えるのが楽。
全部取る
const state = useThree()
state.camera // 現在のカメラ
state.gl // WebGLRenderer
state.size // { width, height, top, left }
state.viewport // ワールド単位のサイズ
セレクタで購読する(再レンダー最小化)
useThree(state => ...) の形でその値が変わった時だけ再レンダーされる。
zustand を内部で使っている。
// size 全体ではなく width だけ購読
const width = useThree((s) => s.size.width)
// camera だけ購読(変わらないので実質一度だけ)
const camera = useThree((s) => s.camera)
// 複数値を組み合わせる
const aspect = useThree((s) => s.size.width / s.size.height)
イベントの外から state にアクセス: get()
useFrame 以外のイベントコールバック内で「最新の state を一発取りたい」場合。
const get = useThree((s) => s.get)
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key !== " ") return
const { camera } = get()
camera.position.z -= 1 // ジャンプ
}
window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey)
}, [get])
state を変える: set()
カメラを差し替える、events を変えるなど。
const set = useThree((s) => s.set)
set({ camera: newCamera })
set((prev) => ({ ...prev, frameloop: "demand" }))
サイズ変化に反応
function ResizeAwareCamera() {
const camera = useThree(s => s.camera)
const aspect = useThree(s => s.size.width / s.size.height)
useEffect(() => {
if (camera instanceof THREE.PerspectiveCamera) {
camera.aspect = aspect
camera.updateProjectionMatrix()
}
}, [camera, aspect])
return null
}
外部から Canvas に触るには
Canvas の外では useThree が使えない。
Canvas の中に「ブリッジ」コンポーネントを置いて外側の状態と繋ぐパターンか、Canvas の onCreated で外部参照を注入する:
const apiRef = useRef<ThreeState | null>(null)
return (
<>
<Canvas onCreated={(state) => { apiRef.current = state }}>
...
</Canvas>
{/* 外側のボタン等から */}
<button onClick={() => apiRef.current?.gl.setClearColor("red")}>赤</button>
</>
)
または、Zustand のグローバル store を Canvas 内外で共有するのが綺麗。
useLoader
Three.js のローダー(TextureLoader、GLTFLoader、FontLoader 等)を
Suspense と統合して呼ぶ。同じパスは内部でキャッシュされる。
import { useLoader } from "@react-three/fiber"
import * as THREE from "three"
function Wall() {
const tex = useLoader(THREE.TextureLoader, "/wall.jpg")
return (
<mesh>
<planeGeometry args={[3, 3]} />
<meshStandardMaterial map={tex} />
</mesh>
)
}
// Canvas 側で Suspense を入れる
<Canvas>
<Suspense fallback={null}>
<Wall />
</Suspense>
</Canvas>
同時に複数読み込む
パスを配列で渡すと、対応する結果の配列が返る。
const [tex1, tex2, tex3] = useLoader(THREE.TextureLoader, [
"/a.jpg", "/b.jpg", "/c.jpg",
])
ローダーを設定する(DRACO 付き GLTF など)
第3引数にextension functionを渡してローダーを設定。
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"
const gltf = useLoader(GLTFLoader, "/compressed.glb", (loader) => {
const draco = new DRACOLoader().setDecoderPath("/draco/")
loader.setDRACOLoader(draco)
})
preload でキャッシュ温め
useLoader.preload(loader, paths) で先読みできる。マウント前に裏で取りに行ける。
drei の useGLTF も同様の preload を持つ。
// アプリの起動直後にコール
useLoader.preload(THREE.TextureLoader, "/hero.jpg")
useGLTF.preload("/robot.glb")
キャッシュをクリア
import { useLoader } from "@react-three/fiber"
useLoader.clear(THREE.TextureLoader, "/wall.jpg")
エラーハンドリング
ローダー失敗時は Suspense とエラーバウンダリでキャッチ:
<ErrorBoundary fallback={<Text>読み込み失敗</Text>}>
<Suspense fallback={<Loading />}>
<Model />
</Suspense>
</ErrorBoundary>
useGraph
GLTF などで読み込んだシーンを名前で引ける形に展開するヘルパー。
{ nodes, materials } が返る。
import { useGraph } from "@react-three/fiber"
import { useGLTF } from "@react-three/drei"
function Robot() {
const { scene } = useGLTF("/robot.glb")
const { nodes, materials } = useGraph(scene)
return (
<group>
<mesh
geometry={(nodes.Body as THREE.Mesh).geometry}
material={materials.MetalGray}
/>
<mesh
geometry={(nodes.Head as THREE.Mesh).geometry}
material={materials.SkinTone}
/>
</group>
)
}
とはいえ、実用では gltfjsx で GLTF から JSX を自動生成するのが一番楽。生成されたコードはそのまま貼って使える。
その他のフック・ユーティリティ
drei の頻出フック
useGLTF(path)— GLTF 読み込み(Suspense)useTexture(paths)— テクスチャ(複数 OK)useFBX(path)— FBX 読み込みuseAnimations(animations, ref)— AnimationMixer 管理useCursor(hovered, onHover, onLeave)— ホバーカーソル切り替えuseScroll()— ScrollControls 内のスクロール量useKeyboardControls()— KeyboardControls 内のキー入力useHelper(ref, HelperClass, ...args)— ヘルパー(軸・カメラ・ライト等)useEnvironment(opts)— HDR 環境光の動的読み込みuseDetectGPU()— GPU 性能を判定
useFrame と useEffect の使い分け
| やりたいこと | 使うもの |
|---|---|
| 毎フレーム動かす | useFrame |
| マウント時に1回だけセットアップ | useEffect with empty deps |
| props 変化に反応 | useEffect with deps |
| state 変化を読みたいだけ | useThree セレクタ |
| 外部の DOM イベントに反応 | useEffect + addEventListener |
| 非同期にロードして待ちたい | useLoader + Suspense |
カスタムフックのパターン
マウス追従
function useFollowMouse(strength = 1) {
const ref = useRef<THREE.Object3D>(null!)
const v = useRef(new THREE.Vector3()).current
useFrame((state) => {
if (!ref.current) return
v.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera)
ref.current.position.lerp(v, 0.1 * strength)
})
return ref
}
function Cursor() {
const ref = useFollowMouse()
return (
<mesh ref={ref}>
<sphereGeometry args={[0.05]} />
<meshBasicMaterial color="white" />
</mesh>
)
}
サイズに応じてカメラ調整
function useResponsiveCamera(targetWidth = 5) {
const camera = useThree(s => s.camera) as THREE.PerspectiveCamera
const aspect = useThree(s => s.size.width / s.size.height)
useEffect(() => {
const desiredFov = 2 * Math.atan((targetWidth / 2) / camera.position.z) * (180 / Math.PI)
camera.fov = desiredFov
camera.aspect = aspect
camera.updateProjectionMatrix()
}, [camera, aspect, targetWidth])
}
キーボード入力
function useKey(key: string) {
const pressed = useRef(false)
useEffect(() => {
const down = (e: KeyboardEvent) => { if (e.key === key) pressed.current = true }
const up = (e: KeyboardEvent) => { if (e.key === key) pressed.current = false }
window.addEventListener("keydown", down)
window.addEventListener("keyup", up)
return () => {
window.removeEventListener("keydown", down)
window.removeEventListener("keyup", up)
}
}, [key])
return pressed
}
function Player() {
const ref = useRef<THREE.Mesh>(null!)
const w = useKey("w"), s = useKey("s"), a = useKey("a"), d = useKey("d")
useFrame((_, dt) => {
if (!ref.current) return
if (w.current) ref.current.position.z -= 5 * dt
if (s.current) ref.current.position.z += 5 * dt
if (a.current) ref.current.position.x -= 5 * dt
if (d.current) ref.current.position.x += 5 * dt
})
return <mesh ref={ref}> ... </mesh>
}
まとめ: どのフックを使うか
| やりたいこと | 使うフック |
|---|---|
| 毎フレーム動かす | useFrame |
| カメラ・レンダラ・シーンに触る | useThree |
| テクスチャ・GLTF・フォントを読む | useLoader または drei の useGLTF / useTexture |
| GLTF を name で引きたい | useGraph |
| 外部の状態と連動 | 普通の zustand / Redux / Context |
| マウント時の初期化 | useEffect |