r3f-fundamentals by enzed/r3f-skills
npx skills add https://github.com/enzed/r3f-skills --skill r3f-fundamentalsimport { Canvas } from '@react-three/fiber'
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
function RotatingBox() {
const meshRef = useRef()
useFrame((state, delta) => {
meshRef.current.rotation.x += delta
meshRef.current.rotation.y += delta * 0.5
})
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
)
}
export default function App() {
return (
<Canvas camera={{ position: [0, 0, 5], fov: 75 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<RotatingBox />
</Canvas>
)
}
创建 WebGL 上下文、场景、相机和渲染器的根组件。
import { Canvas } from '@react-three/fiber'
function App() {
return (
<Canvas
// 相机配置
camera={{
position: [0, 5, 10],
fov: 75,
near: 0.1,
far: 1000,
}}
// 或使用正交相机
orthographic
camera={{ zoom: 50, position: [0, 0, 100] }}
// 渲染器设置
gl={{
antialias: true,
alpha: true,
powerPreference: 'high-performance',
preserveDrawingBuffer: true, // 用于截图
}}
dpr={[1, 2]} // 像素比最小/最大值
// 阴影
shadows // 或 shadows="soft" | "basic" | "percentage"
// 色彩管理
flat // 禁用自动 sRGB 色彩管理
// 帧循环控制
frameloop="demand" // 'always' | 'demand' | 'never'
// 事件处理
eventSource={document.getElementById('root')}
eventPrefix="client" // 'offset' | 'client' | 'page' | 'layer' | 'screen'
// 回调函数
onCreated={(state) => {
console.log('Canvas 准备就绪:', state.gl, state.scene, state.camera)
}}
onPointerMissed={() => console.log('点击了背景')}
// 样式
style={{ width: '100%', height: '100vh' }}
>
<Scene />
</Canvas>
)
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
R3F 设置了合理的默认值:
订阅渲染循环。每帧调用(通常为 60fps)。
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'
function AnimatedMesh() {
const meshRef = useRef()
useFrame((state, delta, xrFrame) => {
// state: 完整的 R3F 状态(参见 useThree)
// delta: 自上一帧以来的时间(秒)
// xrFrame: 如果处于 VR/AR 模式,则为 XR 帧
// 动画旋转
meshRef.current.rotation.y += delta
// 访问时钟
const elapsed = state.clock.elapsedTime
meshRef.current.position.y = Math.sin(elapsed) * 2
// 访问指针位置(-1 到 1)
const { x, y } = state.pointer
meshRef.current.rotation.x = y * 0.5
meshRef.current.rotation.z = x * 0.5
})
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
)
}
使用优先级控制渲染顺序(数值越高,执行越晚)。
// 默认优先级为 0
useFrame((state, delta) => {
// 最先运行
}, -1)
useFrame((state, delta) => {
// 在优先级 -1 之后运行
}, 0)
// 使用正优先级进行手动渲染
useFrame((state, delta) => {
// 接管渲染
state.gl.render(state.scene, state.camera)
}, 1)
function ConditionalAnimation({ active }) {
useFrame((state, delta) => {
if (!active) return // 不活跃时跳过
meshRef.current.rotation.y += delta
})
}
访问 R3F 状态存储。
import { useThree } from '@react-three/fiber'
function CameraInfo() {
// 获取完整状态(任何更改都会触发重新渲染)
const state = useThree()
// 选择性订阅(推荐)
const camera = useThree((state) => state.camera)
const gl = useThree((state) => state.gl)
const scene = useThree((state) => state.scene)
const size = useThree((state) => state.size)
// 可用的状态属性:
// gl: WebGLRenderer
// scene: Scene
// camera: Camera
// raycaster: Raycaster
// pointer: Vector2(归一化,-1 到 1)
// mouse: Vector2(已弃用,使用 pointer)
// clock: Clock
// size: { width, height, top, left }
// viewport: { width, height, factor, distance, aspect }
// performance: { current, min, max, debounce, regress }
// events: 事件处理器
// set: 状态设置器
// get: 状态获取器
// invalidate: 触发重新渲染(用于 frameloop="demand")
// advance: 前进一帧(用于 frameloop="never")
return null
}
// 响应视口
function ResponsiveObject() {
const viewport = useThree((state) => state.viewport)
return (
<mesh scale={[viewport.width / 4, viewport.height / 4, 1]}>
<planeGeometry />
<meshBasicMaterial color="blue" />
</mesh>
)
}
// 手动渲染触发
function TriggerRender() {
const invalidate = useThree((state) => state.invalidate)
const handleClick = () => {
// 当使用 frameloop="demand" 时触发渲染
invalidate()
}
}
// 更新相机
function CameraController() {
const camera = useThree((state) => state.camera)
const set = useThree((state) => state.set)
useEffect(() => {
camera.position.set(10, 10, 10)
camera.lookAt(0, 0, 0)
}, [camera])
}
所有 Three.js 对象都可用作 JSX 元素(驼峰命名法)。
// 基本网格结构
<mesh
position={[0, 0, 0]} // x, y, z
rotation={[0, Math.PI, 0]} // 欧拉角(弧度)
scale={[1, 2, 1]} // x, y, z 或单个数字
visible={true}
castShadow
receiveShadow
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="red" />
</mesh>
// 使用 ref
const meshRef = useRef()
<mesh ref={meshRef} />
// meshRef.current 是 THREE.Mesh
通过 args 属性传递构造函数参数:
// BoxGeometry(宽度, 高度, 深度, 宽度分段, 高度分段, 深度分段)
<boxGeometry args={[1, 1, 1, 1, 1, 1]} />
// SphereGeometry(半径, 宽度分段, 高度分段)
<sphereGeometry args={[1, 32, 32]} />
// PlaneGeometry(宽度, 高度, 宽度分段, 高度分段)
<planeGeometry args={[10, 10]} />
// CylinderGeometry(顶部半径, 底部半径, 高度, 径向分段)
<cylinderGeometry args={[1, 1, 2, 32]} />
<group position={[5, 0, 0]} rotation={[0, Math.PI / 4, 0]}>
<mesh position={[-1, 0, 0]}>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
<mesh position={[1, 0, 0]}>
<boxGeometry />
<meshStandardMaterial color="blue" />
</mesh>
</group>
使用短横线表示嵌套属性:
<mesh
position-x={5}
rotation-y={Math.PI}
scale-z={2}
>
<meshStandardMaterial
color="red"
metalness={0.8}
roughness={0.2}
/>
</mesh>
// 阴影相机属性
<directionalLight
castShadow
shadow-mapSize={[2048, 2048]}
shadow-camera-left={-10}
shadow-camera-right={10}
shadow-camera-top={10}
shadow-camera-bottom={-10}
/>
控制子元素如何附加到父元素:
<mesh>
<boxGeometry />
{/* 默认:作为 'material' 附加 */}
<meshStandardMaterial />
</mesh>
{/* 显式附加 */}
<mesh>
<boxGeometry attach="geometry" />
<meshStandardMaterial attach="material" />
</mesh>
{/* 数组附加 */}
<mesh>
<boxGeometry />
<meshStandardMaterial attach="material-0" color="red" />
<meshStandardMaterial attach="material-1" color="blue" />
</mesh>
{/* 使用函数自定义附加 */}
<someObject>
<texture
attach={(parent, self) => {
parent.map = self
return () => { parent.map = null } // 清理函数
}}
/>
</someObject>
R3F 在 3D 对象上提供 React 风格的事件。
function InteractiveBox() {
const [hovered, setHovered] = useState(false)
const [clicked, setClicked] = useState(false)
return (
<mesh
onClick={(e) => {
e.stopPropagation() // 阻止冒泡
setClicked(!clicked)
// 事件属性:
console.log(e.object) // THREE.Mesh
console.log(e.point) // Vector3 - 交点
console.log(e.distance) // 到相机的距离
console.log(e.face) // 相交的面
console.log(e.faceIndex) // 面索引
console.log(e.uv) // UV 坐标
console.log(e.normal) // 面法线
console.log(e.pointer) // 归一化的指针坐标
console.log(e.ray) // Raycaster 射线
console.log(e.camera) // 相机
console.log(e.delta) // 移动距离(拖拽事件)
}}
onContextMenu={(e) => console.log('右键点击')}
onDoubleClick={(e) => console.log('双击')}
onPointerOver={(e) => {
e.stopPropagation()
setHovered(true)
document.body.style.cursor = 'pointer'
}}
onPointerOut={(e) => {
setHovered(false)
document.body.style.cursor = 'default'
}}
onPointerDown={(e) => console.log('指针按下')}
onPointerUp={(e) => console.log('指针抬起')}
onPointerMove={(e) => console.log('在网格上移动')}
onWheel={(e) => console.log('滚轮:', e.deltaY)}
scale={hovered ? 1.2 : 1}
>
<boxGeometry />
<meshStandardMaterial color={clicked ? 'hotpink' : 'orange'} />
</mesh>
)
}
事件在场景图中向上冒泡:
<group onClick={(e) => console.log('组被点击')}>
<mesh onClick={(e) => {
e.stopPropagation() // 阻止冒泡到组
console.log('网格被点击')
}}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
</group>
直接使用现有的 Three.js 对象:
import * as THREE from 'three'
// 现有对象
const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshStandardMaterial({ color: 'red' })
const mesh = new THREE.Mesh(geometry, material)
function Scene() {
return <primitive object={mesh} position={[0, 1, 0]} />
}
// 常用于加载的模型
function Model({ gltf }) {
return <primitive object={gltf.scene} />
}
注册自定义 Three.js 类以供 JSX 使用:
import { extend } from '@react-three/fiber'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
// 扩展一次(通常在模块级别)
extend({ OrbitControls })
// 现在可以作为 JSX 使用
function Scene() {
const { camera, gl } = useThree()
return <orbitControls args={[camera, gl.domElement]} />
}
// TypeScript 声明
declare global {
namespace JSX {
interface IntrinsicElements {
orbitControls: ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls>
}
}
}
import { useRef, useEffect } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
function MeshWithRef() {
const meshRef = useRef<THREE.Mesh>(null)
const materialRef = useRef<THREE.MeshStandardMaterial>(null)
useEffect(() => {
if (meshRef.current) {
// 直接访问 Three.js
meshRef.current.geometry.computeBoundingBox()
console.log(meshRef.current.geometry.boundingBox)
}
}, [])
useFrame(() => {
if (materialRef.current) {
materialRef.current.color.setHSL(Math.random(), 1, 0.5)
}
})
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial ref={materialRef} />
</mesh>
)
}
// 不好:每次渲染都创建新对象
<mesh position={[x, y, z]} />
// 好:修改现有位置
const meshRef = useRef()
useFrame(() => {
meshRef.current.position.x = x
})
<mesh ref={meshRef} />
// 好:对静态值使用 useMemo
const position = useMemo(() => [x, y, z], [x, y, z])
<mesh position={position} />
// 隔离动画组件以防止父组件重新渲染
function Scene() {
return (
<>
<StaticEnvironment />
<AnimatedObject /> {/* 只有这个组件在动画时重新渲染 */}
</>
)
}
function AnimatedObject() {
const ref = useRef()
useFrame((_, delta) => {
ref.current.rotation.y += delta
})
return <mesh ref={ref}><boxGeometry /></mesh>
}
R3F 自动销毁几何体、材质和纹理。可通过以下方式覆盖:
<mesh dispose={null}> {/* 防止自动销毁 */}
<boxGeometry />
<meshStandardMaterial />
</mesh>
// styles.css
html, body, #root {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
// App.tsx
<Canvas style={{ width: '100%', height: '100%' }}>
function ResponsiveScene() {
const { viewport } = useThree()
return (
<mesh scale={Math.min(viewport.width, viewport.height) / 5}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}
import { forwardRef } from 'react'
const CustomMesh = forwardRef((props, ref) => {
return (
<mesh ref={ref} {...props}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
)
})
// 使用
const meshRef = useRef()
<CustomMesh ref={meshRef} position={[0, 1, 0]} />
Leva 提供了一个 GUI,用于在开发过程中实时调整参数。
npm install leva
import { useControls } from 'leva'
function DebugMesh() {
const { position, color, scale, visible } = useControls({
position: { value: [0, 0, 0], step: 0.1 },
color: '#ff0000',
scale: { value: 1, min: 0.1, max: 5, step: 0.1 },
visible: true,
})
return (
<mesh position={position} scale={scale} visible={visible}>
<boxGeometry />
<meshStandardMaterial color={color} />
</mesh>
)
}
import { useControls, folder } from 'leva'
function DebugScene() {
const { lightIntensity, lightColor, shadowMapSize } = useControls({
Lighting: folder({
lightIntensity: { value: 1, min: 0, max: 5 },
lightColor: '#ffffff',
shadowMapSize: { value: 1024, options: [512, 1024, 2048, 4096] },
}),
Camera: folder({
fov: { value: 75, min: 30, max: 120 },
near: { value: 0.1, min: 0.01, max: 1 },
}),
})
return (
<directionalLight
intensity={lightIntensity}
color={lightColor}
shadow-mapSize={[shadowMapSize, shadowMapSize]}
/>
)
}
import { useControls, button } from 'leva'
function DebugActions() {
const meshRef = useRef()
useControls({
'重置位置': button(() => {
meshRef.current.position.set(0, 0, 0)
}),
'随机颜色': button(() => {
meshRef.current.material.color.setHex(Math.random() * 0xffffff)
}),
'记录状态': button(() => {
console.log(meshRef.current.position)
}),
})
return <mesh ref={meshRef}>...</mesh>
}
import { Leva } from 'leva'
function App() {
return (
<>
{/* 在生产环境中隐藏 Leva 面板 */}
<Leva hidden={process.env.NODE_ENV === 'production'} />
<Canvas>
<Scene />
</Canvas>
</>
)
}
import { useControls, monitor } from 'leva'
import { useFrame } from '@react-three/fiber'
function PerformanceMonitor() {
const [fps, setFps] = useState(0)
useControls({
FPS: monitor(() => fps, { graph: true, interval: 100 }),
})
useFrame((state) => {
// 更新 FPS 显示
setFps(Math.round(1 / state.clock.getDelta()))
})
return null
}
function AnimatedDebugMesh() {
const meshRef = useRef()
const { speed, amplitude, enabled } = useControls('Animation', {
enabled: true,
speed: { value: 1, min: 0, max: 5 },
amplitude: { value: 1, min: 0, max: 3 },
})
useFrame(({ clock }) => {
if (!enabled) return
meshRef.current.position.y = Math.sin(clock.elapsedTime * speed) * amplitude
})
return (
<mesh ref={meshRef}>
<sphereGeometry />
<meshStandardMaterial color="cyan" />
</mesh>
)
}
r3f-geometry - 几何体创建r3f-materials - 材质配置r3f-lighting - 灯光和阴影r3f-interaction - 控件和用户输入每周安装量
302
仓库
GitHub 星标数
58
首次出现
2026年1月20日
安全审计
安装于
codex245
gemini-cli242
opencode237
github-copilot222
cursor218
claude-code199
import { Canvas } from '@react-three/fiber'
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
function RotatingBox() {
const meshRef = useRef()
useFrame((state, delta) => {
meshRef.current.rotation.x += delta
meshRef.current.rotation.y += delta * 0.5
})
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="hotpink" />
</mesh>
)
}
export default function App() {
return (
<Canvas camera={{ position: [0, 0, 5], fov: 75 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<RotatingBox />
</Canvas>
)
}
The root component that creates the WebGL context, scene, camera, and renderer.
import { Canvas } from '@react-three/fiber'
function App() {
return (
<Canvas
// Camera configuration
camera={{
position: [0, 5, 10],
fov: 75,
near: 0.1,
far: 1000,
}}
// Or use orthographic
orthographic
camera={{ zoom: 50, position: [0, 0, 100] }}
// Renderer settings
gl={{
antialias: true,
alpha: true,
powerPreference: 'high-performance',
preserveDrawingBuffer: true, // For screenshots
}}
dpr={[1, 2]} // Pixel ratio min/max
// Shadows
shadows // or shadows="soft" | "basic" | "percentage"
// Color management
flat // Disable automatic sRGB color management
// Frame loop control
frameloop="demand" // 'always' | 'demand' | 'never'
// Event handling
eventSource={document.getElementById('root')}
eventPrefix="client" // 'offset' | 'client' | 'page' | 'layer' | 'screen'
// Callbacks
onCreated={(state) => {
console.log('Canvas ready:', state.gl, state.scene, state.camera)
}}
onPointerMissed={() => console.log('Clicked background')}
// Styling
style={{ width: '100%', height: '100vh' }}
>
<Scene />
</Canvas>
)
}
R3F sets sensible defaults:
Subscribe to the render loop. Called every frame (typically 60fps).
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'
function AnimatedMesh() {
const meshRef = useRef()
useFrame((state, delta, xrFrame) => {
// state: Full R3F state (see useThree)
// delta: Time since last frame in seconds
// xrFrame: XR frame if in VR/AR mode
// Animate rotation
meshRef.current.rotation.y += delta
// Access clock
const elapsed = state.clock.elapsedTime
meshRef.current.position.y = Math.sin(elapsed) * 2
// Access pointer position (-1 to 1)
const { x, y } = state.pointer
meshRef.current.rotation.x = y * 0.5
meshRef.current.rotation.z = x * 0.5
})
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
)
}
Control render order with priority (higher = later).
// Default priority is 0
useFrame((state, delta) => {
// Runs first
}, -1)
useFrame((state, delta) => {
// Runs after priority -1
}, 0)
// Manual rendering with positive priority
useFrame((state, delta) => {
// Take over rendering
state.gl.render(state.scene, state.camera)
}, 1)
function ConditionalAnimation({ active }) {
useFrame((state, delta) => {
if (!active) return // Skip when inactive
meshRef.current.rotation.y += delta
})
}
Access the R3F state store.
import { useThree } from '@react-three/fiber'
function CameraInfo() {
// Get full state (triggers re-render on any change)
const state = useThree()
// Selective subscription (recommended)
const camera = useThree((state) => state.camera)
const gl = useThree((state) => state.gl)
const scene = useThree((state) => state.scene)
const size = useThree((state) => state.size)
// Available state properties:
// gl: WebGLRenderer
// scene: Scene
// camera: Camera
// raycaster: Raycaster
// pointer: Vector2 (normalized -1 to 1)
// mouse: Vector2 (deprecated, use pointer)
// clock: Clock
// size: { width, height, top, left }
// viewport: { width, height, factor, distance, aspect }
// performance: { current, min, max, debounce, regress }
// events: Event handlers
// set: State setter
// get: State getter
// invalidate: Trigger re-render (for frameloop="demand")
// advance: Advance one frame (for frameloop="never")
return null
}
// Responsive to viewport
function ResponsiveObject() {
const viewport = useThree((state) => state.viewport)
return (
<mesh scale={[viewport.width / 4, viewport.height / 4, 1]}>
<planeGeometry />
<meshBasicMaterial color="blue" />
</mesh>
)
}
// Manual render trigger
function TriggerRender() {
const invalidate = useThree((state) => state.invalidate)
const handleClick = () => {
// Trigger render when using frameloop="demand"
invalidate()
}
}
// Update camera
function CameraController() {
const camera = useThree((state) => state.camera)
const set = useThree((state) => state.set)
useEffect(() => {
camera.position.set(10, 10, 10)
camera.lookAt(0, 0, 0)
}, [camera])
}
All Three.js objects are available as JSX elements (camelCase).
// Basic mesh structure
<mesh
position={[0, 0, 0]} // x, y, z
rotation={[0, Math.PI, 0]} // Euler angles in radians
scale={[1, 2, 1]} // x, y, z or single number
visible={true}
castShadow
receiveShadow
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="red" />
</mesh>
// With ref
const meshRef = useRef()
<mesh ref={meshRef} />
// meshRef.current is the THREE.Mesh
Constructor arguments via args prop:
// BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments)
<boxGeometry args={[1, 1, 1, 1, 1, 1]} />
// SphereGeometry(radius, widthSegments, heightSegments)
<sphereGeometry args={[1, 32, 32]} />
// PlaneGeometry(width, height, widthSegments, heightSegments)
<planeGeometry args={[10, 10]} />
// CylinderGeometry(radiusTop, radiusBottom, height, radialSegments)
<cylinderGeometry args={[1, 1, 2, 32]} />
<group position={[5, 0, 0]} rotation={[0, Math.PI / 4, 0]}>
<mesh position={[-1, 0, 0]}>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
<mesh position={[1, 0, 0]}>
<boxGeometry />
<meshStandardMaterial color="blue" />
</mesh>
</group>
Use dashes for nested properties:
<mesh
position-x={5}
rotation-y={Math.PI}
scale-z={2}
>
<meshStandardMaterial
color="red"
metalness={0.8}
roughness={0.2}
/>
</mesh>
// Shadow camera properties
<directionalLight
castShadow
shadow-mapSize={[2048, 2048]}
shadow-camera-left={-10}
shadow-camera-right={10}
shadow-camera-top={10}
shadow-camera-bottom={-10}
/>
Control how children attach to parents:
<mesh>
<boxGeometry />
{/* Default: attaches as 'material' */}
<meshStandardMaterial />
</mesh>
{/* Explicit attach */}
<mesh>
<boxGeometry attach="geometry" />
<meshStandardMaterial attach="material" />
</mesh>
{/* Array attachment */}
<mesh>
<boxGeometry />
<meshStandardMaterial attach="material-0" color="red" />
<meshStandardMaterial attach="material-1" color="blue" />
</mesh>
{/* Custom attachment with function */}
<someObject>
<texture
attach={(parent, self) => {
parent.map = self
return () => { parent.map = null } // Cleanup
}}
/>
</someObject>
R3F provides React-style events on 3D objects.
function InteractiveBox() {
const [hovered, setHovered] = useState(false)
const [clicked, setClicked] = useState(false)
return (
<mesh
onClick={(e) => {
e.stopPropagation() // Prevent bubbling
setClicked(!clicked)
// Event properties:
console.log(e.object) // THREE.Mesh
console.log(e.point) // Vector3 - intersection point
console.log(e.distance) // Distance from camera
console.log(e.face) // Intersected face
console.log(e.faceIndex) // Face index
console.log(e.uv) // UV coordinates
console.log(e.normal) // Face normal
console.log(e.pointer) // Normalized pointer coords
console.log(e.ray) // Raycaster ray
console.log(e.camera) // Camera
console.log(e.delta) // Distance moved (drag events)
}}
onContextMenu={(e) => console.log('Right click')}
onDoubleClick={(e) => console.log('Double click')}
onPointerOver={(e) => {
e.stopPropagation()
setHovered(true)
document.body.style.cursor = 'pointer'
}}
onPointerOut={(e) => {
setHovered(false)
document.body.style.cursor = 'default'
}}
onPointerDown={(e) => console.log('Pointer down')}
onPointerUp={(e) => console.log('Pointer up')}
onPointerMove={(e) => console.log('Moving over mesh')}
onWheel={(e) => console.log('Wheel:', e.deltaY)}
scale={hovered ? 1.2 : 1}
>
<boxGeometry />
<meshStandardMaterial color={clicked ? 'hotpink' : 'orange'} />
</mesh>
)
}
Events bubble up through the scene graph:
<group onClick={(e) => console.log('Group clicked')}>
<mesh onClick={(e) => {
e.stopPropagation() // Stop bubbling to group
console.log('Mesh clicked')
}}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
</group>
Use existing Three.js objects directly:
import * as THREE from 'three'
// Existing object
const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshStandardMaterial({ color: 'red' })
const mesh = new THREE.Mesh(geometry, material)
function Scene() {
return <primitive object={mesh} position={[0, 1, 0]} />
}
// Common with loaded models
function Model({ gltf }) {
return <primitive object={gltf.scene} />
}
Register custom Three.js classes for JSX use:
import { extend } from '@react-three/fiber'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
// Extend once (usually at module level)
extend({ OrbitControls })
// Now use as JSX
function Scene() {
const { camera, gl } = useThree()
return <orbitControls args={[camera, gl.domElement]} />
}
// TypeScript declaration
declare global {
namespace JSX {
interface IntrinsicElements {
orbitControls: ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls>
}
}
}
import { useRef, useEffect } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
function MeshWithRef() {
const meshRef = useRef<THREE.Mesh>(null)
const materialRef = useRef<THREE.MeshStandardMaterial>(null)
useEffect(() => {
if (meshRef.current) {
// Direct Three.js access
meshRef.current.geometry.computeBoundingBox()
console.log(meshRef.current.geometry.boundingBox)
}
}, [])
useFrame(() => {
if (materialRef.current) {
materialRef.current.color.setHSL(Math.random(), 1, 0.5)
}
})
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial ref={materialRef} />
</mesh>
)
}
// BAD: Creates new object every render
<mesh position={[x, y, z]} />
// GOOD: Mutate existing position
const meshRef = useRef()
useFrame(() => {
meshRef.current.position.x = x
})
<mesh ref={meshRef} />
// GOOD: Use useMemo for static values
const position = useMemo(() => [x, y, z], [x, y, z])
<mesh position={position} />
// Isolate animated components to prevent parent re-renders
function Scene() {
return (
<>
<StaticEnvironment />
<AnimatedObject /> {/* Only this re-renders on animation */}
</>
)
}
function AnimatedObject() {
const ref = useRef()
useFrame((_, delta) => {
ref.current.rotation.y += delta
})
return <mesh ref={ref}><boxGeometry /></mesh>
}
R3F auto-disposes geometries, materials, and textures. Override with:
<mesh dispose={null}> {/* Prevent auto-dispose */}
<boxGeometry />
<meshStandardMaterial />
</mesh>
// styles.css
html, body, #root {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
// App.tsx
<Canvas style={{ width: '100%', height: '100%' }}>
function ResponsiveScene() {
const { viewport } = useThree()
return (
<mesh scale={Math.min(viewport.width, viewport.height) / 5}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}
import { forwardRef } from 'react'
const CustomMesh = forwardRef((props, ref) => {
return (
<mesh ref={ref} {...props}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
)
})
// Usage
const meshRef = useRef()
<CustomMesh ref={meshRef} position={[0, 1, 0]} />
Leva provides a GUI for tweaking parameters in real-time during development.
npm install leva
import { useControls } from 'leva'
function DebugMesh() {
const { position, color, scale, visible } = useControls({
position: { value: [0, 0, 0], step: 0.1 },
color: '#ff0000',
scale: { value: 1, min: 0.1, max: 5, step: 0.1 },
visible: true,
})
return (
<mesh position={position} scale={scale} visible={visible}>
<boxGeometry />
<meshStandardMaterial color={color} />
</mesh>
)
}
import { useControls, folder } from 'leva'
function DebugScene() {
const { lightIntensity, lightColor, shadowMapSize } = useControls({
Lighting: folder({
lightIntensity: { value: 1, min: 0, max: 5 },
lightColor: '#ffffff',
shadowMapSize: { value: 1024, options: [512, 1024, 2048, 4096] },
}),
Camera: folder({
fov: { value: 75, min: 30, max: 120 },
near: { value: 0.1, min: 0.01, max: 1 },
}),
})
return (
<directionalLight
intensity={lightIntensity}
color={lightColor}
shadow-mapSize={[shadowMapSize, shadowMapSize]}
/>
)
}
import { useControls, button } from 'leva'
function DebugActions() {
const meshRef = useRef()
useControls({
'Reset Position': button(() => {
meshRef.current.position.set(0, 0, 0)
}),
'Random Color': button(() => {
meshRef.current.material.color.setHex(Math.random() * 0xffffff)
}),
'Log State': button(() => {
console.log(meshRef.current.position)
}),
})
return <mesh ref={meshRef}>...</mesh>
}
import { Leva } from 'leva'
function App() {
return (
<>
{/* Hide Leva panel in production */}
<Leva hidden={process.env.NODE_ENV === 'production'} />
<Canvas>
<Scene />
</Canvas>
</>
)
}
import { useControls, monitor } from 'leva'
import { useFrame } from '@react-three/fiber'
function PerformanceMonitor() {
const [fps, setFps] = useState(0)
useControls({
FPS: monitor(() => fps, { graph: true, interval: 100 }),
})
useFrame((state) => {
// Update FPS display
setFps(Math.round(1 / state.clock.getDelta()))
})
return null
}
function AnimatedDebugMesh() {
const meshRef = useRef()
const { speed, amplitude, enabled } = useControls('Animation', {
enabled: true,
speed: { value: 1, min: 0, max: 5 },
amplitude: { value: 1, min: 0, max: 3 },
})
useFrame(({ clock }) => {
if (!enabled) return
meshRef.current.position.y = Math.sin(clock.elapsedTime * speed) * amplitude
})
return (
<mesh ref={meshRef}>
<sphereGeometry />
<meshStandardMaterial color="cyan" />
</mesh>
)
}
r3f-geometry - Geometry creationr3f-materials - Material configurationr3f-lighting - Lights and shadowsr3f-interaction - Controls and user inputWeekly Installs
302
Repository
GitHub Stars
58
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykPass
Installed on
codex245
gemini-cli242
opencode237
github-copilot222
cursor218
claude-code199
Vue.js测试最佳实践:Vue 3组件、组合式函数、Pinia与异步测试完整指南
3,700 周安装