arkaos 5.5.0 → 5.7.0
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/README.md +2 -2
- package/THE-ARKAOS-GUIDE.md +1 -1
- package/VERSION +1 -1
- package/arka/SKILL.md +1 -1
- package/config/skills-provenance.yaml +17 -0
- package/departments/brand/skills/design-system/SKILL.md +4 -2
- package/departments/brand/skills/motion-design/SKILL.md +362 -0
- package/departments/brand/skills/motion-design/references/director/choreography.md +93 -0
- package/departments/brand/skills/motion-design/references/director/context-adaptation.md +83 -0
- package/departments/brand/skills/motion-design/references/director/core-philosophy.md +53 -0
- package/departments/brand/skills/motion-design/references/director/decision-framework.md +91 -0
- package/departments/brand/skills/motion-design/references/director/disney-principles.md +102 -0
- package/departments/brand/skills/motion-design/references/director/emotion-mapping.md +71 -0
- package/departments/brand/skills/motion-design/references/director/motion-personality.md +89 -0
- package/departments/brand/skills/motion-design/references/director/narrative-structure.md +62 -0
- package/departments/brand/skills/motion-design/references/motion-design-skill.LICENSE +21 -0
- package/departments/brand/skills/motion-design/references/patterns/ambient-continuous.md +81 -0
- package/departments/brand/skills/motion-design/references/patterns/entrance-exit.md +82 -0
- package/departments/brand/skills/motion-design/references/patterns/multi-element.md +69 -0
- package/departments/brand/skills/motion-design/references/patterns/state-feedback.md +96 -0
- package/departments/brand/skills/motion-design/references/reference/property-selection.md +95 -0
- package/departments/brand/skills/motion-design/references/reference/quality-checklist.md +67 -0
- package/departments/brand/skills/motion-design/references/reference/timing-easing-tables.md +106 -0
- package/departments/brand/skills/motion-design/references/reference/troubleshooting.md +73 -0
- package/departments/dev/skills/canvas-generative/SKILL.md +3 -3
- package/departments/dev/skills/threejs/SKILL.md +173 -0
- package/departments/dev/skills/threejs/references/genjutsu.LICENSE +21 -0
- package/departments/dev/skills/threejs/references/r3f.md +249 -0
- package/departments/dev/skills/threejs/references/scene-setup.md +404 -0
- package/departments/dev/skills/threejs/references/shaders.md +452 -0
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
- package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
- package/harness/opencode/agents/arka-cfo-helena.md +1 -1
- package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
- package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
- package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
- package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
- package/harness/opencode/agents/arka-coo-sofia.md +1 -1
- package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
- package/harness/opencode/agents/arka-cqo-marta.md +1 -1
- package/harness/opencode/agents/arka-cto-marco.md +1 -1
- package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
- package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
- package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
- package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
- package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
- package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
- package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
- package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
- package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
- package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
- package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
- package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
- package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
- package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
- package/harness/zed/.rules +1 -1
- package/knowledge/commands-registry.json +1 -1
- package/knowledge/skills-manifest.json +31 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
> Derived from genjutsu's `threejs-r3f` skill (MIT — `genjutsu.LICENSE`
|
|
2
|
+
> in this directory). The React Three Fiber layer of `dev/threejs`.
|
|
3
|
+
|
|
4
|
+
# Three.js & React Three Fiber
|
|
5
|
+
|
|
6
|
+
> 3D on the web. Three.js is the engine, R3F is the React renderer.
|
|
7
|
+
> Concise rules here. Deep-dive in `references/`.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## When to Use What
|
|
12
|
+
|
|
13
|
+
| Need | Tool | Why |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Full 3D scene (models, lights, physics) | R3F + drei | Declarative, React-friendly, ecosystem |
|
|
16
|
+
| Vanilla 3D (no React) | Three.js direct | Lighter, no React overhead |
|
|
17
|
+
| Simple 3D transforms on UI | CSS `transform3d` | GPU-composited, no WebGL context |
|
|
18
|
+
| 2D particles / generative | Canvas 2D | Simpler API, less GPU overhead |
|
|
19
|
+
| Shader-only visuals (no scene graph) | Raw WebGL / ShaderMaterial | Maximum control, minimal abstraction |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Scene Setup Patterns
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { Canvas } from '@react-three/fiber'
|
|
27
|
+
import { Environment, OrbitControls } from '@react-three/drei'
|
|
28
|
+
import { Suspense } from 'react'
|
|
29
|
+
|
|
30
|
+
<Canvas camera={{ position: [0, 2, 5], fov: 45 }} dpr={[1, 2]} gl={{ antialias: true }}>
|
|
31
|
+
<Suspense fallback={null}>
|
|
32
|
+
<Environment preset="studio" />
|
|
33
|
+
<OrbitControls makeDefault />
|
|
34
|
+
<Scene />
|
|
35
|
+
</Suspense>
|
|
36
|
+
</Canvas>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Rules:**
|
|
40
|
+
- Always wrap scene content in `<Suspense>` -- loaders (GLTF, textures, HDRI) need it
|
|
41
|
+
- Set `dpr={[1, 2]}` to clamp pixel ratio (Retina without melting GPUs)
|
|
42
|
+
- Keep the Canvas parent component minimal -- re-renders propagate into the scene
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## R3F Hooks
|
|
47
|
+
|
|
48
|
+
| Hook | Purpose | Gotcha |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `useFrame((state, delta) => {})` | Per-frame logic (animation, physics) | Never setState inside |
|
|
51
|
+
| `useThree()` | Access gl, scene, camera, size, viewport, pointer | Destructure only what you need |
|
|
52
|
+
| `useLoader(TextureLoader, url)` | Load any Three.js resource | Wrap parent in Suspense |
|
|
53
|
+
| `useGraph(scene)` | Extract nodes/materials from loaded scene | Useful after useGLTF |
|
|
54
|
+
|
|
55
|
+
### useFrame Tips
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
useFrame((state, delta) => {
|
|
59
|
+
// Use delta for framerate-independent animation
|
|
60
|
+
meshRef.current.rotation.y += delta * 0.5
|
|
61
|
+
// Access clock for time-based effects
|
|
62
|
+
material.uniforms.uTime.value = state.clock.elapsedTime
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Drei Essentials
|
|
69
|
+
|
|
70
|
+
| Component | Use Case |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `Environment` | HDRI lighting (presets: studio, sunset, city, forest, dawn) |
|
|
73
|
+
| `Float` | Idle floating animation (speed, rotationIntensity, floatIntensity) |
|
|
74
|
+
| `Text3D` | Extruded 3D text (needs JSON font from Facetype.js) |
|
|
75
|
+
| `useGLTF` | Load .glb/.gltf models (returns { nodes, materials, scene }) |
|
|
76
|
+
| `useGLTF.preload(url)` | Preload model before component mounts |
|
|
77
|
+
| `MeshTransmissionMaterial` | Glass/crystal/liquid refraction effects |
|
|
78
|
+
| `PresentationControls` | Drag-to-rotate for product showcases |
|
|
79
|
+
| `Center` | Auto-center any group of meshes |
|
|
80
|
+
| `Detailed` | LOD -- swap geometry by camera distance |
|
|
81
|
+
| `useTexture` | Load textures with Suspense support |
|
|
82
|
+
| `Instances` | Declarative instancing for repeated meshes |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Postprocessing
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { EffectComposer, Bloom, ChromaticAberration } from '@react-three/postprocessing'
|
|
90
|
+
import { BlendFunction } from 'postprocessing'
|
|
91
|
+
|
|
92
|
+
<EffectComposer>
|
|
93
|
+
<Bloom
|
|
94
|
+
luminanceThreshold={1}
|
|
95
|
+
luminanceSmoothing={0.4}
|
|
96
|
+
intensity={0.6}
|
|
97
|
+
/>
|
|
98
|
+
<ChromaticAberration
|
|
99
|
+
blendFunction={BlendFunction.NORMAL}
|
|
100
|
+
offset={[0.002, 0.002]}
|
|
101
|
+
/>
|
|
102
|
+
</EffectComposer>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Rules:**
|
|
106
|
+
- Bloom is selective by default -- lift material color/emissive above 1.0 to make it glow
|
|
107
|
+
- `luminanceThreshold={1}` = nothing glows unless explicitly emissive
|
|
108
|
+
- Order matters inside EffectComposer
|
|
109
|
+
- Effects are merged into a single pass (performant by design)
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Performance Patterns
|
|
114
|
+
|
|
115
|
+
| Pattern | When |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `<Instances>` / `InstancedMesh` | 100+ identical meshes (particles, trees, crowds) |
|
|
118
|
+
| `<Detailed distances={[0, 50, 100]}>` | LOD: swap hi/lo models by distance |
|
|
119
|
+
| `dispose={null}` on `<primitive>` | Prevent auto-dispose when reusing shared geometry |
|
|
120
|
+
| `useGLTF` + Draco | Compress .glb models (70-90% size reduction) |
|
|
121
|
+
| `useKTX2` (drei) | Compressed GPU textures (1/4 VRAM) — `useTexture` is TextureLoader-only and cannot decode .ktx2 |
|
|
122
|
+
| `frameloop="demand"` on Canvas | Only render when something changes (static scenes) |
|
|
123
|
+
| `invalidate()` from useThree | Trigger a render in demand mode |
|
|
124
|
+
| `<Canvas eventSource={ref}>` | Delegate pointer events to an ancestor element (overlay UIs); rendering stays on the main thread |
|
|
125
|
+
|
|
126
|
+
**Target metrics:** < 100 draw calls, < 1M triangles, 60fps on mid-range GPU.
|
|
127
|
+
Use `stats-gl` or `r3f-perf` to monitor.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Do Not
|
|
132
|
+
|
|
133
|
+
### 1. Never setState in useFrame
|
|
134
|
+
|
|
135
|
+
Causes full React re-render 60x/second. Mutate refs directly.
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
// BAD
|
|
139
|
+
useFrame(() => {
|
|
140
|
+
setRotation(prev => prev + 0.01) // React re-render every frame
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// GOOD
|
|
144
|
+
useFrame((_, delta) => {
|
|
145
|
+
meshRef.current.rotation.y += delta * 0.5 // Direct mutation, zero re-renders
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 2. Never allocate in the render loop
|
|
150
|
+
|
|
151
|
+
`new Vector3()` per frame = GC spikes = stutter.
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
// BAD
|
|
155
|
+
useFrame((state) => {
|
|
156
|
+
const target = new THREE.Vector3(0, Math.sin(state.clock.elapsedTime), 0)
|
|
157
|
+
meshRef.current.position.copy(target)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// GOOD
|
|
161
|
+
const _target = useMemo(() => new THREE.Vector3(), [])
|
|
162
|
+
useFrame((state) => {
|
|
163
|
+
_target.set(0, Math.sin(state.clock.elapsedTime), 0)
|
|
164
|
+
meshRef.current.position.copy(_target)
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 3. Never forget dispose (memory leak)
|
|
169
|
+
|
|
170
|
+
Three.js textures, geometries, and materials live on the GPU. Unmounting a React component does NOT free them.
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
// BAD -- texture stays in VRAM after unmount
|
|
174
|
+
const texture = useLoader(TextureLoader, '/big-texture.jpg')
|
|
175
|
+
|
|
176
|
+
// GOOD -- R3F auto-disposes objects mounted as JSX primitives.
|
|
177
|
+
// useLoader results are CACHED globally: disposing the texture directly
|
|
178
|
+
// poisons the cache and the next consumer renders a disposed resource.
|
|
179
|
+
// Evict from the cache instead, then dispose:
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
return () => {
|
|
182
|
+
useLoader.clear(TextureLoader, '/big-texture.jpg')
|
|
183
|
+
texture.dispose()
|
|
184
|
+
}
|
|
185
|
+
}, [texture])
|
|
186
|
+
|
|
187
|
+
// Resources YOU constructed (not via useLoader) are yours to dispose:
|
|
188
|
+
useEffect(() => () => { geometry.dispose(); material.dispose() }, [])
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 4. Never re-render the Canvas parent
|
|
192
|
+
|
|
193
|
+
Parent state changes re-render the Canvas subtree — the Canvas does not
|
|
194
|
+
remount, but every child re-renders, props reconcile against the scene
|
|
195
|
+
graph, and R3F work you meant to keep out of React's render path runs
|
|
196
|
+
again. Keep fast-changing state out of the Canvas parent (zustand or
|
|
197
|
+
refs).
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
// BAD
|
|
201
|
+
function App() {
|
|
202
|
+
const [uiState, setUiState] = useState(false) // fast-changing state here re-renders the whole Canvas subtree
|
|
203
|
+
return (
|
|
204
|
+
<>
|
|
205
|
+
<button onClick={() => setUiState(!uiState)}>Toggle</button>
|
|
206
|
+
<Canvas><Scene config={uiState} /></Canvas>
|
|
207
|
+
</>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// GOOD -- isolate Canvas in its own component
|
|
212
|
+
function App() {
|
|
213
|
+
return (
|
|
214
|
+
<>
|
|
215
|
+
<UI />
|
|
216
|
+
<SceneCanvas />
|
|
217
|
+
</>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 5. Never load assets without Suspense
|
|
223
|
+
|
|
224
|
+
Loaders (useGLTF, useTexture, useLoader) throw promises. Without Suspense, you get crashes.
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
// BAD
|
|
228
|
+
<Canvas>
|
|
229
|
+
<Model /> {/* useGLTF inside -- will throw */}
|
|
230
|
+
</Canvas>
|
|
231
|
+
|
|
232
|
+
// GOOD
|
|
233
|
+
<Canvas>
|
|
234
|
+
<Suspense fallback={<Loader />}>
|
|
235
|
+
<Model />
|
|
236
|
+
</Suspense>
|
|
237
|
+
</Canvas>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Quick Reference: Loading Sub-resources
|
|
243
|
+
|
|
244
|
+
| Need | Load |
|
|
245
|
+
|---|---|
|
|
246
|
+
| Scene boilerplate, lighting rigs, controls | `scene-setup.md` (this directory) |
|
|
247
|
+
| Custom shaders, GLSL patterns, uniforms | `shaders.md` (this directory) |
|
|
248
|
+
| Animation principles, easing, timing | the brand guidelines' Motion System (§06), reached via `departments/brand/references/uiux-knowledge-and-tools.md` |
|
|
249
|
+
| Scroll-scrubbed pre-rendered worlds (no live engine) | `departments/dev/skills/scroll-world/SKILL.md` |
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Scene Setup Reference
|
|
2
|
+
|
|
3
|
+
> Complete R3F boilerplate patterns with TypeScript.
|
|
4
|
+
> Copy-paste ready. Adapt to your project.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Minimal Canvas Setup
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { Canvas } from '@react-three/fiber'
|
|
12
|
+
import { Suspense } from 'react'
|
|
13
|
+
|
|
14
|
+
function App() {
|
|
15
|
+
return (
|
|
16
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
17
|
+
<Canvas
|
|
18
|
+
camera={{ position: [0, 2, 5], fov: 45, near: 0.1, far: 100 }}
|
|
19
|
+
dpr={[1, 2]}
|
|
20
|
+
gl={{ antialias: true, alpha: false, powerPreference: 'high-performance' }}
|
|
21
|
+
>
|
|
22
|
+
<Suspense fallback={null}>
|
|
23
|
+
<Scene />
|
|
24
|
+
</Suspense>
|
|
25
|
+
</Canvas>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Props breakdown:**
|
|
32
|
+
- `dpr={[1, 2]}` -- clamp device pixel ratio (retina without burning GPUs)
|
|
33
|
+
- `alpha: false` -- opaque background = faster compositing
|
|
34
|
+
- `powerPreference: 'high-performance'` -- request discrete GPU on laptops
|
|
35
|
+
- `frameloop="demand"` -- add for static scenes (render only on invalidate)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Lighting Rigs
|
|
40
|
+
|
|
41
|
+
### Studio (product shots, clean shadows)
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
function StudioLighting() {
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<ambientLight intensity={0.4} />
|
|
48
|
+
<directionalLight
|
|
49
|
+
position={[5, 8, 3]}
|
|
50
|
+
intensity={1.2}
|
|
51
|
+
castShadow
|
|
52
|
+
shadow-mapSize-width={2048}
|
|
53
|
+
shadow-mapSize-height={2048}
|
|
54
|
+
shadow-camera-near={0.5}
|
|
55
|
+
shadow-camera-far={50}
|
|
56
|
+
shadow-camera-left={-10}
|
|
57
|
+
shadow-camera-right={10}
|
|
58
|
+
shadow-camera-top={10}
|
|
59
|
+
shadow-camera-bottom={-10}
|
|
60
|
+
/>
|
|
61
|
+
<directionalLight position={[-3, 4, -5]} intensity={0.3} />
|
|
62
|
+
</>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Studio with HDRI (easiest, best quality)
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { Environment } from '@react-three/drei'
|
|
71
|
+
|
|
72
|
+
function StudioHDRI() {
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<Environment preset="studio" background={false} />
|
|
76
|
+
{/* Optional fill light to reduce contrast */}
|
|
77
|
+
<ambientLight intensity={0.2} />
|
|
78
|
+
</>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Environment presets:** `apartment`, `city`, `dawn`, `forest`, `lobby`, `night`, `park`, `studio`, `sunset`, `warehouse`
|
|
84
|
+
|
|
85
|
+
### Outdoor (natural, directional sun)
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
function OutdoorLighting() {
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
<Environment preset="sunset" background />
|
|
92
|
+
<directionalLight
|
|
93
|
+
position={[10, 15, 5]}
|
|
94
|
+
intensity={2}
|
|
95
|
+
castShadow
|
|
96
|
+
shadow-mapSize={2048}
|
|
97
|
+
color="#ffeedd"
|
|
98
|
+
/>
|
|
99
|
+
<hemisphereLight args={['#87CEEB', '#362907', 0.3]} />
|
|
100
|
+
</>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Dramatic (high contrast, noir)
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
function DramaticLighting() {
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
<color attach="background" args={['#000000']} />
|
|
112
|
+
<fog attach="fog" args={['#000000', 5, 20]} />
|
|
113
|
+
<spotLight
|
|
114
|
+
position={[3, 8, 2]}
|
|
115
|
+
angle={0.3}
|
|
116
|
+
penumbra={0.8}
|
|
117
|
+
intensity={3}
|
|
118
|
+
castShadow
|
|
119
|
+
color="#ff6644"
|
|
120
|
+
/>
|
|
121
|
+
<pointLight position={[-5, 2, -3]} intensity={0.5} color="#4466ff" />
|
|
122
|
+
{/* No ambient -- pure darkness outside lights */}
|
|
123
|
+
</>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Camera Configurations
|
|
131
|
+
|
|
132
|
+
### Perspective (default, most scenes)
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<Canvas camera={{ position: [0, 2, 5], fov: 45 }}>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- **fov 35-45:** Natural, cinematic feel
|
|
139
|
+
- **fov 60-75:** Wider, more immersive
|
|
140
|
+
- **fov 20-30:** Telephoto, compressed depth
|
|
141
|
+
|
|
142
|
+
### Orthographic (isometric, UI, 2D-in-3D)
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
<Canvas orthographic camera={{ position: [0, 5, 10], zoom: 50 }}>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Animated Camera (smooth transitions)
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { useFrame, useThree } from '@react-three/fiber'
|
|
152
|
+
import { useMemo } from 'react'
|
|
153
|
+
import * as THREE from 'three'
|
|
154
|
+
|
|
155
|
+
function CameraRig({ target }: { target: [number, number, number] }) {
|
|
156
|
+
const _target = useMemo(() => new THREE.Vector3(), [])
|
|
157
|
+
const _position = useMemo(() => new THREE.Vector3(), [])
|
|
158
|
+
|
|
159
|
+
useFrame((state, delta) => {
|
|
160
|
+
// Smooth lerp to target
|
|
161
|
+
_target.set(...target)
|
|
162
|
+
state.camera.position.lerp(_position.set(target[0], target[1] + 2, target[2] + 5), delta * 2)
|
|
163
|
+
state.camera.lookAt(_target)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Controls
|
|
173
|
+
|
|
174
|
+
### OrbitControls (inspect / product viewer)
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import { OrbitControls } from '@react-three/drei'
|
|
178
|
+
|
|
179
|
+
<OrbitControls
|
|
180
|
+
makeDefault
|
|
181
|
+
enableDamping
|
|
182
|
+
dampingFactor={0.05}
|
|
183
|
+
minDistance={2}
|
|
184
|
+
maxDistance={20}
|
|
185
|
+
minPolarAngle={0}
|
|
186
|
+
maxPolarAngle={Math.PI / 2} // prevent going below ground
|
|
187
|
+
autoRotate={false}
|
|
188
|
+
autoRotateSpeed={0.5}
|
|
189
|
+
/>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### PresentationControls (drag-to-rotate, bounded)
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
import { PresentationControls } from '@react-three/drei'
|
|
196
|
+
|
|
197
|
+
<PresentationControls
|
|
198
|
+
global // listen on entire canvas
|
|
199
|
+
snap // snap back to rest
|
|
200
|
+
speed={1.5}
|
|
201
|
+
zoom={0.8}
|
|
202
|
+
rotation={[0, -Math.PI / 4, 0]} // initial angle
|
|
203
|
+
polar={[-Math.PI / 4, Math.PI / 4]}
|
|
204
|
+
azimuth={[-Math.PI / 4, Math.PI / 4]}
|
|
205
|
+
>
|
|
206
|
+
<Model />
|
|
207
|
+
</PresentationControls>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### MapControls (top-down, pan-based)
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
import { MapControls } from '@react-three/drei'
|
|
214
|
+
|
|
215
|
+
<MapControls
|
|
216
|
+
makeDefault
|
|
217
|
+
enableRotate={false}
|
|
218
|
+
minDistance={5}
|
|
219
|
+
maxDistance={50}
|
|
220
|
+
/>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Pointer-based camera (follow cursor)
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
function PointerCamera() {
|
|
227
|
+
useFrame((state) => {
|
|
228
|
+
const { x, y } = state.pointer // normalized -1 to 1
|
|
229
|
+
state.camera.position.x = THREE.MathUtils.lerp(state.camera.position.x, x * 2, 0.05)
|
|
230
|
+
state.camera.position.y = THREE.MathUtils.lerp(state.camera.position.y, y * 1.5 + 1, 0.05)
|
|
231
|
+
state.camera.lookAt(0, 0, 0)
|
|
232
|
+
})
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Loading Models
|
|
240
|
+
|
|
241
|
+
### useGLTF (standard)
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import * as THREE from 'three'
|
|
245
|
+
import { useGLTF } from '@react-three/drei'
|
|
246
|
+
import type { ComponentProps } from 'react'
|
|
247
|
+
import type { GLTF } from 'three-stdlib'
|
|
248
|
+
|
|
249
|
+
type ModelGLTF = GLTF & {
|
|
250
|
+
nodes: {
|
|
251
|
+
Body: THREE.Mesh
|
|
252
|
+
Eyes: THREE.Mesh
|
|
253
|
+
}
|
|
254
|
+
materials: {
|
|
255
|
+
skin: THREE.MeshStandardMaterial
|
|
256
|
+
eyes: THREE.MeshStandardMaterial
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// React 19 typings removed the global JSX namespace; use the element's
|
|
261
|
+
// component props. The double cast is deliberate: GLTF's nodes/materials
|
|
262
|
+
// are loosely typed upstream, so the assertion must go through unknown.
|
|
263
|
+
function Model(props: ComponentProps<'group'>) {
|
|
264
|
+
const { nodes, materials } = useGLTF('/model.glb') as unknown as ModelGLTF
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<group {...props} dispose={null}>
|
|
268
|
+
<mesh geometry={nodes.Body.geometry} material={materials.skin} />
|
|
269
|
+
<mesh geometry={nodes.Eyes.geometry} material={materials.eyes} />
|
|
270
|
+
</group>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
useGLTF.preload('/model.glb')
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Tip:** Use [gltf.pmnd.rs](https://gltf.pmnd.rs/) to auto-generate typed R3F components from .glb files.
|
|
278
|
+
|
|
279
|
+
### useGLTF with Draco compression
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
import { useGLTF } from '@react-three/drei'
|
|
283
|
+
|
|
284
|
+
// Draco decoder loaded from CDN automatically
|
|
285
|
+
const { nodes } = useGLTF('/model-draco.glb', true) // second arg enables Draco
|
|
286
|
+
|
|
287
|
+
useGLTF.preload('/model-draco.glb', true)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### useTexture
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
import { useTexture } from '@react-three/drei'
|
|
294
|
+
|
|
295
|
+
function TexturedMesh() {
|
|
296
|
+
const [colorMap, normalMap, roughnessMap] = useTexture([
|
|
297
|
+
'/textures/color.jpg',
|
|
298
|
+
'/textures/normal.jpg',
|
|
299
|
+
'/textures/roughness.jpg',
|
|
300
|
+
])
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<mesh>
|
|
304
|
+
<sphereGeometry args={[1, 64, 64]} />
|
|
305
|
+
<meshStandardMaterial
|
|
306
|
+
map={colorMap}
|
|
307
|
+
normalMap={normalMap}
|
|
308
|
+
roughnessMap={roughnessMap}
|
|
309
|
+
/>
|
|
310
|
+
</mesh>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
useTexture.preload(['/textures/color.jpg', '/textures/normal.jpg', '/textures/roughness.jpg'])
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Responsive Canvas
|
|
320
|
+
|
|
321
|
+
### Fill parent container
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
// Parent must have explicit dimensions
|
|
325
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
326
|
+
<Canvas>
|
|
327
|
+
<Scene />
|
|
328
|
+
</Canvas>
|
|
329
|
+
</div>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Respond to viewport size inside scene
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
function ResponsiveObject() {
|
|
336
|
+
const { viewport } = useThree()
|
|
337
|
+
// viewport.width/height = world units visible at z=0
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<mesh scale={viewport.width > 6 ? 1 : 0.6}>
|
|
341
|
+
<boxGeometry />
|
|
342
|
+
<meshStandardMaterial />
|
|
343
|
+
</mesh>
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Adaptive performance
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
import { useState } from 'react'
|
|
352
|
+
import { Canvas } from '@react-three/fiber'
|
|
353
|
+
import { PerformanceMonitor } from '@react-three/drei'
|
|
354
|
+
|
|
355
|
+
function App() {
|
|
356
|
+
const [dpr, setDpr] = useState(1.5)
|
|
357
|
+
return (
|
|
358
|
+
<Canvas dpr={dpr}>
|
|
359
|
+
<PerformanceMonitor
|
|
360
|
+
onIncline={() => setDpr(2)}
|
|
361
|
+
onDecline={() => setDpr(1)}
|
|
362
|
+
>
|
|
363
|
+
<Scene />
|
|
364
|
+
</PerformanceMonitor>
|
|
365
|
+
</Canvas>
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Shadows Setup
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
<Canvas shadows>
|
|
376
|
+
<directionalLight castShadow shadow-mapSize={2048} />
|
|
377
|
+
<mesh castShadow>
|
|
378
|
+
<boxGeometry />
|
|
379
|
+
<meshStandardMaterial />
|
|
380
|
+
</mesh>
|
|
381
|
+
<mesh receiveShadow rotation-x={-Math.PI / 2} position-y={-1}>
|
|
382
|
+
<planeGeometry args={[20, 20]} />
|
|
383
|
+
<shadowMaterial opacity={0.3} />
|
|
384
|
+
</mesh>
|
|
385
|
+
</Canvas>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
- `shadows` on Canvas enables the shadow system
|
|
389
|
+
- `castShadow` on lights and meshes that cast
|
|
390
|
+
- `receiveShadow` on meshes that receive
|
|
391
|
+
- `<shadowMaterial>` for transparent ground shadows (product shots)
|
|
392
|
+
- `<ContactShadows>` from drei for soft baked-look shadows without shadow maps
|
|
393
|
+
|
|
394
|
+
```tsx
|
|
395
|
+
import { ContactShadows } from '@react-three/drei'
|
|
396
|
+
|
|
397
|
+
<ContactShadows
|
|
398
|
+
position={[0, -1, 0]}
|
|
399
|
+
opacity={0.4}
|
|
400
|
+
scale={10}
|
|
401
|
+
blur={2}
|
|
402
|
+
far={4}
|
|
403
|
+
/>
|
|
404
|
+
```
|