关于 react-three-fiber
帮助 React 开发者使用 React Three Fiber 在应用中构建 3D 体验的技能,它是 Three.js 的声明式渲染器。内容涵盖场景组合、Hooks、资源加载、交互、性能实践及可复用起步资源。
主要功能
- 01用 JSX 组合 Three.js 场景
- 02通过 React Hooks 管理 3D 状态
- 03加载并缓存资源
- 04构建可交互的 3D 对象
- 05应用渲染优化方法
- 06生成组件和场景样板代码
适用场景
- 01为 React 应用添加 3D 场景
- 02制作可交互的产品展示
- 03用 React 开发游戏或模拟
- 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 Hooks
- 使用 React 框架(Next.js、Gatsby、Remix)
主要优势:
- 声明式:像编写 React 组件一样编写 3D 场景
- React 集成:可全面使用 Hooks、上下文和状态管理
- 可复用性:创建并分享 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- 摄像机配置(位置、视野、近裁剪面、远裁剪面)gl- WebGL 渲染器设置dpr- 设备像素比(默认值:[1, 2])shadows- 启用阴影映射(默认值:false)frameloop- "always"(默认值)、"demand" 或 "never"flat- 禁用色彩管理,以使用更简单的颜色linear- 使用线性色彩空间,而非 sRGB
2. 声明式 3D 对象
使用 JSX 和 kebab-case 属性创建 Three.js 对象:
// 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 Hook
在每一帧(动画循环)执行代码:
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- 场景状态(摄像机、场景、gl、时钟等)delta- 距上一帧的时间(用于确保与帧率无关)xrFrame- XR 帧数据(用于 VR/AR)
重要提示:切勿在 useFrame 中使用 setState,否则会导致不必要的重新渲染!
4. useThree Hook
访问场景状态和方法:
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 Hook
加载资源,并自动缓存且集成 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 Hook(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} />
✅ 解决方案:直接修改 refs
// ✅ 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. 延迟加载大型资源
始终将异步操作包裹在 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 组件示例