--- name: r3f-interaction description: React Three Fiber interaction - pointer events, controls, gestures, selection. Use when handling user input, implementing click detection, adding camera controls, or creating interactive 3D experiences. --- # React Three Fiber Interaction ## Quick Start ```tsx import { Canvas } from '@react-three/fiber' import { OrbitControls } from '@react-three/drei' function InteractiveMesh() { return ( console.log('Clicked!', e.point)} onPointerOver={(e) => console.log('Hover')} onPointerOut={(e) => console.log('Unhover')} > ) } export default function App() { return ( ) } ``` ## Pointer Events R3F provides built-in pointer events on mesh elements. ### Available Events ```tsx {}} // 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 > ``` ### 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 ( ) } ``` ### Hover Effects ```tsx import { useState } from 'react' function HoverableMesh() { const [hovered, setHovered] = useState(false) return ( { e.stopPropagation() setHovered(true) document.body.style.cursor = 'pointer' }} onPointerOut={(e) => { setHovered(false) document.body.style.cursor = 'default' }} scale={hovered ? 1.2 : 1} > ) } ``` ### Selective Raycasting ```tsx // Disable raycasting for specific objects null}> // Or use layers console.log('clicked')} > ``` ## Camera Controls ### OrbitControls ```tsx import { OrbitControls } from '@react-three/drei' function Scene() { return ( <> ) } ``` ### OrbitControls with Ref ```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 } ``` ### MapControls Top-down map-style controls. ```tsx import { MapControls } from '@react-three/drei' ``` ### FlyControls Free-flying camera controls. ```tsx import { FlyControls } from '@react-three/drei' ``` ### FirstPersonControls FPS-style controls. ```tsx import { FirstPersonControls } from '@react-three/drei' ``` ### PointerLockControls Lock pointer for FPS games. ```tsx import { PointerLockControls } from '@react-three/drei' import { useRef } from 'react' function Scene() { const controlsRef = useRef() return ( <> {/* Click to lock pointer */} controlsRef.current?.lock()}> ) } ``` ### 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 ( <> ) } ``` ### TrackballControls Unconstrained rotation controls. ```tsx import { TrackballControls } from '@react-three/drei' ``` ### ArcballControls Arc-based rotation controls. ```tsx import { ArcballControls } from '@react-three/drei' ``` ## 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 ( <> { // Disable orbit while transforming if (orbitRef.current) orbitRef.current.enabled = false }} onMouseUp={() => { if (orbitRef.current) orbitRef.current.enabled = true }} /> {/* Mode switching buttons in HTML */}
) } ``` ### PivotControls Alternative transform gizmo with pivot point. ```tsx import { PivotControls } from '@react-three/drei' function Scene() { return ( ) } ``` ## Drag Controls ### useDrag from @use-gesture/react ```bash npm install @use-gesture/react ``` ```tsx 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 ( ) } ``` ### DragControls (Drei) ```tsx import { DragControls, OrbitControls } from '@react-three/drei' import { useRef } from 'react' function Scene() { const meshRef = useRef() const orbitRef = useRef() return ( <> { if (orbitRef.current) orbitRef.current.enabled = false }} onDragEnd={() => { if (orbitRef.current) orbitRef.current.enabled = true }} > ) } ``` ## Keyboard Controls ### 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 ( ) } export default function App() { return ( ) } ``` ### 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 } ``` ## 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) => ( { e.stopPropagation() setSelected(i) }} > ))} {/* Click on empty space to deselect */} setSelected(null)} > ) } ``` ### 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 ( {[0, 1, 2, 3, 4].map((id) => ( ))} ) } ``` ## 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 ( ) } ``` ### 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 (
Label
) } ``` ## Gesture Recognition ### usePinch and 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 ( ) } ``` ## 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 ( ) } export default function App() { return ( {/* HTML content that scrolls */}

Page 1

Page 2

Page 3

) } ``` ## Presentation Controls For product showcases with limited rotation. ```tsx import { PresentationControls } from '@react-three/drei' function ProductShowcase() { return ( ) } ``` ## Performance Tips 1. **Stop propagation**: Prevent unnecessary raycasts 2. **Use layers**: Filter raycast targets 3. **Simpler collision meshes**: Use invisible simple geometry 4. **Throttle events**: Limit onPointerMove frequency 5. **Disable controls when not needed**: `enabled={false}` ```tsx // Use simpler geometry for raycasting function OptimizedInteraction() { return ( {/* Complex visible mesh */} null}> {/* Simple invisible collision mesh */} console.log('clicked')}> ) } // 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 ( ) } ``` ## See Also - `r3f-fundamentals` - Canvas and scene setup - `r3f-animation` - Animating interactions - `r3f-postprocessing` - Visual feedback effects (outline, selection)