react-three-fiber by freshtechbro/claudedesignskills
npx skills add https://github.com/freshtechbro/claudedesignskills --skill react-three-fiberReact Three Fiber (R3F) 是 Three.js 的 React 渲染器,它将声明式、基于组件的 3D 开发引入 React 应用程序。您无需以命令式方式创建和管理 Three.js 对象,而是使用直接映射到 Three.js 对象的 JSX 组件来构建 3D 场景。
何时使用此技能:
主要优势:
<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 内容放在这里 */}
</Canvas>
)
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
Canvas 属性:
camera - 相机配置(位置、视野、近平面、远平面)gl - WebGL 渲染器设置dpr - 设备像素比(默认:[1, 2])shadows - 启用阴影映射(默认:false)frameloop - "always"(默认)、"demand" 或 "never"flat - 禁用色彩管理以获得更简单的颜色linear - 使用线性色彩空间而非 sRGB使用带有短横线命名法属性的 JSX 创建 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")简写表示法:
// 完整表示法
<mesh position={[1, 2, 3]} />
// 轴特定(短横线表示法)
<mesh position-x={1} position-y={2} position-z={3} />
在每一帧(动画循环)上执行代码:
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'
function RotatingBox() {
const meshRef = useRef()
useFrame((state, delta) => {
// 在每一帧上旋转网格
meshRef.current.rotation.x += delta
meshRef.current.rotation.y += delta * 0.5
// 访问场景状态
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 —— 这会导致不必要的重新渲染!
访问场景状态和方法:
import { useThree } from '@react-three/fiber'
function CameraInfo() {
const { camera, gl, scene, size, viewport } = useThree()
// 选择性订阅(仅在尺寸变化时重新渲染)
const size = useThree((state) => state.size)
// 非响应式地获取状态
const get = useThree((state) => state.get)
const freshState = get() // 获取最新状态而不触发重新渲染
return null
}
可用状态:
camera - 默认相机scene - Three.js 场景gl - WebGL 渲染器size - 画布尺寸viewport - 以 3D 单位表示的视口尺寸clock - Three.js 时钟pointer - 归一化的鼠标坐标invalidate() - 手动触发渲染setSize() - 手动调整画布大小加载资源,具有自动缓存和 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)
})
预加载:
// 在组件挂载前预加载资源
useLoader.preload(GLTFLoader, '/model.glb')
import { Canvas } from '@react-three/fiber'
function Scene() {
return (
<>
{/* 灯光 */}
<ambientLight intensity={0.5} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
{/* 对象 */}
<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>
)
}
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>
)
}
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
function AnimatedSphere() {
const meshRef = useRef()
useFrame((state, delta) => {
// 旋转
meshRef.current.rotation.y += delta
// 位置振荡
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>
)
}
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>
)
}
function Lighting() {
return (
<>
{/* 环境光用于基础照明 */}
<ambientLight intensity={0.3} />
{/* 带有阴影的平行光 */}
<directionalLight
position={[5, 5, 5]}
intensity={1}
castShadow
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
/>
{/* 点光源用于强调 */}
<pointLight position={[-5, 5, -5]} intensity={0.5} color="blue" />
{/* 聚光灯用于聚焦照明 */}
<spotLight
position={[10, 10, 10]}
angle={0.3}
penumbra={1}
intensity={1}
/>
</>
)
}
import { useMemo, useRef } from 'react'
import * as THREE from 'three'
import { useFrame } from '@react-three/fiber'
function Particles({ count = 1000 }) {
const meshRef = useRef()
// 生成随机位置
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>
)
}
function Robot() {
return (
<group position={[0, 0, 0]}>
{/* 身体 */}
<mesh position={[0, 0, 0]}>
<boxGeometry args={[1, 2, 1]} />
<meshStandardMaterial color="gray" />
</mesh>
{/* 头部 */}
<mesh position={[0, 1.5, 0]}>
<sphereGeometry args={[0.5, 32, 32]} />
<meshStandardMaterial color="silver" />
</mesh>
{/* 手臂 */}
<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 是 R3F 必备的助手库,提供开箱即用的组件:
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 preset="sunset" />
{/* 或自定义 */}
<Environment files="/hdri.hdr" />
{/* 柔和的接触阴影 */}
<ContactShadows
opacity={0.5}
scale={10}
blur={1}
far={10}
resolution={256}
/>
<Model />
</Canvas>
import { Text, Text3D } from '@react-three/drei'
// 2D 广告牌文本
<Text
position={[0, 2, 0]}
fontSize={1}
color="white"
anchorX="center"
anchorY="middle"
>
Hello World
</Text>
// 3D 挤出文本
<Text3D
font="/fonts/helvetiker_regular.typeface.json"
size={1}
height={0.2}
>
3D Text
<meshNormalMaterial />
</Text3D>
import { useGLTF } from '@react-three/drei'
function Model() {
const { scene, materials, nodes } = useGLTF('/model.glb')
return <primitive object={scene} />
}
// 预加载
useGLTF.preload('/model.glb')
import { Center, Bounds, useBounds } from '@react-three/drei'
// 自动居中对象
<Center>
<Model />
</Center>
// 自动调整相机以适应边界
<Bounds fit clip observe margin={1.2}>
<Model />
</Bounds>
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 归一化的滚动位置
meshRef.current.position.y = offset * 10
})
return <mesh ref={meshRef}>...</mesh>
}
<Canvas>
<ScrollControls pages={3} damping={0.5}>
<Scroll>
<AnimatedScene />
</Scroll>
{/* HTML 叠加层 */}
<Scroll html>
<div style={{ height: '100vh' }}>
<h1>Scrollable content</h1>
</div>
</Scroll>
</ScrollControls>
</Canvas>
import { useRef, useEffect } from 'react'
import { useFrame } from '@react-three/fiber'
import gsap from 'gsap'
function AnimatedBox() {
const meshRef = useRef()
useEffect(() => {
// GSAP 时间线动画
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>
)
}
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>
)
}
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>
)
}
<Canvas frameloop="demand">
{/* 仅在需要时渲染 */}
</Canvas>
// 手动触发渲染
function MyComponent() {
const invalidate = useThree((state) => state.invalidate)
return (
<mesh onClick={() => invalidate()}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}
使用 <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>
)
}
相机视野外的对象会自动被剔除。
// 对始终可见的对象禁用
<mesh frustumCulled={false}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
import { Detailed } from '@react-three/drei'
<Detailed distances={[0, 10, 20]}>
{/* 高细节 - 靠近相机 */}
<mesh geometry={highPolyGeometry} />
{/* 中等细节 */}
<mesh geometry={mediumPolyGeometry} />
{/* 低细节 - 远离相机 */}
<mesh geometry={lowPolyGeometry} />
</Detailed>
import { AdaptiveDpr, AdaptiveEvents, PerformanceMonitor } from '@react-three/drei'
<Canvas>
{/* 性能下降时降低 DPR */}
<AdaptiveDpr pixelated />
{/* 减少光线投射频率 */}
<AdaptiveEvents />
{/* 监控并响应性能 */}
<PerformanceMonitor
onIncline={() => console.log('Performance improved')}
onDecline={() => console.log('Performance degraded')}
>
<Scene />
</PerformanceMonitor>
</Canvas>
使用 useThree 选择器来避免不必要的重新渲染:
// ❌ 任何状态变化都会触发重新渲染
const state = useThree()
// ✅ 仅在尺寸变化时重新渲染
const size = useThree((state) => state.size)
// ✅ 仅在相机变化时重新渲染
const camera = useThree((state) => state.camera)
// ❌ 错误:每一帧都触发 React 重新渲染
const [x, setX] = useState(0)
useFrame(() => setX((x) => x + 0.1))
return <mesh position-x={x} />
✅ 解决方案:直接修改 refs
// ✅ 正确:直接修改,无重新渲染
const meshRef = useRef()
useFrame((state, delta) => {
meshRef.current.position.x += delta
})
return <mesh ref={meshRef} />
// ❌ 错误:每次渲染都创建新的 Vector3
<mesh position={new THREE.Vector3(1, 2, 3)} />
✅ 解决方案:使用数组或 useMemo
// ✅ 正确:使用数组表示法
<mesh position={[1, 2, 3]} />
// 或对复杂对象使用 useMemo
const position = useMemo(() => new THREE.Vector3(1, 2, 3), [])
<mesh position={position} />
// ❌ 错误:每次渲染都加载纹理
function Component() {
const [texture, setTexture] = useState()
useEffect(() => {
new TextureLoader().load('/texture.jpg', setTexture)
}, [])
return texture ? <meshBasicMaterial map={texture} /> : null
}
✅ 解决方案:使用 useLoader(自动缓存)
// ✅ 正确:缓存并重用
function Component() {
const texture = useLoader(TextureLoader, '/texture.jpg')
return <meshBasicMaterial map={texture} />
}
// ❌ 错误:卸载并重新挂载(开销大)
{stage === 1 && <Stage1 />}
{stage === 2 && <Stage2 />}
{stage === 3 && <Stage3 />}
✅ 解决方案:使用可见性属性
// ✅ 正确:组件保持挂载,只是隐藏
<Stage1 visible={stage === 1} />
<Stage2 visible={stage === 2} />
<Stage3 visible={stage === 3} />
function Stage1({ visible, ...props }) {
return <group {...props} visible={visible}>...</group>
}
// ❌ 错误:崩溃 - useThree 必须在 Canvas 内部使用
function App() {
const { size } = useThree()
return <Canvas>...</Canvas>
}
✅ 解决方案:在 Canvas 子组件内部使用 Hooks
// ✅ 正确:useThree 在 Canvas 子组件内部
function CameraInfo() {
const { size } = useThree()
return null
}
function App() {
return (
<Canvas>
<CameraInfo />
</Canvas>
)
}
// ❌ 错误:内存泄漏 - 纹理未释放
const texture = useLoader(TextureLoader, '/texture.jpg')
✅ 解决方案:R3F 会自动处理释放,但手动创建的 Three.js 对象要小心
// ✅ 正确:需要时手动清理
useEffect(() => {
const geometry = new THREE.SphereGeometry(1)
const material = new THREE.MeshBasicMaterial()
return () => {
geometry.dispose()
material.dispose()
}
}, [])
将场景拆分为可重用的组件:
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>
始终将异步操作包装在 Suspense 中:
<Canvas>
<Suspense fallback={<Loader />}>
<Model />
<Environment />
</Suspense>
</Canvas>
import { ThreeElements } from '@react-three/fiber'
function Box(props: ThreeElements['mesh']) {
return (
<mesh {...props}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}
src/
components/
3d/
Scene.tsx
Lights.tsx
Camera.tsx
models/
Robot.tsx
Character.tsx
effects/
PostProcessing.tsx
监控重新渲染并优化导致性能问题的组件。
references/api_reference.md - 完整的 R3F 和 Drei API 文档references/hooks_guide.md - 详细的 Hooks 使用和模式references/drei_helpers.md - 全面的 Drei 库指南scripts/component_generator.py - 生成 R3F 组件样板代码scripts/scene_setup.py - 使用常见模式初始化 R3F 场景assets/starter_r3f/ - 完整的 R3F + Vite 启动模板assets/examples/ - 真实世界的 R3F 组件示例每周安装量
88
仓库
GitHub 星标数
11
首次出现
Feb 27, 2026
安全审计
安装于
opencode88
gemini-cli86
github-copilot86
amp86
cline86
codex86
React Three Fiber (R3F) is a React renderer for Three.js that brings declarative, component-based 3D development to React applications. Instead of imperatively creating and managing Three.js objects, you build 3D scenes using JSX components that map directly to Three.js objects.
When to Use This Skill :
Key Benefits :
The <Canvas> component sets up a Three.js scene, camera, renderer, and render loop.
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 Props :
camera - Camera configuration (position, fov, near, far)gl - WebGL renderer settingsdpr - Device pixel ratio (default: [1, 2])shadows - Enable shadow mapping (default: false)frameloop - "always" (default), "demand", or "never"flat - Disable color management for simpler colorslinear - Use linear color space instead of sRGBThree.js objects are created using JSX with kebab-case props:
// 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>
Prop Mapping :
position → object.position.set(x, y, z)rotation → object.rotation.set(x, y, z)scale → object.scale.set(x, y, z)args → Constructor arguments for geometry/materialattach → Attach to parent property (e.g., attach="material")Shorthand Notation :
// Full notation
<mesh position={[1, 2, 3]} />
// Axis-specific (dash notation)
<mesh position-x={1} position-y={2} position-z={3} />
Execute code on every frame (animation loop):
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 Parameters :
state - Scene state (camera, scene, gl, clock, etc.)delta - Time since last frame (for frame-rate independence)xrFrame - XR frame data (for VR/AR)Important : Never use setState inside useFrame - it causes unnecessary re-renders!
Access scene state and methods:
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
}
Available State :
camera - Default camerascene - Three.js scenegl - WebGL renderersize - Canvas dimensionsviewport - Viewport dimensions in 3D unitsclock - Three.js clockpointer - Normalized mouse coordinatesinvalidate() - Manually trigger rendersetSize() - Manually resize canvasLoad assets with automatic caching and Suspense integration:
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>
)
}
Loading Multiple Assets :
const [texture1, texture2, texture3] = useLoader(TextureLoader, [
'/tex1.jpg',
'/tex2.jpg',
'/tex3.jpg'
])
Loader Extensions :
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
useLoader(GLTFLoader, '/model.glb', (loader) => {
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/')
loader.setDRACOLoader(dracoLoader)
})
Pre-loading :
// Pre-load assets before component mounts
useLoader.preload(GLTFLoader, '/model.glb')
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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}
/>
</>
)
}
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>
)
}
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 is the essential helper library for R3F, providing ready-to-use components:
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>
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')
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>
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>
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>
)
}
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>
)
}
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>
)
}
<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>
)
}
Use <instancedMesh> for rendering many identical objects:
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>
)
}
Objects outside the camera view are automatically culled.
// Disable for always-visible objects
<mesh frustumCulled={false}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
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>
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>
Use useThree selectors to avoid unnecessary re-renders:
// ❌ 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)
// ❌ BAD: Triggers React re-renders every frame
const [x, setX] = useState(0)
useFrame(() => setX((x) => x + 0.1))
return <mesh position-x={x} />
✅ Solution : Mutate refs directly
// ✅ GOOD: Direct mutation, no re-renders
const meshRef = useRef()
useFrame((state, delta) => {
meshRef.current.position.x += delta
})
return <mesh ref={meshRef} />
// ❌ BAD: Creates new Vector3 every render
<mesh position={new THREE.Vector3(1, 2, 3)} />
✅ Solution : Use arrays or 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} />
// ❌ BAD: Loads texture every render
function Component() {
const [texture, setTexture] = useState()
useEffect(() => {
new TextureLoader().load('/texture.jpg', setTexture)
}, [])
return texture ? <meshBasicMaterial map={texture} /> : null
}
✅ Solution : Use useLoader (automatic caching)
// ✅ GOOD: Cached and reused
function Component() {
const texture = useLoader(TextureLoader, '/texture.jpg')
return <meshBasicMaterial map={texture} />
}
// ❌ BAD: Unmounts and remounts (expensive)
{stage === 1 && <Stage1 />}
{stage === 2 && <Stage2 />}
{stage === 3 && <Stage3 />}
✅ Solution : Use visibility prop
// ✅ 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>
}
// ❌ BAD: Crashes - useThree must be inside Canvas
function App() {
const { size } = useThree()
return <Canvas>...</Canvas>
}
✅ Solution : Use hooks inside Canvas children
// ✅ GOOD: useThree inside Canvas child
function CameraInfo() {
const { size } = useThree()
return null
}
function App() {
return (
<Canvas>
<CameraInfo />
</Canvas>
)
}
// ❌ BAD: Memory leak - textures not disposed
const texture = useLoader(TextureLoader, '/texture.jpg')
✅ Solution : R3F handles disposal automatically, but be careful with manual Three.js objects
// ✅ GOOD: Manual cleanup when needed
useEffect(() => {
const geometry = new THREE.SphereGeometry(1)
const material = new THREE.MeshBasicMaterial()
return () => {
geometry.dispose()
material.dispose()
}
}, [])
Break scenes into reusable components:
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>
Always wrap async operations in Suspense:
<Canvas>
<Suspense fallback={<Loader />}>
<Model />
<Environment />
</Suspense>
</Canvas>
import { ThreeElements } from '@react-three/fiber'
function Box(props: ThreeElements['mesh']) {
return (
<mesh {...props}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}
src/
components/
3d/
Scene.tsx
Lights.tsx
Camera.tsx
models/
Robot.tsx
Character.tsx
effects/
PostProcessing.tsx
Monitor re-renders and optimize components causing performance issues.
references/api_reference.md - Complete R3F & Drei API documentationreferences/hooks_guide.md - Detailed hooks usage and patternsreferences/drei_helpers.md - Comprehensive Drei library guidescripts/component_generator.py - Generate R3F component boilerplatescripts/scene_setup.py - Initialize R3F scene with common patternsassets/starter_r3f/ - Complete R3F + Vite starter templateassets/examples/ - Real-world R3F component examplesWeekly Installs
88
Repository
GitHub Stars
11
First Seen
Feb 27, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode88
gemini-cli86
github-copilot86
amp86
cline86
codex86
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
122,000 周安装