r3f-interaction
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Three Fiber Interaction
React Three Fiber 交互
Quick Start
快速开始
tsx
import { Canvas } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
function InteractiveMesh() {
return (
<mesh
onClick={(e) => console.log('Clicked!', e.point)}
onPointerOver={(e) => console.log('Hover')}
onPointerOut={(e) => console.log('Unhover')}
>
<boxGeometry />
<meshStandardMaterial color="hotpink" />
</mesh>
)
}
export default function App() {
return (
<Canvas>
<ambientLight />
<InteractiveMesh />
<OrbitControls />
</Canvas>
)
}tsx
import { Canvas } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'
function InteractiveMesh() {
return (
<mesh
onClick={(e) => console.log('Clicked!', e.point)}
onPointerOver={(e) => console.log('Hover')}
onPointerOut={(e) => console.log('Unhover')}
>
<boxGeometry />
<meshStandardMaterial color="hotpink" />
</mesh>
)
}
export default function App() {
return (
<Canvas>
<ambientLight />
<InteractiveMesh />
<OrbitControls />
</Canvas>
)
}Pointer Events
指针事件
R3F provides built-in pointer events on mesh elements.
R3F 在网格元素上提供了内置的指针事件。
Available Events
可用事件
tsx
<mesh
// Click events
onClick={(e) => {}} // Click (pointerdown + pointerup on same object)
onDoubleClick={(e) => {}} // Double click
onContextMenu={(e) => {}} // Right click
// Pointer events
onPointerDown={(e) => {}} // Pointer pressed
onPointerUp={(e) => {}} // Pointer released
onPointerMove={(e) => {}} // Pointer moved while over object
onPointerOver={(e) => {}} // Pointer enters object
onPointerOut={(e) => {}} // Pointer leaves object
onPointerEnter={(e) => {}} // Pointer enters object (no bubbling)
onPointerLeave={(e) => {}} // Pointer leaves object (no bubbling)
onPointerMissed={(e) => {}} // Click that missed all objects
// Wheel
onWheel={(e) => {}} // Mouse wheel
// Touch
onPointerCancel={(e) => {}} // Touch cancelled
>
<boxGeometry />
<meshStandardMaterial />
</mesh>tsx
<mesh
// 点击事件
onClick={(e) => {}} // 点击(在同一对象上按下并释放指针)
onDoubleClick={(e) => {}} // 双击
onContextMenu={(e) => {}} // 右键点击
// 指针事件
onPointerDown={(e) => {}} // 指针按下
onPointerUp={(e) => {}} // 指针释放
onPointerMove={(e) => {}} // 指针在对象上移动
onPointerOver={(e) => {}} // 指针进入对象
onPointerOut={(e) => {}} // 指针离开对象
onPointerEnter={(e) => {}} // 指针进入对象(不冒泡)
onPointerLeave={(e) => {}} // 指针离开对象(不冒泡)
onPointerMissed={(e) => {}} // 未命中任何对象的点击
// 滚轮
onWheel={(e) => {}} // 鼠标滚轮
// 触摸
onPointerCancel={(e) => {}} // 触摸被取消
>
<boxGeometry />
<meshStandardMaterial />
</mesh>Event Object
事件对象
tsx
function InteractiveMesh() {
const handleClick = (event) => {
// Stop propagation to parent objects
event.stopPropagation()
// Event properties
console.log({
object: event.object, // The mesh that was clicked
point: event.point, // World coordinates of intersection
distance: event.distance, // Distance from camera
face: event.face, // Intersected face
faceIndex: event.faceIndex, // Face index
uv: event.uv, // UV coordinates at intersection
normal: event.normal, // Face normal
camera: event.camera, // Current camera
ray: event.ray, // Ray used for intersection
intersections: event.intersections, // All intersections
nativeEvent: event.nativeEvent, // Original DOM event
delta: event.delta, // Click distance (useful for drag detection)
})
}
return (
<mesh onClick={handleClick}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}tsx
function InteractiveMesh() {
const handleClick = (event) => {
// 阻止事件冒泡到父对象
event.stopPropagation()
// 事件属性
console.log({
object: event.object, // 被点击的网格
point: event.point, // 交点的世界坐标
distance: event.distance, // 到相机的距离
face: event.face, // 被相交的面
faceIndex: event.faceIndex, // 面的索引
uv: event.uv, // 交点的UV坐标
normal: event.normal, // 面的法线
camera: event.camera, // 当前相机
ray: event.ray, // 用于相交检测的射线
intersections: event.intersections, // 所有交点
nativeEvent: event.nativeEvent, // 原始DOM事件
delta: event.delta, // 点击距离(用于拖拽检测)
})
}
return (
<mesh onClick={handleClick}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}Hover Effects
悬停效果
tsx
import { useState } from 'react'
function HoverableMesh() {
const [hovered, setHovered] = useState(false)
return (
<mesh
onPointerOver={(e) => {
e.stopPropagation()
setHovered(true)
document.body.style.cursor = 'pointer'
}}
onPointerOut={(e) => {
setHovered(false)
document.body.style.cursor = 'default'
}}
scale={hovered ? 1.2 : 1}
>
<boxGeometry />
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}tsx
import { useState } from 'react'
function HoverableMesh() {
const [hovered, setHovered] = useState(false)
return (
<mesh
onPointerOver={(e) => {
e.stopPropagation()
setHovered(true)
document.body.style.cursor = 'pointer'
}}
onPointerOut={(e) => {
setHovered(false)
document.body.style.cursor = 'default'
}}
scale={hovered ? 1.2 : 1}
>
<boxGeometry />
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}Selective Raycasting
选择性射线检测
tsx
// Disable raycasting for specific objects
<mesh raycast={() => null}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
// Or use layers
<mesh
layers={1} // Only raycast against layer 1
onClick={() => console.log('clicked')}
>
<boxGeometry />
<meshStandardMaterial />
</mesh>tsx
// 禁用特定对象的射线检测
<mesh raycast={() => null}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
// 或使用图层
<mesh
layers={1} // 仅对第1层进行射线检测
onClick={() => console.log('clicked')}
>
<boxGeometry />
<meshStandardMaterial />
</mesh>Camera Controls
相机控制器
OrbitControls
OrbitControls
tsx
import { OrbitControls } from '@react-three/drei'
function Scene() {
return (
<>
<mesh>
<boxGeometry />
<meshStandardMaterial />
</mesh>
<OrbitControls
makeDefault // Use as default controls
enableDamping // Smooth movement
dampingFactor={0.05}
enableZoom={true}
enablePan={true}
enableRotate={true}
autoRotate={false}
autoRotateSpeed={2}
minDistance={2}
maxDistance={50}
minPolarAngle={0} // Top limit
maxPolarAngle={Math.PI / 2} // Horizon limit
minAzimuthAngle={-Math.PI / 4} // Left limit
maxAzimuthAngle={Math.PI / 4} // Right limit
target={[0, 1, 0]} // Look-at point
/>
</>
)
}tsx
import { OrbitControls } from '@react-three/drei'
function Scene() {
return (
<>
<mesh>
<boxGeometry />
<meshStandardMaterial />
</mesh>
<OrbitControls
makeDefault // 设为默认控制器
enableDamping // 平滑移动
dampingFactor={0.05}
enableZoom={true}
enablePan={true}
enableRotate={true}
autoRotate={false}
autoRotateSpeed={2}
minDistance={2}
maxDistance={50}
minPolarAngle={0} // 顶部限制
maxPolarAngle={Math.PI / 2} // 水平限制
minAzimuthAngle={-Math.PI / 4} // 左侧限制
maxAzimuthAngle={Math.PI / 4} // 右侧限制
target={[0, 1, 0]} // 看向的点
/>
</>
)
}OrbitControls with Ref
带Ref的OrbitControls
tsx
import { OrbitControls } from '@react-three/drei'
import { useRef, useEffect } from 'react'
function Scene() {
const controlsRef = useRef()
useEffect(() => {
// Access controls methods
if (controlsRef.current) {
controlsRef.current.reset()
controlsRef.current.target.set(0, 1, 0)
controlsRef.current.update()
}
}, [])
return <OrbitControls ref={controlsRef} />
}tsx
import { OrbitControls } from '@react-three/drei'
import { useRef, useEffect } from 'react'
function Scene() {
const controlsRef = useRef()
useEffect(() => {
// 访问控制器方法
if (controlsRef.current) {
controlsRef.current.reset()
controlsRef.current.target.set(0, 1, 0)
controlsRef.current.update()
}
}, [])
return <OrbitControls ref={controlsRef} />
}MapControls
MapControls
Top-down map-style controls.
tsx
import { MapControls } from '@react-three/drei'
<MapControls
enableDamping
dampingFactor={0.05}
screenSpacePanning={false} // Pan in world space
maxPolarAngle={Math.PI / 2}
/>俯视地图风格的控制器。
tsx
import { MapControls } from '@react-three/drei'
<MapControls
enableDamping
dampingFactor={0.05}
screenSpacePanning={false} // 在世界空间中平移
maxPolarAngle={Math.PI / 2}
/>FlyControls
FlyControls
Free-flying camera controls.
tsx
import { FlyControls } from '@react-three/drei'
<FlyControls
movementSpeed={10}
rollSpeed={Math.PI / 24}
dragToLook
/>自由飞行相机控制器。
tsx
import { FlyControls } from '@react-three/drei'
<FlyControls
movementSpeed={10}
rollSpeed={Math.PI / 24}
dragToLook
/>FirstPersonControls
FirstPersonControls
FPS-style controls.
tsx
import { FirstPersonControls } from '@react-three/drei'
<FirstPersonControls
movementSpeed={10}
lookSpeed={0.1}
lookVertical
/>FPS风格控制器。
tsx
import { FirstPersonControls } from '@react-three/drei'
<FirstPersonControls
movementSpeed={10}
lookSpeed={0.1}
lookVertical
/>PointerLockControls
PointerLockControls
Lock pointer for FPS games.
tsx
import { PointerLockControls } from '@react-three/drei'
import { useRef } from 'react'
function Scene() {
const controlsRef = useRef()
return (
<>
<PointerLockControls ref={controlsRef} />
{/* Click to lock pointer */}
<mesh onClick={() => controlsRef.current?.lock()}>
<planeGeometry args={[10, 10]} />
<meshBasicMaterial color="green" />
</mesh>
</>
)
}用于FPS游戏的指针锁定控制器。
tsx
import { PointerLockControls } from '@react-three/drei'
import { useRef } from 'react'
function Scene() {
const controlsRef = useRef()
return (
<>
<PointerLockControls ref={controlsRef} />
{/* 点击锁定指针 */}
<mesh onClick={() => controlsRef.current?.lock()}>
<planeGeometry args={[10, 10]} />
<meshBasicMaterial color="green" />
</mesh>
</>
)
}CameraControls
CameraControls
Advanced camera controls with smooth transitions.
tsx
import { CameraControls } from '@react-three/drei'
import { useRef } from 'react'
function Scene() {
const controlsRef = useRef()
const focusOnObject = async () => {
// Smooth transition to target
await controlsRef.current?.setLookAt(
5, 3, 5, // Camera position
0, 0, 0, // Look-at target
true // Enable transition
)
}
return (
<>
<CameraControls ref={controlsRef} />
<mesh onClick={focusOnObject}>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
</>
)
}带平滑过渡的高级相机控制器。
tsx
import { CameraControls } from '@react-three/drei'
import { useRef } from 'react'
function Scene() {
const controlsRef = useRef()
const focusOnObject = async () => {
// 平滑过渡到目标
await controlsRef.current?.setLookAt(
5, 3, 5, // 相机位置
0, 0, 0, // 看向的目标
true // 启用过渡
)
}
return (
<>
<CameraControls ref={controlsRef} />
<mesh onClick={focusOnObject}>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
</>
)
}TrackballControls
TrackballControls
Unconstrained rotation controls.
tsx
import { TrackballControls } from '@react-three/drei'
<TrackballControls
rotateSpeed={2.0}
zoomSpeed={1.2}
panSpeed={0.8}
staticMoving={true}
/>无约束旋转控制器。
tsx
import { TrackballControls } from '@react-three/drei'
<TrackballControls
rotateSpeed={2.0}
zoomSpeed={1.2}
panSpeed={0.8}
staticMoving={true}
/>ArcballControls
ArcballControls
Arc-based rotation controls.
tsx
import { ArcballControls } from '@react-three/drei'
<ArcballControls
enableAnimations
dampingFactor={25}
/>基于弧线的旋转控制器。
tsx
import { ArcballControls } from '@react-three/drei'
<ArcballControls
enableAnimations
dampingFactor={25}
/>Transform Controls
变换控制器
Gizmo for moving/rotating/scaling objects.
tsx
import { TransformControls, OrbitControls } from '@react-three/drei'
import { useRef, useState } from 'react'
function Scene() {
const meshRef = useRef()
const [mode, setMode] = useState('translate')
const orbitRef = useRef()
return (
<>
<OrbitControls ref={orbitRef} makeDefault />
<TransformControls
object={meshRef}
mode={mode} // 'translate' | 'rotate' | 'scale'
space="local" // 'local' | 'world'
onMouseDown={() => {
// Disable orbit while transforming
if (orbitRef.current) orbitRef.current.enabled = false
}}
onMouseUp={() => {
if (orbitRef.current) orbitRef.current.enabled = true
}}
/>
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
{/* Mode switching buttons in HTML */}
<div className="controls">
<button onClick={() => setMode('translate')}>Move</button>
<button onClick={() => setMode('rotate')}>Rotate</button>
<button onClick={() => setMode('scale')}>Scale</button>
</div>
</>
)
}用于移动/旋转/缩放对象的 gizmo 工具。
tsx
import { TransformControls, OrbitControls } from '@react-three/drei'
import { useRef, useState } from 'react'
function Scene() {
const meshRef = useRef()
const [mode, setMode] = useState('translate')
const orbitRef = useRef()
return (
<>
<OrbitControls ref={orbitRef} makeDefault />
<TransformControls
object={meshRef}
mode={mode} // 'translate' | 'rotate' | 'scale'
space="local" // 'local' | 'world'
onMouseDown={() => {
// 变换时禁用轨道控制
if (orbitRef.current) orbitRef.current.enabled = false
}}
onMouseUp={() => {
if (orbitRef.current) orbitRef.current.enabled = true
}}
/>
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
{/* HTML中的模式切换按钮 */}
<div className="controls">
<button onClick={() => setMode('translate')}>移动</button>
<button onClick={() => setMode('rotate')}>旋转</button>
<button onClick={() => setMode('scale')}>缩放</button>
</div>
</>
)
}PivotControls
PivotControls
Alternative transform gizmo with pivot point.
tsx
import { PivotControls } from '@react-three/drei'
function Scene() {
return (
<PivotControls
anchor={[0, 0, 0]} // Anchor point
depthTest={false} // Always visible
lineWidth={2} // Axis line width
axisColors={['red', 'green', 'blue']}
scale={1} // Gizmo scale
fixed={false} // Fixed screen size
>
<mesh>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
</PivotControls>
)
}带支点的备选变换 gizmo。
tsx
import { PivotControls } from '@react-three/drei'
function Scene() {
return (
<PivotControls
anchor={[0, 0, 0]} // 锚点
depthTest={false} // 始终可见
lineWidth={2} // 轴线宽度
axisColors={['red', 'green', 'blue']}
scale={1} // Gizmo 缩放比例
fixed={false} // 固定屏幕大小
>
<mesh>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
</PivotControls>
)
}Drag Controls
拖拽控制器
useDrag from @use-gesture/react
来自@use-gesture/react的useDrag
bash
npm install @use-gesture/reacttsx
import { useDrag } from '@use-gesture/react'
import { useSpring, animated } from '@react-spring/three'
import { useThree } from '@react-three/fiber'
function DraggableMesh() {
const { size, viewport } = useThree()
const aspect = size.width / viewport.width
const [spring, api] = useSpring(() => ({
position: [0, 0, 0],
config: { mass: 1, tension: 280, friction: 60 }
}))
const bind = useDrag(({ movement: [mx, my], down }) => {
api.start({
position: down ? [mx / aspect, -my / aspect, 0] : [0, 0, 0]
})
})
return (
<animated.mesh {...bind()} position={spring.position}>
<boxGeometry />
<meshStandardMaterial color="hotpink" />
</animated.mesh>
)
}bash
npm install @use-gesture/reacttsx
import { useDrag } from '@use-gesture/react'
import { useSpring, animated } from '@react-spring/three'
import { useThree } from '@react-three/fiber'
function DraggableMesh() {
const { size, viewport } = useThree()
const aspect = size.width / viewport.width
const [spring, api] = useSpring(() => ({
position: [0, 0, 0],
config: { mass: 1, tension: 280, friction: 60 }
}))
const bind = useDrag(({ movement: [mx, my], down }) => {
api.start({
position: down ? [mx / aspect, -my / aspect, 0] : [0, 0, 0]
})
})
return (
<animated.mesh {...bind()} position={spring.position}>
<boxGeometry />
<meshStandardMaterial color="hotpink" />
</animated.mesh>
)
}DragControls (Drei)
DragControls(Drei)
tsx
import { DragControls, OrbitControls } from '@react-three/drei'
import { useRef } from 'react'
function Scene() {
const meshRef = useRef()
const orbitRef = useRef()
return (
<>
<OrbitControls ref={orbitRef} makeDefault />
<DragControls
onDragStart={() => {
if (orbitRef.current) orbitRef.current.enabled = false
}}
onDragEnd={() => {
if (orbitRef.current) orbitRef.current.enabled = true
}}
>
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
</DragControls>
</>
)
}tsx
import { DragControls, OrbitControls } from '@react-three/drei'
import { useRef } from 'react'
function Scene() {
const meshRef = useRef()
const orbitRef = useRef()
return (
<>
<OrbitControls ref={orbitRef} makeDefault />
<DragControls
onDragStart={() => {
if (orbitRef.current) orbitRef.current.enabled = false
}}
onDragEnd={() => {
if (orbitRef.current) orbitRef.current.enabled = true
}}
>
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
</DragControls>
</>
)
}Keyboard Controls
键盘控制器
KeyboardControls (Drei)
KeyboardControls(Drei)
tsx
import { KeyboardControls, useKeyboardControls } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'
// Define key mappings
const keyMap = [
{ name: 'forward', keys: ['ArrowUp', 'KeyW'] },
{ name: 'backward', keys: ['ArrowDown', 'KeyS'] },
{ name: 'left', keys: ['ArrowLeft', 'KeyA'] },
{ name: 'right', keys: ['ArrowRight', 'KeyD'] },
{ name: 'jump', keys: ['Space'] },
{ name: 'sprint', keys: ['ShiftLeft'] },
]
function Player() {
const meshRef = useRef()
const [, getKeys] = useKeyboardControls()
useFrame((state, delta) => {
const { forward, backward, left, right, jump, sprint } = getKeys()
const speed = sprint ? 10 : 5
if (forward) meshRef.current.position.z -= speed * delta
if (backward) meshRef.current.position.z += speed * delta
if (left) meshRef.current.position.x -= speed * delta
if (right) meshRef.current.position.x += speed * delta
if (jump) meshRef.current.position.y += speed * delta
})
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="blue" />
</mesh>
)
}
export default function App() {
return (
<KeyboardControls map={keyMap}>
<Canvas>
<ambientLight />
<Player />
</Canvas>
</KeyboardControls>
)
}tsx
import { KeyboardControls, useKeyboardControls } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'
// 定义按键映射
const keyMap = [
{ name: 'forward', keys: ['ArrowUp', 'KeyW'] },
{ name: 'backward', keys: ['ArrowDown', 'KeyS'] },
{ name: 'left', keys: ['ArrowLeft', 'KeyA'] },
{ name: 'right', keys: ['ArrowRight', 'KeyD'] },
{ name: 'jump', keys: ['Space'] },
{ name: 'sprint', keys: ['ShiftLeft'] },
]
function Player() {
const meshRef = useRef()
const [, getKeys] = useKeyboardControls()
useFrame((state, delta) => {
const { forward, backward, left, right, jump, sprint } = getKeys()
const speed = sprint ? 10 : 5
if (forward) meshRef.current.position.z -= speed * delta
if (backward) meshRef.current.position.z += speed * delta
if (left) meshRef.current.position.x -= speed * delta
if (right) meshRef.current.position.x += speed * delta
if (jump) meshRef.current.position.y += speed * delta
})
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="blue" />
</mesh>
)
}
export default function App() {
return (
<KeyboardControls map={keyMap}>
<Canvas>
<ambientLight />
<Player />
</Canvas>
</KeyboardControls>
)
}Subscribe to Key Changes
订阅按键变化
tsx
import { useKeyboardControls } from '@react-three/drei'
import { useEffect } from 'react'
function KeyListener() {
const jumpPressed = useKeyboardControls((state) => state.jump)
useEffect(() => {
if (jumpPressed) {
console.log('Jump!')
}
}, [jumpPressed])
return null
}tsx
import { useKeyboardControls } from '@react-three/drei'
import { useEffect } from 'react'
function KeyListener() {
const jumpPressed = useKeyboardControls((state) => state.jump)
useEffect(() => {
if (jumpPressed) {
console.log('跳跃!')
}
}, [jumpPressed])
return null
}Selection System
选择系统
Click to Select
点击选择
tsx
import { useState } from 'react'
function SelectableScene() {
const [selected, setSelected] = useState(null)
return (
<>
{[[-2, 0, 0], [0, 0, 0], [2, 0, 0]].map((position, i) => (
<mesh
key={i}
position={position}
onClick={(e) => {
e.stopPropagation()
setSelected(i)
}}
>
<boxGeometry />
<meshStandardMaterial
color={selected === i ? 'hotpink' : 'orange'}
emissive={selected === i ? 'hotpink' : 'black'}
emissiveIntensity={0.3}
/>
</mesh>
))}
{/* Click on empty space to deselect */}
<mesh
position={[0, -1, 0]}
rotation={[-Math.PI / 2, 0, 0]}
onClick={() => setSelected(null)}
>
<planeGeometry args={[20, 20]} />
<meshStandardMaterial color="gray" />
</mesh>
</>
)
}tsx
import { useState } from 'react'
function SelectableScene() {
const [selected, setSelected] = useState(null)
return (
<>
{[[-2, 0, 0], [0, 0, 0], [2, 0, 0]].map((position, i) => (
<mesh
key={i}
position={position}
onClick={(e) => {
e.stopPropagation()
setSelected(i)
}}
>
<boxGeometry />
<meshStandardMaterial
color={selected === i ? 'hotpink' : 'orange'}
emissive={selected === i ? 'hotpink' : 'black'}
emissiveIntensity={0.3}
/>
</mesh>
))}
{/* 点击空白区域取消选择 */}
<mesh
position={[0, -1, 0]}
rotation={[-Math.PI / 2, 0, 0]}
onClick={() => setSelected(null)}
>
<planeGeometry args={[20, 20]} />
<meshStandardMaterial color="gray" />
</mesh>
</>
)
}Multi-Select with Outline
带轮廓的多选
tsx
import { useState } from 'react'
import { EffectComposer, Outline, Selection, Select } from '@react-three/postprocessing'
function MultiSelectScene() {
const [selected, setSelected] = useState(new Set())
const toggleSelect = (id, event) => {
event.stopPropagation()
setSelected((prev) => {
const next = new Set(prev)
if (event.shiftKey) {
// Multi-select with shift
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
} else {
// Single select
next.clear()
next.add(id)
}
return next
})
}
return (
<Selection>
<EffectComposer autoClear={false}>
<Outline
blur
visibleEdgeColor={0xffffff}
edgeStrength={10}
/>
</EffectComposer>
{[0, 1, 2, 3, 4].map((id) => (
<Select key={id} enabled={selected.has(id)}>
<mesh
position={[(id - 2) * 2, 0, 0]}
onClick={(e) => toggleSelect(id, e)}
>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
</Select>
))}
</Selection>
)
}tsx
import { useState } from 'react'
import { EffectComposer, Outline, Selection, Select } from '@react-three/postprocessing'
function MultiSelectScene() {
const [selected, setSelected] = useState(new Set())
const toggleSelect = (id, event) => {
event.stopPropagation()
setSelected((prev) => {
const next = new Set(prev)
if (event.shiftKey) {
// 按住Shift键多选
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
} else {
// 单选
next.clear()
next.add(id)
}
return next
})
}
return (
<Selection>
<EffectComposer autoClear={false}>
<Outline
blur
visibleEdgeColor={0xffffff}
edgeStrength={10}
/>
</EffectComposer>
{[0, 1, 2, 3, 4].map((id) => (
<Select key={id} enabled={selected.has(id)}>
<mesh
position={[(id - 2) * 2, 0, 0]}
onClick={(e) => toggleSelect(id, e)}
>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
</Select>
))}
</Selection>
)
}Screen-Space to World-Space
屏幕空间转世界空间
Get World Position from Click
通过点击获取世界坐标
tsx
import { useThree } from '@react-three/fiber'
import * as THREE from 'three'
function ClickToPlace() {
const { camera, raycaster, pointer } = useThree()
const planeRef = useRef()
const handleClick = (event) => {
// Create intersection plane
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
const intersection = new THREE.Vector3()
// Cast ray from pointer
raycaster.setFromCamera(pointer, camera)
raycaster.ray.intersectPlane(plane, intersection)
console.log('World position:', intersection)
}
return (
<mesh
ref={planeRef}
rotation={[-Math.PI / 2, 0, 0]}
onClick={handleClick}
>
<planeGeometry args={[100, 100]} />
<meshBasicMaterial visible={false} />
</mesh>
)
}tsx
import { useThree } from '@react-three/fiber'
import * as THREE from 'three'
function ClickToPlace() {
const { camera, raycaster, pointer } = useThree()
const planeRef = useRef()
const handleClick = (event) => {
// 创建相交平面
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
const intersection = new THREE.Vector3()
// 从指针位置发射射线
raycaster.setFromCamera(pointer, camera)
raycaster.ray.intersectPlane(plane, intersection)
console.log('世界坐标:', intersection)
}
return (
<mesh
ref={planeRef}
rotation={[-Math.PI / 2, 0, 0]}
onClick={handleClick}
>
<planeGeometry args={[100, 100]} />
<meshBasicMaterial visible={false} />
</mesh>
)
}World Position to Screen Position
世界坐标转屏幕坐标
tsx
import { useThree, useFrame } from '@react-three/fiber'
import { Html } from '@react-three/drei'
import * as THREE from 'three'
function WorldToScreen({ target }) {
const { camera, size } = useThree()
const getScreenPosition = (worldPos) => {
const vector = worldPos.clone()
vector.project(camera)
return {
x: (vector.x * 0.5 + 0.5) * size.width,
y: (1 - (vector.y * 0.5 + 0.5)) * size.height
}
}
// Or use Html component which handles this automatically
return (
<Html position={target}>
<div className="label">Label</div>
</Html>
)
}tsx
import { useThree, useFrame } from '@react-three/fiber'
import { Html } from '@react-three/drei'
import * as THREE from 'three'
function WorldToScreen({ target }) {
const { camera, size } = useThree()
const getScreenPosition = (worldPos) => {
const vector = worldPos.clone()
vector.project(camera)
return {
x: (vector.x * 0.5 + 0.5) * size.width,
y: (1 - (vector.y * 0.5 + 0.5)) * size.height
}
}
// 或使用Html组件,它会自动处理这个转换
return (
<Html position={target}>
<div className="label">标签</div>
</Html>
)
}Gesture Recognition
手势识别
usePinch and useWheel
usePinch和useWheel
tsx
import { usePinch, useWheel } from '@use-gesture/react'
import { useSpring, animated } from '@react-spring/three'
function ZoomableMesh() {
const [spring, api] = useSpring(() => ({
scale: 1,
config: { mass: 1, tension: 200, friction: 30 }
}))
usePinch(
({ offset: [s] }) => {
api.start({ scale: s })
},
{ target: window }
)
useWheel(
({ delta: [, dy] }) => {
api.start({ scale: spring.scale.get() - dy * 0.001 })
},
{ target: window }
)
return (
<animated.mesh scale={spring.scale}>
<boxGeometry />
<meshStandardMaterial color="cyan" />
</animated.mesh>
)
}tsx
import { usePinch, useWheel } from '@use-gesture/react'
import { useSpring, animated } from '@react-spring/three'
function ZoomableMesh() {
const [spring, api] = useSpring(() => ({
scale: 1,
config: { mass: 1, tension: 200, friction: 30 }
}))
usePinch(
({ offset: [s] }) => {
api.start({ scale: s })
},
{ target: window }
)
useWheel(
({ delta: [, dy] }) => {
api.start({ scale: spring.scale.get() - dy * 0.001 })
},
{ target: window }
)
return (
<animated.mesh scale={spring.scale}>
<boxGeometry />
<meshStandardMaterial color="cyan" />
</animated.mesh>
)
}Scroll Controls
滚动控制器
tsx
import { Canvas } from '@react-three/fiber'
import { ScrollControls, Scroll, useScroll } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'
function AnimatedOnScroll() {
const meshRef = useRef()
const scroll = useScroll()
useFrame(() => {
const offset = scroll.offset // 0 to 1
meshRef.current.rotation.y = offset * Math.PI * 2
meshRef.current.position.y = offset * 5
})
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
)
}
export default function App() {
return (
<Canvas>
<ScrollControls pages={3} damping={0.25}>
<Scroll>
<AnimatedOnScroll />
</Scroll>
{/* HTML content that scrolls */}
<Scroll html>
<h1 style={{ position: 'absolute', top: '10vh' }}>Page 1</h1>
<h1 style={{ position: 'absolute', top: '110vh' }}>Page 2</h1>
<h1 style={{ position: 'absolute', top: '210vh' }}>Page 3</h1>
</Scroll>
</ScrollControls>
</Canvas>
)
}tsx
import { Canvas } from '@react-three/fiber'
import { ScrollControls, Scroll, useScroll } from '@react-three/drei'
import { useFrame } from '@react-three/fiber'
import { useRef } from 'react'
function AnimatedOnScroll() {
const meshRef = useRef()
const scroll = useScroll()
useFrame(() => {
const offset = scroll.offset // 0 到 1
meshRef.current.rotation.y = offset * Math.PI * 2
meshRef.current.position.y = offset * 5
})
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
)
}
export default function App() {
return (
<Canvas>
<ScrollControls pages={3} damping={0.25}>
<Scroll>
<AnimatedOnScroll />
</Scroll>
{/* 可滚动的HTML内容 */}
<Scroll html>
<h1 style={{ position: 'absolute', top: '10vh' }}>第1页</h1>
<h1 style={{ position: 'absolute', top: '110vh' }}>第2页</h1>
<h1 style={{ position: 'absolute', top: '210vh' }}>第3页</h1>
</Scroll>
</ScrollControls>
</Canvas>
)
}Presentation Controls
展示控制器
For product showcases with limited rotation.
tsx
import { PresentationControls } from '@react-three/drei'
function ProductShowcase() {
return (
<PresentationControls
global // Apply to whole scene
snap // Snap back when released
speed={1} // Rotation speed
zoom={1} // Zoom speed
rotation={[0, 0, 0]} // Initial rotation
polar={[-Math.PI / 4, Math.PI / 4]} // Vertical limits
azimuth={[-Math.PI / 4, Math.PI / 4]} // Horizontal limits
config={{ mass: 1, tension: 170, friction: 26 }}
>
<mesh>
<boxGeometry />
<meshStandardMaterial color="gold" />
</mesh>
</PresentationControls>
)
}用于产品展示,限制旋转范围。
tsx
import { PresentationControls } from '@react-three/drei'
function ProductShowcase() {
return (
<PresentationControls
global // 应用于整个场景
snap // 释放后自动复位
speed={1} // 旋转速度
zoom={1} // 缩放速度
rotation={[0, 0, 0]} // 初始旋转角度
polar={[-Math.PI / 4, Math.PI / 4]} // 垂直旋转范围
azimuth={[-Math.PI / 4, Math.PI / 4]} // 水平旋转范围
config={{ mass: 1, tension: 170, friction: 26 }}
>
<mesh>
<boxGeometry />
<meshStandardMaterial color="gold" />
</mesh>
</PresentationControls>
)
}Performance Tips
性能优化技巧
- Stop propagation: Prevent unnecessary raycasts
- Use layers: Filter raycast targets
- Simpler collision meshes: Use invisible simple geometry
- Throttle events: Limit onPointerMove frequency
- Disable controls when not needed:
enabled={false}
tsx
// Use simpler geometry for raycasting
function OptimizedInteraction() {
return (
<group>
{/* Complex visible mesh */}
<mesh raycast={() => null}>
<torusKnotGeometry args={[1, 0.4, 100, 16]} />
<meshStandardMaterial color="purple" />
</mesh>
{/* Simple invisible collision mesh */}
<mesh onClick={() => console.log('clicked')}>
<sphereGeometry args={[1.5]} />
<meshBasicMaterial visible={false} />
</mesh>
</group>
)
}
// Throttle pointer move events
import { useMemo, useCallback } from 'react'
import throttle from 'lodash/throttle'
function ThrottledHover() {
const handleMove = useMemo(
() => throttle((e) => {
console.log('Move', e.point)
}, 100),
[]
)
return (
<mesh onPointerMove={handleMove}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}- 阻止事件冒泡:避免不必要的射线检测
- 使用图层:过滤射线检测目标
- 简化碰撞网格:使用不可见的简单几何体
- 节流事件:限制onPointerMove的触发频率
- 不需要时禁用控制器:
enabled={false}
tsx
// 使用更简单的几何体进行射线检测
function OptimizedInteraction() {
return (
<group>
{/* 复杂的可见网格 */}
<mesh raycast={() => null}>
<torusKnotGeometry args={[1, 0.4, 100, 16]} />
<meshStandardMaterial color="purple" />
</mesh>
{/* 简单的不可见碰撞网格 */}
<mesh onClick={() => console.log('clicked')}>
<sphereGeometry args={[1.5]} />
<meshBasicMaterial visible={false} />
</mesh>
</group>
)
}
// 节流指针移动事件
import { useMemo, useCallback } from 'react'
import throttle from 'lodash/throttle'
function ThrottledHover() {
const handleMove = useMemo(
() => throttle((e) => {
console.log('移动', e.point)
}, 100),
[]
)
return (
<mesh onPointerMove={handleMove}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
)
}See Also
另请参阅
- - Canvas and scene setup
r3f-fundamentals - - Animating interactions
r3f-animation - - Visual feedback effects (outline, selection)
r3f-postprocessing
- - Canvas和场景设置
r3f-fundamentals - - 交互动画
r3f-animation - - 视觉反馈效果(轮廓、选择)
r3f-postprocessing