react-three-fiber について
React Three Fiberを使ってReactアプリに3D体験を構築するためのスキルです。Three.jsの宣言的レンダラーを扱い、シーン構成、フック、アセット読み込み、操作、パフォーマンス、再利用可能なスターター素材を解説します。
主な機能
- 01JSXでThree.jsシーンを構成
- 02Reactフックで3D状態を扱う
- 03アセットの読み込みとキャッシュ
- 04操作可能な3Dオブジェクトの作成
- 05レンダリング最適化の適用
- 06コンポーネントとシーンのひな形生成
活用シーン
- 01Reactアプリへの3Dシーン追加
- 02操作可能な製品展示の制作
- 03Reactでのゲームやシミュレーション開発
- 04再利用可能な3Dコンポーネントの開発
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ freshtechbro/ claudedesignskills --skill react-three-fiberインストール後、Claude Code で/react-three-fiberを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/examples/README.mdassets/starter_r3f/index.htmlpackage.jsonREADME.mdvite.config.jsassets/starter_r3f/src/App.jsxExperience.jsxmain.jsxassets/starter_r3f/src/components/Box.jsxSphere.jsxreferences/api_reference.mdscripts/component_generator.pyscene_setup.py詳細
React Three Fiber
概要
React Three Fiber(R3F)は Three.js 用の React レンダラーで、宣言的なコンポーネントベースの 3D 開発を React アプリケーションにもたらします。Three.js オブジェクトを命令的に作成・管理する代わりに、Three.js オブジェクトに直接対応する JSX コンポーネントを使って 3D シーンを構築します。
このスキルを使用する場面:
- React アプリケーション内で 3D 体験を構築する場合
- インタラクティブな製品コンフィギュレーターやショーケースを作成する場合
- 3D ポートフォリオ、ギャラリー、ストーリーテリング体験を開発する場合
- React でゲームやシミュレーションを構築する場合
- 既存の React プロジェクトに 3D 要素を追加する場合
- 3D グラフィックスで状態管理や React フックが必要な場合
- React フレームワーク(Next.js、Gatsby、Remix)を使用する場合
主なメリット:
- 宣言的:React コンポーネントのように 3D シーンを記述できます
- React との統合:フック、コンテキスト、状態管理をすべて利用できます
- 再利用性:3D コンポーネントライブラリを作成して共有できます
- パフォーマンス:レンダリングの最適化と差分調整が自動で行われます
- エコシステム:Drei ヘルパー、Zustand、Framer Motion などと連携します
- TypeScript のサポート:Three.js オブジェクトの型安全性を完全に確保できます
基本概念
1. Canvas コンポーネント
<Canvas> コンポーネントは、Three.js のシーン、カメラ、レンダラー、レンダーループをセットアップします。
import { Canvas } from '@react-three/fiber'
function App() {
return (
<Canvas
camera={{ position: [0, 0, 5], fov: 75 }}
gl={{ antialias: true }}
dpr={[1, 2]}
>
{/* 3D content goes here */}
</Canvas>
)
}
Canvas のプロパティ:
camera- カメラの設定(位置、fov、near、far)gl- WebGL レンダラーの設定dpr- デバイスのピクセル比(デフォルト:[1, 2])shadows- シャドウマッピングを有効にします(デフォルト:false)frameloop- 「always」(デフォルト)、「demand」、または「never」flat- よりシンプルな色にするため、カラー管理を無効にしますlinear- sRGB の代わりにリニアカラースペースを使用します
2. 宣言的な 3D オブジェクト
Three.js オブジェクトは、ケバブケースのプロパティを使った JSX で作成します。
// THREE.Mesh + THREE.BoxGeometry + THREE.MeshStandardMaterial
<mesh position={[0, 0, 0]} rotation={[0, Math.PI / 4, 0]}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
プロパティの対応関係:
position→object.position.set(x, y, z)rotation→object.rotation.set(x, y, z)scale→object.scale.set(x, y, z)args→ ジオメトリ/マテリアルのコンストラクター引数attach→ 親のプロパティにアタッチします(例:attach="material")
省略記法:
// Full notation
<mesh position={[1, 2, 3]} />
// Axis-specific (dash notation)
<mesh position-x={1} position-y={2} position-z={3} />
3. useFrame フック
毎フレーム(アニメーションループ)でコードを実行します。
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'
function RotatingBox() {
const meshRef = useRef()
useFrame((state, delta) => {
// Rotate mesh on every frame
meshRef.current.rotation.x += delta
meshRef.current.rotation.y += delta * 0.5
// Access scene state
const time = state.clock.elapsedTime
meshRef.current.position.y = Math.sin(time) * 2
})
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
)
}
useFrame のパラメーター:
state- シーンの状態(camera、scene、gl、clock など)delta- 前回のフレームからの経過時間(フレームレートに依存しない処理に使用)xrFrame- XR フレームデータ(VR/AR 用)
重要:useFrame 内では setState を使用しないでください。不要な再レンダリングが発生します。
4. useThree フック
シーンの状態とメソッドにアクセスします。
import { useThree } from '@react-three/fiber'
function CameraInfo() {
const { camera, gl, scene, size, viewport } = useThree()
// Selective subscription (only re-render on size change)
const size = useThree((state) => state.size)
// Get state non-reactively
const get = useThree((state) => state.get)
const freshState = get() // Latest state without triggering re-render
return null
}
利用可能な状態:
camera- デフォルトのカメラscene- Three.js のシーンgl- WebGL レンダラーsize- Canvas の寸法viewport- 3D 単位でのビューポートの寸法clock- Three.js のクロックpointer- 正規化されたマウス座標invalidate()- レンダリングを手動でトリガーしますsetSize()- Canvas のサイズを手動で変更します
5. useLoader フック
自動キャッシュと Suspense との連携により、アセットを読み込みます。
import { Suspense } from 'react'
import { useLoader } from '@react-three/fiber'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { TextureLoader } from 'three'
function Model() {
const gltf = useLoader(GLTFLoader, '/model.glb')
return <primitive object={gltf.scene} />
}
function TexturedMesh() {
const texture = useLoader(TextureLoader, '/texture.jpg')
return (
<mesh>
<boxGeometry />
<meshStandardMaterial map={texture} />
</mesh>
)
}
function App() {
return (
<Canvas>
<Suspense fallback={<LoadingIndicator />}>
<Model />
<TexturedMesh />
</Suspense>
</Canvas>
)
}
複数のアセットの読み込み:
const [texture1, texture2, texture3] = useLoader(TextureLoader, [
'/tex1.jpg',
'/tex2.jpg',
'/tex3.jpg'
])
ローダーの拡張:
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
useLoader(GLTFLoader, '/model.glb', (loader) => {
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/')
loader.setDRACOLoader(dracoLoader)
})
事前読み込み:
// Pre-load assets before component mounts
useLoader.preload(GLTFLoader, '/model.glb')
よく使われるパターン
パターン 1:基本的なシーンのセットアップ
import { Canvas } from '@react-three/fiber'
function Scene() {
return (
<>
{/* Lights */}
<ambientLight intensity={0.5} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
{/* Objects */}
<mesh position={[0, 0, 0]}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
</>
)
}
function App() {
return (
<Canvas camera={{ position: [0, 0, 5], fov: 75 }}>
<Scene />
</Canvas>
)
}
パターン 2:インタラクティブなオブジェクト(クリック、ホバー)
import { useState } from 'react'
function InteractiveBox() {
const [hovered, setHovered] = useState(false)
const [active, setActive] = useState(false)
return (
<mesh
scale={active ? 1.5 : 1}
onClick={() => setActive(!active)}
onPointerOver={() => setHovered(true)}
onPointerOut={() => setHovered(false)}
>
<boxGeometry />
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
パターン 3:useFrame を使ったアニメーションコンポーネント
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
function AnimatedSphere() {
const meshRef = useRef()
useFrame((state, delta) => {
// Rotate
meshRef.current.rotation.y += delta
// Oscillate position
const time = state.clock.elapsedTime
meshRef.current.position.y = Math.sin(time) * 2
})
return (
<mesh ref={meshRef}>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="cyan" />
</mesh>
)
}
パターン 4:GLTF モデルの読み込み
import { Suspense } from 'react'
import { useLoader } from '@react-three/fiber'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
function Model({ url }) {
const gltf = useLoader(GLTFLoader, url)
return (
<primitive
object={gltf.scene}
scale={0.5}
position={[0, 0, 0]}
/>
)
}
function App() {
return (
<Canvas>
<Suspense fallback={<LoadingPlaceholder />}>
<Model url="/model.glb" />
</Suspense>
</Canvas>
)
}
function LoadingPlaceholder() {
return (
<mesh>
<boxGeometry />
<meshBasicMaterial wireframe />
</mesh>
)
}
パターン 5:複数のライト
function Lighting() {
return (
<>
{/* Ambient light for base illumination */}
<ambientLight intensity={0.3} />
{/* Directional light with shadows */}
<directionalLight
position={[5, 5, 5]}
intensity={1}
castShadow
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
/>
{/* Point light for accent */}
<pointLight position={[-5, 5, -5]} intensity={0.5} color="blue" />
{/* Spot light for focused illumination */}
<spotLight
position={[10, 10, 10]}
angle={0.3}
penumbra={1}
intensity={1}
/>
</>
)
}
パターン 6:インスタンシング(多数のオブジェクト)
import { useMemo, useRef } from 'react'
import * as THREE from 'three'
import { useFrame } from '@react-three/fiber'
function Particles({ count = 1000 }) {
const meshRef = useRef()
// Generate random positions
const particles = useMemo(() => {
const temp = []
for (let i = 0; i < count; i++) {
const t = Math.random() * 100
const factor = 20 + Math.random() * 100
const speed = 0.01 + Math.random() / 200
const x = Math.random() * 2 - 1
const y = Math.random() * 2 - 1
const z = Math.random() * 2 - 1
temp.push({ t, factor, speed, x, y, z, mx: 0, my: 0 })
}
return temp
}, [count])
const dummy = useMemo(() => new THREE.Object3D(), [])
useFrame(() => {
particles.forEach((particle, i) => {
let { t, factor, speed, x, y, z } = particle
t = particle.t += speed / 2
const a = Math.cos(t) + Math.sin(t * 1) / 10
const b = Math.sin(t) + Math.cos(t * 2) / 10
const s = Math.cos(t)
dummy.position.set(
x + Math.cos((t / 10) * factor) + (Math.sin(t * 1) * factor) / 10,
y + Math.sin((t / 10) * factor) + (Math.cos(t * 2) * factor) / 10,
z + Math.cos((t / 10) * factor) + (Math.sin(t * 3) * factor) / 10
)
dummy.scale.set(s, s, s)
dummy.updateMatrix()
meshRef.current.setMatrixAt(i, dummy.matrix)
})
meshRef.current.instanceMatrix.needsUpdate = true
})
return (
<instancedMesh ref={meshRef} args={[null, null, count]}>
<sphereGeometry args={[0.05, 8, 8]} />
<meshBasicMaterial color="white" />
</instancedMesh>
)
}
パターン 7:グループ化とネスト
function Robot() {
return (
<group position={[0, 0, 0]}>
{/* Body */}
<mesh position={[0, 0, 0]}>
<boxGeometry args={[1, 2, 1]} />
<meshStandardMaterial color="gray" />
</mesh>
{/* Head */}
<mesh position={[0, 1.5, 0]}>
<sphereGeometry args={[0.5, 32, 32]} />
<meshStandardMaterial color="silver" />
</mesh>
{/* Arms */}
<group position={[-0.75, 0.5, 0]}>
<mesh>
<cylinderGeometry args={[0.1, 0.1, 1.5]} />
<meshStandardMaterial color="darkgray" />
</mesh>
</group>
<group position={[0.75, 0.5, 0]}>
<mesh>
<cylinderGeometry args={[0.1, 0.1, 1.5]} />
<meshStandardMaterial color="darkgray" />
</mesh>
</group>
</group>
)
}
Drei ヘルパーとの統合
Drei は R3F に欠かせないヘルパーライブラリで、そのまま使えるコンポーネントを提供します。
OrbitControls
import { OrbitControls } from '@react-three/drei'
<Canvas>
<OrbitControls
makeDefault
enableDamping
dampingFactor={0.05}
minDistance={3}
maxDistance={20}
/>
<Box />
</Canvas>
環境とライティング
import { Environment, ContactShadows } from '@react-three/drei'
<Canvas>
{/* HDRI environment map */}
<Environment preset="sunset" />
{/* Or custom */}
<Environment files="/hdri.hdr" />
{/* Soft contact shadows */}
<ContactShadows
opacity={0.5}
scale={10}
blur={1}
far={10}
resolution={256}
/>
<Model />
</Canvas>
テキスト
import { Text, Text3D } from '@react-three/drei'
// 2D Billboard text
<Text
position={[0, 2, 0]}
fontSize={1}
color="white"
anchorX="center"
anchorY="middle"
>
Hello World
</Text>
// 3D extruded text
<Text3D
font="/fonts/helvetiker_regular.typeface.json"
size={1}
height={0.2}
>
3D Text
<meshNormalMaterial />
</Text3D>
useGLTF フック(Drei)
import { useGLTF } from '@react-three/drei'
function Model() {
const { scene, materials, nodes } = useGLTF('/model.glb')
return <primitive object={scene} />
}
// Pre-load
useGLTF.preload('/model.glb')
Center と Bounds
import { Center, Bounds, useBounds } from '@react-three/drei'
// Auto-center objects
<Center>
<Model />
</Center>
// Auto-fit camera to bounds
<Bounds fit clip observe margin={1.2}>
<Model />
</Bounds>
HTML オーバーレイ
import { Html } from '@react-three/drei'
<mesh>
<boxGeometry />
<meshStandardMaterial />
<Html
position={[0, 1, 0]}
center
distanceFactor={10}
>
<div className="annotation">
This is a box
</div>
</Html>
</mesh>
スクロールコントロール
import { ScrollControls, Scroll, useScroll } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
function AnimatedScene() {
const scroll = useScroll()
const meshRef = useRef()
useFrame(() => {
const offset = scroll.offset // 0-1 normalized scroll position
meshRef.current.position.y = offset * 10
})
return <mesh ref={meshRef}>...</mesh>
}
<Canvas>
<ScrollControls pages={3} damping={0.5}>
<Scroll>
<AnimatedScene />
</Scroll>
{/* HTML overlay */}
<Scroll html>
<div style={{ height: '100vh' }}>
<h1>Scrollable content</h1>
</div>
</Scroll>
</ScrollControls>
</Canvas>
他のライブラリとの統合
GSAP との連携
import { useRef, useEffect } from 'react'
import { useFrame } from '@react-three/fiber'
import gsap from 'gsap'
function AnimatedBox() {
const meshRef = useRef()
useEffect(() => {
// GSAP timeline animation
const tl = gsap.timeline({ repeat: -1, yoyo: true })
tl.to(meshRef.current.position, {
y: 2,
duration: 1,
ease: 'power2.inOut'
})
.to(meshRef.current.rotation, {
y: Math.PI * 2,
duration: 2,
ease: 'none'
}, 0)
return () => tl.kill()
}, [])
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
)
}
Framer Motion との連携
import { motion } from 'framer-motion-3d'
function AnimatedSphere() {
return (
<motion.mesh
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 1 }}
>
<sphereGeometry />
<meshStandardMaterial color="hotpink" />
</motion.mesh>
)
}
Zustand との連携(状態管理)
import create from 'zustand'
const useStore = create((set) => ({
color: 'orange',
setColor: (color) => set({ color })
}))
function Box() {
const color = useStore((state) => state.color)
const setColor = useStore((state) => state.setColor)
return (
<mesh onClick={() => setColor('hotpink')}>
<boxGeometry />
<meshStandardMaterial color={color} />
</mesh>
)
}
パフォーマンスの最適化
1. オンデマンドレンダリング
<Canvas frameloop="demand">
{/* Only renders when needed */}
</Canvas>
// Manually trigger render
function MyComponent() {
const invalidate = useThree((state) => state.invalidate)
return (
<mesh onClick={() => invalidate()}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}
2. インスタンシング
同一のオブジェクトを多数レンダリングするには、<instancedMesh>を使用します。
function Particles({ count = 10000 }) {
const meshRef = useRef()
useEffect(() => {
const temp = new THREE.Object3D()
for (let i = 0; i < count; i++) {
temp.position.set(
Math.random() * 10 - 5,
Math.random() * 10 - 5,
Math.random() * 10 - 5
)
temp.updateMatrix()
meshRef.current.setMatrixAt(i, temp.matrix)
}
meshRef.current.instanceMatrix.needsUpdate = true
}, [count])
return (
<instancedMesh ref={meshRef} args={[null, null, count]}>
<sphereGeometry args={[0.1, 8, 8]} />
<meshBasicMaterial color="white" />
</instancedMesh>
)
}
3. フラスタムカリング
カメラの視界外にあるオブジェクトは自動的にカリングされます。
// Disable for always-visible objects
<mesh frustumCulled={false}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
4. LOD(詳細度)
import { Detailed } from '@react-three/drei'
<Detailed distances={[0, 10, 20]}>
{/* High detail - close to camera */}
<mesh geometry={highPolyGeometry} />
{/* Medium detail */}
<mesh geometry={mediumPolyGeometry} />
{/* Low detail - far from camera */}
<mesh geometry={lowPolyGeometry} />
</Detailed>
5. パフォーマンスの適応
import { AdaptiveDpr, AdaptiveEvents, PerformanceMonitor } from '@react-three/drei'
<Canvas>
{/* Reduce DPR when performance drops */}
<AdaptiveDpr pixelated />
{/* Reduce raycast frequency */}
<AdaptiveEvents />
{/* Monitor and respond to performance */}
<PerformanceMonitor
onIncline={() => console.log('Performance improved')}
onDecline={() => console.log('Performance degraded')}
>
<Scene />
</PerformanceMonitor>
</Canvas>
6. 選択的な再レンダリング
不要な再レンダリングを避けるには、useThreeセレクターを使用します。
// ❌ Re-renders on any state change
const state = useThree()
// ✅ Only re-renders when size changes
const size = useThree((state) => state.size)
// ✅ Only re-renders when camera changes
const camera = useThree((state) => state.camera)
よくある落とし穴と解決策
❌ 落とし穴 1:useFrame内でのsetState
// ❌ BAD: Triggers React re-renders every frame
const [x, setX] = useState(0)
useFrame(() => setX((x) => x + 0.1))
return <mesh position-x={x} />
✅ 解決策:refを直接変更する
// ✅ GOOD: Direct mutation, no re-renders
const meshRef = useRef()
useFrame((state, delta) => {
meshRef.current.position.x += delta
})
return <mesh ref={meshRef} />
❌ 落とし穴 2:レンダー中にオブジェクトを作成する
// ❌ BAD: Creates new Vector3 every render
<mesh position={new THREE.Vector3(1, 2, 3)} />
✅ 解決策:配列またはuseMemoを使用する
// ✅ GOOD: Use array notation
<mesh position={[1, 2, 3]} />
// Or useMemo for complex objects
const position = useMemo(() => new THREE.Vector3(1, 2, 3), [])
<mesh position={position} />
❌ 落とし穴 3:useLoaderのキャッシュを使用しない
// ❌ BAD: Loads texture every render
function Component() {
const [texture, setTexture] = useState()
useEffect(() => {
new TextureLoader().load('/texture.jpg', setTexture)
}, [])
return texture ? <meshBasicMaterial map={texture} /> : null
}
✅ 解決策:useLoaderを使用する(自動的にキャッシュされます)
// ✅ GOOD: Cached and reused
function Component() {
const texture = useLoader(TextureLoader, '/texture.jpg')
return <meshBasicMaterial map={texture} />
}
❌ 落とし穴 4:条件付きマウント(コストが高い)
// ❌ BAD: Unmounts and remounts (expensive)
{stage === 1 && <Stage1 />}
{stage === 2 && <Stage2 />}
{stage === 3 && <Stage3 />}
✅ 解決策:visibilityプロパティを使用する
// ✅ GOOD: Components stay mounted, just hidden
<Stage1 visible={stage === 1} />
<Stage2 visible={stage === 2} />
<Stage3 visible={stage === 3} />
function Stage1({ visible, ...props }) {
return <group {...props} visible={visible}>...</group>
}
❌ 落とし穴 5:Canvasの外でuseThreeを使用する
// ❌ BAD: Crashes - useThree must be inside Canvas
function App() {
const { size } = useThree()
return <Canvas>...</Canvas>
}
✅ 解決策:Canvasの子要素内でフックを使用する
// ✅ GOOD: useThree inside Canvas child
function CameraInfo() {
const { size } = useThree()
return null
}
function App() {
return (
<Canvas>
<CameraInfo />
</Canvas>
)
}
❌ 落とし穴 6:リソースを破棄しない
// ❌ BAD: Memory leak - textures not disposed
const texture = useLoader(TextureLoader, '/texture.jpg')
✅ 解決策:R3Fはリソースを自動的に破棄しますが、Three.jsのオブジェクトを手動で扱う場合は注意してください
// ✅ GOOD: Manual cleanup when needed
useEffect(() => {
const geometry = new THREE.SphereGeometry(1)
const material = new THREE.MeshBasicMaterial()
return () => {
geometry.dispose()
material.dispose()
}
}, [])
ベストプラクティス
1. コンポーネントの構成
シーンを再利用可能なコンポーネントに分割します。
function Lights() {
return (
<>
<ambientLight intensity={0.5} />
<spotLight position={[10, 10, 10]} angle={0.15} />
</>
)
}
function Scene() {
return (
<>
<Lights />
<Model />
<Ground />
<Effects />
</>
)
}
<Canvas>
<Scene />
</Canvas>
2. 重いアセットをSuspendする
非同期処理は必ずSuspenseでラップします。
<Canvas>
<Suspense fallback={<Loader />}>
<Model />
<Environment />
</Suspense>
</Canvas>
3. TypeScriptを使用する
import { ThreeElements } from '@react-three/fiber'
function Box(props: ThreeElements['mesh']) {
return (
<mesh {...props}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}
4. 機能ごとに整理する
src/
components/
3d/
Scene.tsx
Lights.tsx
Camera.tsx
models/
Robot.tsx
Character.tsx
effects/
PostProcessing.tsx
5. React DevTools Profilerでテストする
再レンダリングを監視し、パフォーマンスの問題を引き起こしているコンポーネントを最適化します。
リソース
リファレンス
references/api_reference.md- R3FとDreiのAPIドキュメント一式references/hooks_guide.md- フックの使用方法とパターンの詳細references/drei_helpers.md- Dreiライブラリの包括的なガイド
スクリプト
scripts/component_generator.py- R3Fコンポーネントのボイラープレートを生成しますscripts/scene_setup.py- 一般的なパターンを使用してR3Fシーンを初期化します
アセット
assets/starter_r3f/- R3FとViteを使用したスターターテンプレート一式assets/examples/- 実際のR3Fコンポーネントの例
外部リソース
- 公式ドキュメント
- Dreiドキュメント
- Three.jsドキュメント
- R3F Discord
- Poimandres(pmnd.rs) - エコシステムの概要