@lark-apaas/coding-steering 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -13
- package/steering/vite-react/skills/react-three-fiber/SKILL.md +214 -0
- package/steering/vite-react/skills/react-three-fiber/references/animation.md +1001 -0
- package/steering/vite-react/skills/react-three-fiber/references/fundamentals.md +877 -0
- package/steering/vite-react/skills/react-three-fiber/references/geometry.md +717 -0
- package/steering/vite-react/skills/react-three-fiber/references/interaction.md +880 -0
- package/steering/vite-react/skills/react-three-fiber/references/lighting.md +668 -0
- package/steering/vite-react/skills/react-three-fiber/references/loaders.md +607 -0
- package/steering/vite-react/skills/react-three-fiber/references/materials.md +601 -0
- package/steering/vite-react/skills/react-three-fiber/references/physics.md +820 -0
- package/steering/vite-react/skills/react-three-fiber/references/postprocessing.md +754 -0
- package/steering/vite-react/skills/react-three-fiber/references/shaders.md +874 -0
- package/steering/vite-react/skills/react-three-fiber/references/textures.md +635 -0
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
# React Three Fiber Interaction
|
|
2
|
+
|
|
3
|
+
## Quick Start
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Canvas } from '@react-three/fiber'
|
|
7
|
+
import { OrbitControls } from '@react-three/drei'
|
|
8
|
+
|
|
9
|
+
function InteractiveMesh() {
|
|
10
|
+
return (
|
|
11
|
+
<mesh
|
|
12
|
+
onClick={(e) => console.log('Clicked!', e.point)}
|
|
13
|
+
onPointerOver={(e) => console.log('Hover')}
|
|
14
|
+
onPointerOut={(e) => console.log('Unhover')}
|
|
15
|
+
>
|
|
16
|
+
<boxGeometry />
|
|
17
|
+
<meshStandardMaterial color="hotpink" />
|
|
18
|
+
</mesh>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function App() {
|
|
23
|
+
return (
|
|
24
|
+
<Canvas>
|
|
25
|
+
<ambientLight />
|
|
26
|
+
<InteractiveMesh />
|
|
27
|
+
<OrbitControls />
|
|
28
|
+
</Canvas>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Pointer Events
|
|
34
|
+
|
|
35
|
+
R3F provides built-in pointer events on mesh elements.
|
|
36
|
+
|
|
37
|
+
### Available Events
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<mesh
|
|
41
|
+
// Click events
|
|
42
|
+
onClick={(e) => {}} // Click (pointerdown + pointerup on same object)
|
|
43
|
+
onDoubleClick={(e) => {}} // Double click
|
|
44
|
+
onContextMenu={(e) => {}} // Right click
|
|
45
|
+
|
|
46
|
+
// Pointer events
|
|
47
|
+
onPointerDown={(e) => {}} // Pointer pressed
|
|
48
|
+
onPointerUp={(e) => {}} // Pointer released
|
|
49
|
+
onPointerMove={(e) => {}} // Pointer moved while over object
|
|
50
|
+
onPointerOver={(e) => {}} // Pointer enters object
|
|
51
|
+
onPointerOut={(e) => {}} // Pointer leaves object
|
|
52
|
+
onPointerEnter={(e) => {}} // Pointer enters object (no bubbling)
|
|
53
|
+
onPointerLeave={(e) => {}} // Pointer leaves object (no bubbling)
|
|
54
|
+
onPointerMissed={(e) => {}} // Click that missed all objects
|
|
55
|
+
|
|
56
|
+
// Wheel
|
|
57
|
+
onWheel={(e) => {}} // Mouse wheel
|
|
58
|
+
|
|
59
|
+
// Touch
|
|
60
|
+
onPointerCancel={(e) => {}} // Touch cancelled
|
|
61
|
+
>
|
|
62
|
+
<boxGeometry />
|
|
63
|
+
<meshStandardMaterial />
|
|
64
|
+
</mesh>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Event Object
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
function InteractiveMesh() {
|
|
71
|
+
const handleClick = (event) => {
|
|
72
|
+
// Stop propagation to parent objects
|
|
73
|
+
event.stopPropagation()
|
|
74
|
+
|
|
75
|
+
// Event properties
|
|
76
|
+
console.log({
|
|
77
|
+
object: event.object, // The mesh that was clicked
|
|
78
|
+
point: event.point, // World coordinates of intersection
|
|
79
|
+
distance: event.distance, // Distance from camera
|
|
80
|
+
face: event.face, // Intersected face
|
|
81
|
+
faceIndex: event.faceIndex, // Face index
|
|
82
|
+
uv: event.uv, // UV coordinates at intersection
|
|
83
|
+
normal: event.normal, // Face normal
|
|
84
|
+
camera: event.camera, // Current camera
|
|
85
|
+
ray: event.ray, // Ray used for intersection
|
|
86
|
+
intersections: event.intersections, // All intersections
|
|
87
|
+
nativeEvent: event.nativeEvent, // Original DOM event
|
|
88
|
+
delta: event.delta, // Click distance (useful for drag detection)
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<mesh onClick={handleClick}>
|
|
94
|
+
<boxGeometry />
|
|
95
|
+
<meshStandardMaterial />
|
|
96
|
+
</mesh>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Hover Effects
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { useState } from 'react'
|
|
105
|
+
|
|
106
|
+
function HoverableMesh() {
|
|
107
|
+
const [hovered, setHovered] = useState(false)
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<mesh
|
|
111
|
+
onPointerOver={(e) => {
|
|
112
|
+
e.stopPropagation()
|
|
113
|
+
setHovered(true)
|
|
114
|
+
document.body.style.cursor = 'pointer'
|
|
115
|
+
}}
|
|
116
|
+
onPointerOut={(e) => {
|
|
117
|
+
setHovered(false)
|
|
118
|
+
document.body.style.cursor = 'default'
|
|
119
|
+
}}
|
|
120
|
+
scale={hovered ? 1.2 : 1}
|
|
121
|
+
>
|
|
122
|
+
<boxGeometry />
|
|
123
|
+
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
|
|
124
|
+
</mesh>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Selective Raycasting
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
// Disable raycasting for specific objects
|
|
133
|
+
<mesh raycast={() => null}>
|
|
134
|
+
<boxGeometry />
|
|
135
|
+
<meshStandardMaterial />
|
|
136
|
+
</mesh>
|
|
137
|
+
|
|
138
|
+
// Or use layers
|
|
139
|
+
<mesh
|
|
140
|
+
layers={1} // Only raycast against layer 1
|
|
141
|
+
onClick={() => console.log('clicked')}
|
|
142
|
+
>
|
|
143
|
+
<boxGeometry />
|
|
144
|
+
<meshStandardMaterial />
|
|
145
|
+
</mesh>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Camera Controls
|
|
149
|
+
|
|
150
|
+
### OrbitControls
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import { OrbitControls } from '@react-three/drei'
|
|
154
|
+
|
|
155
|
+
function Scene() {
|
|
156
|
+
return (
|
|
157
|
+
<>
|
|
158
|
+
<mesh>
|
|
159
|
+
<boxGeometry />
|
|
160
|
+
<meshStandardMaterial />
|
|
161
|
+
</mesh>
|
|
162
|
+
|
|
163
|
+
<OrbitControls
|
|
164
|
+
makeDefault // Use as default controls
|
|
165
|
+
enableDamping // Smooth movement
|
|
166
|
+
dampingFactor={0.05}
|
|
167
|
+
enableZoom={true}
|
|
168
|
+
enablePan={true}
|
|
169
|
+
enableRotate={true}
|
|
170
|
+
autoRotate={false}
|
|
171
|
+
autoRotateSpeed={2}
|
|
172
|
+
minDistance={2}
|
|
173
|
+
maxDistance={50}
|
|
174
|
+
minPolarAngle={0} // Top limit
|
|
175
|
+
maxPolarAngle={Math.PI / 2} // Horizon limit
|
|
176
|
+
minAzimuthAngle={-Math.PI / 4} // Left limit
|
|
177
|
+
maxAzimuthAngle={Math.PI / 4} // Right limit
|
|
178
|
+
target={[0, 1, 0]} // Look-at point
|
|
179
|
+
/>
|
|
180
|
+
</>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### OrbitControls with Ref
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { OrbitControls } from '@react-three/drei'
|
|
189
|
+
import { useRef, useEffect } from 'react'
|
|
190
|
+
|
|
191
|
+
function Scene() {
|
|
192
|
+
const controlsRef = useRef()
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
// Access controls methods
|
|
196
|
+
if (controlsRef.current) {
|
|
197
|
+
controlsRef.current.reset()
|
|
198
|
+
controlsRef.current.target.set(0, 1, 0)
|
|
199
|
+
controlsRef.current.update()
|
|
200
|
+
}
|
|
201
|
+
}, [])
|
|
202
|
+
|
|
203
|
+
return <OrbitControls ref={controlsRef} />
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### MapControls
|
|
208
|
+
|
|
209
|
+
Top-down map-style controls.
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import { MapControls } from '@react-three/drei'
|
|
213
|
+
|
|
214
|
+
<MapControls
|
|
215
|
+
enableDamping
|
|
216
|
+
dampingFactor={0.05}
|
|
217
|
+
screenSpacePanning={false} // Pan in world space
|
|
218
|
+
maxPolarAngle={Math.PI / 2}
|
|
219
|
+
/>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### FlyControls
|
|
223
|
+
|
|
224
|
+
Free-flying camera controls.
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
import { FlyControls } from '@react-three/drei'
|
|
228
|
+
|
|
229
|
+
<FlyControls
|
|
230
|
+
movementSpeed={10}
|
|
231
|
+
rollSpeed={Math.PI / 24}
|
|
232
|
+
dragToLook
|
|
233
|
+
/>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### FirstPersonControls
|
|
237
|
+
|
|
238
|
+
FPS-style controls.
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
import { FirstPersonControls } from '@react-three/drei'
|
|
242
|
+
|
|
243
|
+
<FirstPersonControls
|
|
244
|
+
movementSpeed={10}
|
|
245
|
+
lookSpeed={0.1}
|
|
246
|
+
lookVertical
|
|
247
|
+
/>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### PointerLockControls
|
|
251
|
+
|
|
252
|
+
Lock pointer for FPS games.
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
import { PointerLockControls } from '@react-three/drei'
|
|
256
|
+
import { useRef } from 'react'
|
|
257
|
+
|
|
258
|
+
function Scene() {
|
|
259
|
+
const controlsRef = useRef()
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<>
|
|
263
|
+
<PointerLockControls ref={controlsRef} />
|
|
264
|
+
|
|
265
|
+
{/* Click to lock pointer */}
|
|
266
|
+
<mesh onClick={() => controlsRef.current?.lock()}>
|
|
267
|
+
<planeGeometry args={[10, 10]} />
|
|
268
|
+
<meshBasicMaterial color="green" />
|
|
269
|
+
</mesh>
|
|
270
|
+
</>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### CameraControls
|
|
276
|
+
|
|
277
|
+
Advanced camera controls with smooth transitions.
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
import { CameraControls } from '@react-three/drei'
|
|
281
|
+
import { useRef } from 'react'
|
|
282
|
+
|
|
283
|
+
function Scene() {
|
|
284
|
+
const controlsRef = useRef()
|
|
285
|
+
|
|
286
|
+
const focusOnObject = async () => {
|
|
287
|
+
// Smooth transition to target
|
|
288
|
+
await controlsRef.current?.setLookAt(
|
|
289
|
+
5, 3, 5, // Camera position
|
|
290
|
+
0, 0, 0, // Look-at target
|
|
291
|
+
true // Enable transition
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<>
|
|
297
|
+
<CameraControls ref={controlsRef} />
|
|
298
|
+
|
|
299
|
+
<mesh onClick={focusOnObject}>
|
|
300
|
+
<boxGeometry />
|
|
301
|
+
<meshStandardMaterial color="red" />
|
|
302
|
+
</mesh>
|
|
303
|
+
</>
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### TrackballControls
|
|
309
|
+
|
|
310
|
+
Unconstrained rotation controls.
|
|
311
|
+
|
|
312
|
+
```tsx
|
|
313
|
+
import { TrackballControls } from '@react-three/drei'
|
|
314
|
+
|
|
315
|
+
<TrackballControls
|
|
316
|
+
rotateSpeed={2.0}
|
|
317
|
+
zoomSpeed={1.2}
|
|
318
|
+
panSpeed={0.8}
|
|
319
|
+
staticMoving={true}
|
|
320
|
+
/>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### ArcballControls
|
|
324
|
+
|
|
325
|
+
Arc-based rotation controls.
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
import { ArcballControls } from '@react-three/drei'
|
|
329
|
+
|
|
330
|
+
<ArcballControls
|
|
331
|
+
enableAnimations
|
|
332
|
+
dampingFactor={25}
|
|
333
|
+
/>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Transform Controls
|
|
337
|
+
|
|
338
|
+
Gizmo for moving/rotating/scaling objects.
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
import { TransformControls, OrbitControls } from '@react-three/drei'
|
|
342
|
+
import { useRef, useState } from 'react'
|
|
343
|
+
|
|
344
|
+
function Scene() {
|
|
345
|
+
const meshRef = useRef()
|
|
346
|
+
const [mode, setMode] = useState('translate')
|
|
347
|
+
const orbitRef = useRef()
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<>
|
|
351
|
+
<OrbitControls ref={orbitRef} makeDefault />
|
|
352
|
+
|
|
353
|
+
<TransformControls
|
|
354
|
+
object={meshRef}
|
|
355
|
+
mode={mode} // 'translate' | 'rotate' | 'scale'
|
|
356
|
+
space="local" // 'local' | 'world'
|
|
357
|
+
onMouseDown={() => {
|
|
358
|
+
// Disable orbit while transforming
|
|
359
|
+
if (orbitRef.current) orbitRef.current.enabled = false
|
|
360
|
+
}}
|
|
361
|
+
onMouseUp={() => {
|
|
362
|
+
if (orbitRef.current) orbitRef.current.enabled = true
|
|
363
|
+
}}
|
|
364
|
+
/>
|
|
365
|
+
|
|
366
|
+
<mesh ref={meshRef}>
|
|
367
|
+
<boxGeometry />
|
|
368
|
+
<meshStandardMaterial color="orange" />
|
|
369
|
+
</mesh>
|
|
370
|
+
|
|
371
|
+
{/* Mode switching buttons in HTML */}
|
|
372
|
+
<div className="controls">
|
|
373
|
+
<button onClick={() => setMode('translate')}>Move</button>
|
|
374
|
+
<button onClick={() => setMode('rotate')}>Rotate</button>
|
|
375
|
+
<button onClick={() => setMode('scale')}>Scale</button>
|
|
376
|
+
</div>
|
|
377
|
+
</>
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### PivotControls
|
|
383
|
+
|
|
384
|
+
Alternative transform gizmo with pivot point.
|
|
385
|
+
|
|
386
|
+
```tsx
|
|
387
|
+
import { PivotControls } from '@react-three/drei'
|
|
388
|
+
|
|
389
|
+
function Scene() {
|
|
390
|
+
return (
|
|
391
|
+
<PivotControls
|
|
392
|
+
anchor={[0, 0, 0]} // Anchor point
|
|
393
|
+
depthTest={false} // Always visible
|
|
394
|
+
lineWidth={2} // Axis line width
|
|
395
|
+
axisColors={['red', 'green', 'blue']}
|
|
396
|
+
scale={1} // Gizmo scale
|
|
397
|
+
fixed={false} // Fixed screen size
|
|
398
|
+
>
|
|
399
|
+
<mesh>
|
|
400
|
+
<boxGeometry />
|
|
401
|
+
<meshStandardMaterial color="orange" />
|
|
402
|
+
</mesh>
|
|
403
|
+
</PivotControls>
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Drag Controls
|
|
409
|
+
|
|
410
|
+
### useDrag from @use-gesture/react
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
npm install @use-gesture/react
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
```tsx
|
|
417
|
+
import { useDrag } from '@use-gesture/react'
|
|
418
|
+
import { useSpring, animated } from '@react-spring/three'
|
|
419
|
+
import { useThree } from '@react-three/fiber'
|
|
420
|
+
|
|
421
|
+
function DraggableMesh() {
|
|
422
|
+
const { size, viewport } = useThree()
|
|
423
|
+
const aspect = size.width / viewport.width
|
|
424
|
+
|
|
425
|
+
const [spring, api] = useSpring(() => ({
|
|
426
|
+
position: [0, 0, 0],
|
|
427
|
+
config: { mass: 1, tension: 280, friction: 60 }
|
|
428
|
+
}))
|
|
429
|
+
|
|
430
|
+
const bind = useDrag(({ movement: [mx, my], down }) => {
|
|
431
|
+
api.start({
|
|
432
|
+
position: down ? [mx / aspect, -my / aspect, 0] : [0, 0, 0]
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<animated.mesh {...bind()} position={spring.position}>
|
|
438
|
+
<boxGeometry />
|
|
439
|
+
<meshStandardMaterial color="hotpink" />
|
|
440
|
+
</animated.mesh>
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### DragControls (Drei)
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
import { DragControls, OrbitControls } from '@react-three/drei'
|
|
449
|
+
import { useRef } from 'react'
|
|
450
|
+
|
|
451
|
+
function Scene() {
|
|
452
|
+
const meshRef = useRef()
|
|
453
|
+
const orbitRef = useRef()
|
|
454
|
+
|
|
455
|
+
return (
|
|
456
|
+
<>
|
|
457
|
+
<OrbitControls ref={orbitRef} makeDefault />
|
|
458
|
+
|
|
459
|
+
<DragControls
|
|
460
|
+
onDragStart={() => {
|
|
461
|
+
if (orbitRef.current) orbitRef.current.enabled = false
|
|
462
|
+
}}
|
|
463
|
+
onDragEnd={() => {
|
|
464
|
+
if (orbitRef.current) orbitRef.current.enabled = true
|
|
465
|
+
}}
|
|
466
|
+
>
|
|
467
|
+
<mesh ref={meshRef}>
|
|
468
|
+
<boxGeometry />
|
|
469
|
+
<meshStandardMaterial color="orange" />
|
|
470
|
+
</mesh>
|
|
471
|
+
</DragControls>
|
|
472
|
+
</>
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## Keyboard Controls
|
|
478
|
+
|
|
479
|
+
### KeyboardControls (Drei)
|
|
480
|
+
|
|
481
|
+
```tsx
|
|
482
|
+
import { KeyboardControls, useKeyboardControls } from '@react-three/drei'
|
|
483
|
+
import { useFrame } from '@react-three/fiber'
|
|
484
|
+
import { useRef } from 'react'
|
|
485
|
+
|
|
486
|
+
// Define key mappings
|
|
487
|
+
const keyMap = [
|
|
488
|
+
{ name: 'forward', keys: ['ArrowUp', 'KeyW'] },
|
|
489
|
+
{ name: 'backward', keys: ['ArrowDown', 'KeyS'] },
|
|
490
|
+
{ name: 'left', keys: ['ArrowLeft', 'KeyA'] },
|
|
491
|
+
{ name: 'right', keys: ['ArrowRight', 'KeyD'] },
|
|
492
|
+
{ name: 'jump', keys: ['Space'] },
|
|
493
|
+
{ name: 'sprint', keys: ['ShiftLeft'] },
|
|
494
|
+
]
|
|
495
|
+
|
|
496
|
+
function Player() {
|
|
497
|
+
const meshRef = useRef()
|
|
498
|
+
const [, getKeys] = useKeyboardControls()
|
|
499
|
+
|
|
500
|
+
useFrame((state, delta) => {
|
|
501
|
+
const { forward, backward, left, right, jump, sprint } = getKeys()
|
|
502
|
+
|
|
503
|
+
const speed = sprint ? 10 : 5
|
|
504
|
+
|
|
505
|
+
if (forward) meshRef.current.position.z -= speed * delta
|
|
506
|
+
if (backward) meshRef.current.position.z += speed * delta
|
|
507
|
+
if (left) meshRef.current.position.x -= speed * delta
|
|
508
|
+
if (right) meshRef.current.position.x += speed * delta
|
|
509
|
+
if (jump) meshRef.current.position.y += speed * delta
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<mesh ref={meshRef}>
|
|
514
|
+
<boxGeometry />
|
|
515
|
+
<meshStandardMaterial color="blue" />
|
|
516
|
+
</mesh>
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export default function App() {
|
|
521
|
+
return (
|
|
522
|
+
<KeyboardControls map={keyMap}>
|
|
523
|
+
<Canvas>
|
|
524
|
+
<ambientLight />
|
|
525
|
+
<Player />
|
|
526
|
+
</Canvas>
|
|
527
|
+
</KeyboardControls>
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Subscribe to Key Changes
|
|
533
|
+
|
|
534
|
+
```tsx
|
|
535
|
+
import { useKeyboardControls } from '@react-three/drei'
|
|
536
|
+
import { useEffect } from 'react'
|
|
537
|
+
|
|
538
|
+
function KeyListener() {
|
|
539
|
+
const jumpPressed = useKeyboardControls((state) => state.jump)
|
|
540
|
+
|
|
541
|
+
useEffect(() => {
|
|
542
|
+
if (jumpPressed) {
|
|
543
|
+
console.log('Jump!')
|
|
544
|
+
}
|
|
545
|
+
}, [jumpPressed])
|
|
546
|
+
|
|
547
|
+
return null
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
## Selection System
|
|
552
|
+
|
|
553
|
+
### Click to Select
|
|
554
|
+
|
|
555
|
+
```tsx
|
|
556
|
+
import { useState } from 'react'
|
|
557
|
+
|
|
558
|
+
function SelectableScene() {
|
|
559
|
+
const [selected, setSelected] = useState(null)
|
|
560
|
+
|
|
561
|
+
return (
|
|
562
|
+
<>
|
|
563
|
+
{[[-2, 0, 0], [0, 0, 0], [2, 0, 0]].map((position, i) => (
|
|
564
|
+
<mesh
|
|
565
|
+
key={i}
|
|
566
|
+
position={position}
|
|
567
|
+
onClick={(e) => {
|
|
568
|
+
e.stopPropagation()
|
|
569
|
+
setSelected(i)
|
|
570
|
+
}}
|
|
571
|
+
>
|
|
572
|
+
<boxGeometry />
|
|
573
|
+
<meshStandardMaterial
|
|
574
|
+
color={selected === i ? 'hotpink' : 'orange'}
|
|
575
|
+
emissive={selected === i ? 'hotpink' : 'black'}
|
|
576
|
+
emissiveIntensity={0.3}
|
|
577
|
+
/>
|
|
578
|
+
</mesh>
|
|
579
|
+
))}
|
|
580
|
+
|
|
581
|
+
{/* Click on empty space to deselect */}
|
|
582
|
+
<mesh
|
|
583
|
+
position={[0, -1, 0]}
|
|
584
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
585
|
+
onClick={() => setSelected(null)}
|
|
586
|
+
>
|
|
587
|
+
<planeGeometry args={[20, 20]} />
|
|
588
|
+
<meshStandardMaterial color="gray" />
|
|
589
|
+
</mesh>
|
|
590
|
+
</>
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Multi-Select with Outline
|
|
596
|
+
|
|
597
|
+
```tsx
|
|
598
|
+
import { useState } from 'react'
|
|
599
|
+
import { EffectComposer, Outline, Selection, Select } from '@react-three/postprocessing'
|
|
600
|
+
|
|
601
|
+
function MultiSelectScene() {
|
|
602
|
+
const [selected, setSelected] = useState(new Set())
|
|
603
|
+
|
|
604
|
+
const toggleSelect = (id, event) => {
|
|
605
|
+
event.stopPropagation()
|
|
606
|
+
setSelected((prev) => {
|
|
607
|
+
const next = new Set(prev)
|
|
608
|
+
if (event.shiftKey) {
|
|
609
|
+
// Multi-select with shift
|
|
610
|
+
if (next.has(id)) {
|
|
611
|
+
next.delete(id)
|
|
612
|
+
} else {
|
|
613
|
+
next.add(id)
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
// Single select
|
|
617
|
+
next.clear()
|
|
618
|
+
next.add(id)
|
|
619
|
+
}
|
|
620
|
+
return next
|
|
621
|
+
})
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return (
|
|
625
|
+
<Selection>
|
|
626
|
+
<EffectComposer autoClear={false}>
|
|
627
|
+
<Outline
|
|
628
|
+
blur
|
|
629
|
+
visibleEdgeColor={0xffffff}
|
|
630
|
+
edgeStrength={10}
|
|
631
|
+
/>
|
|
632
|
+
</EffectComposer>
|
|
633
|
+
|
|
634
|
+
{[0, 1, 2, 3, 4].map((id) => (
|
|
635
|
+
<Select key={id} enabled={selected.has(id)}>
|
|
636
|
+
<mesh
|
|
637
|
+
position={[(id - 2) * 2, 0, 0]}
|
|
638
|
+
onClick={(e) => toggleSelect(id, e)}
|
|
639
|
+
>
|
|
640
|
+
<boxGeometry />
|
|
641
|
+
<meshStandardMaterial color="orange" />
|
|
642
|
+
</mesh>
|
|
643
|
+
</Select>
|
|
644
|
+
))}
|
|
645
|
+
</Selection>
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
## Screen-Space to World-Space
|
|
651
|
+
|
|
652
|
+
### Get World Position from Click
|
|
653
|
+
|
|
654
|
+
```tsx
|
|
655
|
+
import { useThree } from '@react-three/fiber'
|
|
656
|
+
import * as THREE from 'three'
|
|
657
|
+
|
|
658
|
+
function ClickToPlace() {
|
|
659
|
+
const { camera, raycaster, pointer } = useThree()
|
|
660
|
+
const planeRef = useRef()
|
|
661
|
+
|
|
662
|
+
const handleClick = (event) => {
|
|
663
|
+
// Create intersection plane
|
|
664
|
+
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
665
|
+
const intersection = new THREE.Vector3()
|
|
666
|
+
|
|
667
|
+
// Cast ray from pointer
|
|
668
|
+
raycaster.setFromCamera(pointer, camera)
|
|
669
|
+
raycaster.ray.intersectPlane(plane, intersection)
|
|
670
|
+
|
|
671
|
+
console.log('World position:', intersection)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return (
|
|
675
|
+
<mesh
|
|
676
|
+
ref={planeRef}
|
|
677
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
678
|
+
onClick={handleClick}
|
|
679
|
+
>
|
|
680
|
+
<planeGeometry args={[100, 100]} />
|
|
681
|
+
<meshBasicMaterial visible={false} />
|
|
682
|
+
</mesh>
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### World Position to Screen Position
|
|
688
|
+
|
|
689
|
+
```tsx
|
|
690
|
+
import { useThree, useFrame } from '@react-three/fiber'
|
|
691
|
+
import { Html } from '@react-three/drei'
|
|
692
|
+
import * as THREE from 'three'
|
|
693
|
+
|
|
694
|
+
function WorldToScreen({ target }) {
|
|
695
|
+
const { camera, size } = useThree()
|
|
696
|
+
|
|
697
|
+
const getScreenPosition = (worldPos) => {
|
|
698
|
+
const vector = worldPos.clone()
|
|
699
|
+
vector.project(camera)
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
x: (vector.x * 0.5 + 0.5) * size.width,
|
|
703
|
+
y: (1 - (vector.y * 0.5 + 0.5)) * size.height
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Or use Html component which handles this automatically
|
|
708
|
+
return (
|
|
709
|
+
<Html position={target}>
|
|
710
|
+
<div className="label">Label</div>
|
|
711
|
+
</Html>
|
|
712
|
+
)
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
## Gesture Recognition
|
|
717
|
+
|
|
718
|
+
### usePinch and useWheel
|
|
719
|
+
|
|
720
|
+
```tsx
|
|
721
|
+
import { usePinch, useWheel } from '@use-gesture/react'
|
|
722
|
+
import { useSpring, animated } from '@react-spring/three'
|
|
723
|
+
|
|
724
|
+
function ZoomableMesh() {
|
|
725
|
+
const [spring, api] = useSpring(() => ({
|
|
726
|
+
scale: 1,
|
|
727
|
+
config: { mass: 1, tension: 200, friction: 30 }
|
|
728
|
+
}))
|
|
729
|
+
|
|
730
|
+
usePinch(
|
|
731
|
+
({ offset: [s] }) => {
|
|
732
|
+
api.start({ scale: s })
|
|
733
|
+
},
|
|
734
|
+
{ target: window }
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
useWheel(
|
|
738
|
+
({ delta: [, dy] }) => {
|
|
739
|
+
api.start({ scale: spring.scale.get() - dy * 0.001 })
|
|
740
|
+
},
|
|
741
|
+
{ target: window }
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
return (
|
|
745
|
+
<animated.mesh scale={spring.scale}>
|
|
746
|
+
<boxGeometry />
|
|
747
|
+
<meshStandardMaterial color="cyan" />
|
|
748
|
+
</animated.mesh>
|
|
749
|
+
)
|
|
750
|
+
}
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
## Scroll Controls
|
|
754
|
+
|
|
755
|
+
```tsx
|
|
756
|
+
import { Canvas } from '@react-three/fiber'
|
|
757
|
+
import { ScrollControls, Scroll, useScroll } from '@react-three/drei'
|
|
758
|
+
import { useFrame } from '@react-three/fiber'
|
|
759
|
+
import { useRef } from 'react'
|
|
760
|
+
|
|
761
|
+
function AnimatedOnScroll() {
|
|
762
|
+
const meshRef = useRef()
|
|
763
|
+
const scroll = useScroll()
|
|
764
|
+
|
|
765
|
+
useFrame(() => {
|
|
766
|
+
const offset = scroll.offset // 0 to 1
|
|
767
|
+
meshRef.current.rotation.y = offset * Math.PI * 2
|
|
768
|
+
meshRef.current.position.y = offset * 5
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
return (
|
|
772
|
+
<mesh ref={meshRef}>
|
|
773
|
+
<boxGeometry />
|
|
774
|
+
<meshStandardMaterial color="orange" />
|
|
775
|
+
</mesh>
|
|
776
|
+
)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export default function App() {
|
|
780
|
+
return (
|
|
781
|
+
<Canvas>
|
|
782
|
+
<ScrollControls pages={3} damping={0.25}>
|
|
783
|
+
<Scroll>
|
|
784
|
+
<AnimatedOnScroll />
|
|
785
|
+
</Scroll>
|
|
786
|
+
|
|
787
|
+
{/* HTML content that scrolls */}
|
|
788
|
+
<Scroll html>
|
|
789
|
+
<h1 style={{ position: 'absolute', top: '10vh' }}>Page 1</h1>
|
|
790
|
+
<h1 style={{ position: 'absolute', top: '110vh' }}>Page 2</h1>
|
|
791
|
+
<h1 style={{ position: 'absolute', top: '210vh' }}>Page 3</h1>
|
|
792
|
+
</Scroll>
|
|
793
|
+
</ScrollControls>
|
|
794
|
+
</Canvas>
|
|
795
|
+
)
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
## Presentation Controls
|
|
800
|
+
|
|
801
|
+
For product showcases with limited rotation.
|
|
802
|
+
|
|
803
|
+
```tsx
|
|
804
|
+
import { PresentationControls } from '@react-three/drei'
|
|
805
|
+
|
|
806
|
+
function ProductShowcase() {
|
|
807
|
+
return (
|
|
808
|
+
<PresentationControls
|
|
809
|
+
global // Apply to whole scene
|
|
810
|
+
snap // Snap back when released
|
|
811
|
+
speed={1} // Rotation speed
|
|
812
|
+
zoom={1} // Zoom speed
|
|
813
|
+
rotation={[0, 0, 0]} // Initial rotation
|
|
814
|
+
polar={[-Math.PI / 4, Math.PI / 4]} // Vertical limits
|
|
815
|
+
azimuth={[-Math.PI / 4, Math.PI / 4]} // Horizontal limits
|
|
816
|
+
config={{ mass: 1, tension: 170, friction: 26 }}
|
|
817
|
+
>
|
|
818
|
+
<mesh>
|
|
819
|
+
<boxGeometry />
|
|
820
|
+
<meshStandardMaterial color="gold" />
|
|
821
|
+
</mesh>
|
|
822
|
+
</PresentationControls>
|
|
823
|
+
)
|
|
824
|
+
}
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
## Performance Tips
|
|
828
|
+
|
|
829
|
+
1. **Stop propagation**: Prevent unnecessary raycasts
|
|
830
|
+
2. **Use layers**: Filter raycast targets
|
|
831
|
+
3. **Simpler collision meshes**: Use invisible simple geometry
|
|
832
|
+
4. **Throttle events**: Limit onPointerMove frequency
|
|
833
|
+
5. **Disable controls when not needed**: `enabled={false}`
|
|
834
|
+
|
|
835
|
+
```tsx
|
|
836
|
+
// Use simpler geometry for raycasting
|
|
837
|
+
function OptimizedInteraction() {
|
|
838
|
+
return (
|
|
839
|
+
<group>
|
|
840
|
+
{/* Complex visible mesh */}
|
|
841
|
+
<mesh raycast={() => null}>
|
|
842
|
+
<torusKnotGeometry args={[1, 0.4, 100, 16]} />
|
|
843
|
+
<meshStandardMaterial color="purple" />
|
|
844
|
+
</mesh>
|
|
845
|
+
|
|
846
|
+
{/* Simple invisible collision mesh */}
|
|
847
|
+
<mesh onClick={() => console.log('clicked')}>
|
|
848
|
+
<sphereGeometry args={[1.5]} />
|
|
849
|
+
<meshBasicMaterial visible={false} />
|
|
850
|
+
</mesh>
|
|
851
|
+
</group>
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Throttle pointer move events
|
|
856
|
+
import { useMemo, useCallback } from 'react'
|
|
857
|
+
import throttle from 'lodash/throttle'
|
|
858
|
+
|
|
859
|
+
function ThrottledHover() {
|
|
860
|
+
const handleMove = useMemo(
|
|
861
|
+
() => throttle((e) => {
|
|
862
|
+
console.log('Move', e.point)
|
|
863
|
+
}, 100),
|
|
864
|
+
[]
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
return (
|
|
868
|
+
<mesh onPointerMove={handleMove}>
|
|
869
|
+
<boxGeometry />
|
|
870
|
+
<meshStandardMaterial />
|
|
871
|
+
</mesh>
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
## See Also
|
|
877
|
+
|
|
878
|
+
- `r3f-fundamentals` - Canvas and scene setup
|
|
879
|
+
- `r3f-animation` - Animating interactions
|
|
880
|
+
- `r3f-postprocessing` - Visual feedback effects (outline, selection)
|