omgkit 2.1.0 → 2.2.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/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,59 +1,1328 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: threejs
|
|
3
|
-
description: Three.js 3D graphics
|
|
3
|
+
description: Three.js 3D graphics with WebGL, React Three Fiber, shaders, physics, and immersive experiences
|
|
4
|
+
category: frontend
|
|
5
|
+
triggers:
|
|
6
|
+
- three.js
|
|
7
|
+
- threejs
|
|
8
|
+
- 3d graphics
|
|
9
|
+
- webgl
|
|
10
|
+
- react three fiber
|
|
11
|
+
- r3f
|
|
12
|
+
- 3d animation
|
|
13
|
+
- shaders
|
|
14
|
+
- 3d scenes
|
|
4
15
|
---
|
|
5
16
|
|
|
6
|
-
# Three.js
|
|
17
|
+
# Three.js 3D Graphics
|
|
7
18
|
|
|
8
|
-
|
|
9
|
-
|
|
19
|
+
Enterprise-grade **Three.js 3D graphics** following industry best practices. This skill covers scene setup, React Three Fiber integration, materials and lighting, animations, shaders, physics simulation, and performance optimization used by top engineering teams.
|
|
20
|
+
|
|
21
|
+
## Purpose
|
|
22
|
+
|
|
23
|
+
Build immersive 3D web experiences:
|
|
24
|
+
|
|
25
|
+
- Create interactive 3D scenes with proper camera controls
|
|
26
|
+
- Build declarative 3D with React Three Fiber
|
|
27
|
+
- Implement physically-based rendering (PBR) materials
|
|
28
|
+
- Add realistic lighting and shadows
|
|
29
|
+
- Create smooth animations and transitions
|
|
30
|
+
- Write custom shaders for visual effects
|
|
31
|
+
- Integrate physics simulations
|
|
32
|
+
- Optimize performance for production
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
### 1. Core Three.js Setup
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// lib/three/SceneManager.ts
|
|
10
40
|
import * as THREE from 'three';
|
|
41
|
+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
|
42
|
+
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
|
|
43
|
+
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
|
|
44
|
+
|
|
45
|
+
interface SceneManagerOptions {
|
|
46
|
+
container: HTMLElement;
|
|
47
|
+
width?: number;
|
|
48
|
+
height?: number;
|
|
49
|
+
antialias?: boolean;
|
|
50
|
+
alpha?: boolean;
|
|
51
|
+
pixelRatio?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class SceneManager {
|
|
55
|
+
private scene: THREE.Scene;
|
|
56
|
+
private camera: THREE.PerspectiveCamera;
|
|
57
|
+
private renderer: THREE.WebGLRenderer;
|
|
58
|
+
private controls: OrbitControls;
|
|
59
|
+
private composer: EffectComposer;
|
|
60
|
+
private clock: THREE.Clock;
|
|
61
|
+
private animationFrameId: number | null = null;
|
|
62
|
+
private updateCallbacks: Set<(delta: number, elapsed: number) => void> = new Set();
|
|
63
|
+
|
|
64
|
+
constructor(options: SceneManagerOptions) {
|
|
65
|
+
const {
|
|
66
|
+
container,
|
|
67
|
+
width = container.clientWidth,
|
|
68
|
+
height = container.clientHeight,
|
|
69
|
+
antialias = true,
|
|
70
|
+
alpha = false,
|
|
71
|
+
pixelRatio = Math.min(window.devicePixelRatio, 2),
|
|
72
|
+
} = options;
|
|
73
|
+
|
|
74
|
+
// Scene
|
|
75
|
+
this.scene = new THREE.Scene();
|
|
76
|
+
this.scene.background = alpha ? null : new THREE.Color(0x111111);
|
|
77
|
+
|
|
78
|
+
// Camera
|
|
79
|
+
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
|
80
|
+
this.camera.position.set(0, 2, 5);
|
|
81
|
+
|
|
82
|
+
// Renderer
|
|
83
|
+
this.renderer = new THREE.WebGLRenderer({
|
|
84
|
+
antialias,
|
|
85
|
+
alpha,
|
|
86
|
+
powerPreference: 'high-performance',
|
|
87
|
+
});
|
|
88
|
+
this.renderer.setSize(width, height);
|
|
89
|
+
this.renderer.setPixelRatio(pixelRatio);
|
|
90
|
+
this.renderer.shadowMap.enabled = true;
|
|
91
|
+
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
92
|
+
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
93
|
+
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
94
|
+
this.renderer.toneMappingExposure = 1.0;
|
|
95
|
+
container.appendChild(this.renderer.domElement);
|
|
96
|
+
|
|
97
|
+
// Controls
|
|
98
|
+
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
|
99
|
+
this.controls.enableDamping = true;
|
|
100
|
+
this.controls.dampingFactor = 0.05;
|
|
101
|
+
this.controls.minDistance = 1;
|
|
102
|
+
this.controls.maxDistance = 100;
|
|
103
|
+
|
|
104
|
+
// Post-processing
|
|
105
|
+
this.composer = new EffectComposer(this.renderer);
|
|
106
|
+
this.composer.addPass(new RenderPass(this.scene, this.camera));
|
|
107
|
+
|
|
108
|
+
// Clock
|
|
109
|
+
this.clock = new THREE.Clock();
|
|
110
|
+
|
|
111
|
+
// Handle resize
|
|
112
|
+
this.handleResize = this.handleResize.bind(this);
|
|
113
|
+
window.addEventListener('resize', this.handleResize);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private handleResize(): void {
|
|
117
|
+
const container = this.renderer.domElement.parentElement;
|
|
118
|
+
if (!container) return;
|
|
119
|
+
|
|
120
|
+
const width = container.clientWidth;
|
|
121
|
+
const height = container.clientHeight;
|
|
11
122
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
123
|
+
this.camera.aspect = width / height;
|
|
124
|
+
this.camera.updateProjectionMatrix();
|
|
125
|
+
|
|
126
|
+
this.renderer.setSize(width, height);
|
|
127
|
+
this.composer.setSize(width, height);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public addUpdateCallback(callback: (delta: number, elapsed: number) => void): void {
|
|
131
|
+
this.updateCallbacks.add(callback);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public removeUpdateCallback(callback: (delta: number, elapsed: number) => void): void {
|
|
135
|
+
this.updateCallbacks.delete(callback);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public start(): void {
|
|
139
|
+
if (this.animationFrameId !== null) return;
|
|
140
|
+
|
|
141
|
+
const animate = () => {
|
|
142
|
+
this.animationFrameId = requestAnimationFrame(animate);
|
|
143
|
+
|
|
144
|
+
const delta = this.clock.getDelta();
|
|
145
|
+
const elapsed = this.clock.getElapsedTime();
|
|
146
|
+
|
|
147
|
+
// Update controls
|
|
148
|
+
this.controls.update();
|
|
149
|
+
|
|
150
|
+
// Run update callbacks
|
|
151
|
+
this.updateCallbacks.forEach((callback) => callback(delta, elapsed));
|
|
152
|
+
|
|
153
|
+
// Render
|
|
154
|
+
this.composer.render();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
animate();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public stop(): void {
|
|
161
|
+
if (this.animationFrameId !== null) {
|
|
162
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
163
|
+
this.animationFrameId = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public getScene(): THREE.Scene {
|
|
168
|
+
return this.scene;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public getCamera(): THREE.PerspectiveCamera {
|
|
172
|
+
return this.camera;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public getRenderer(): THREE.WebGLRenderer {
|
|
176
|
+
return this.renderer;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public dispose(): void {
|
|
180
|
+
this.stop();
|
|
181
|
+
window.removeEventListener('resize', this.handleResize);
|
|
182
|
+
|
|
183
|
+
// Dispose of all objects in scene
|
|
184
|
+
this.scene.traverse((object) => {
|
|
185
|
+
if (object instanceof THREE.Mesh) {
|
|
186
|
+
object.geometry.dispose();
|
|
187
|
+
if (Array.isArray(object.material)) {
|
|
188
|
+
object.material.forEach((m) => m.dispose());
|
|
189
|
+
} else {
|
|
190
|
+
object.material.dispose();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
this.renderer.dispose();
|
|
196
|
+
this.controls.dispose();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
15
199
|
|
|
16
|
-
|
|
17
|
-
document.
|
|
200
|
+
// Usage
|
|
201
|
+
const container = document.getElementById('canvas-container')!;
|
|
202
|
+
const sceneManager = new SceneManager({ container, antialias: true });
|
|
203
|
+
|
|
204
|
+
// Add objects
|
|
205
|
+
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
|
206
|
+
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
|
|
207
|
+
const cube = new THREE.Mesh(geometry, material);
|
|
208
|
+
cube.castShadow = true;
|
|
209
|
+
sceneManager.getScene().add(cube);
|
|
210
|
+
|
|
211
|
+
// Add lights
|
|
212
|
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
|
213
|
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
|
214
|
+
directionalLight.position.set(5, 10, 5);
|
|
215
|
+
directionalLight.castShadow = true;
|
|
216
|
+
sceneManager.getScene().add(ambientLight, directionalLight);
|
|
217
|
+
|
|
218
|
+
// Animation
|
|
219
|
+
sceneManager.addUpdateCallback((delta) => {
|
|
220
|
+
cube.rotation.x += delta;
|
|
221
|
+
cube.rotation.y += delta * 0.5;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
sceneManager.start();
|
|
18
225
|
```
|
|
19
226
|
|
|
20
|
-
|
|
227
|
+
### 2. React Three Fiber Integration
|
|
228
|
+
|
|
21
229
|
```tsx
|
|
230
|
+
// components/Scene.tsx
|
|
231
|
+
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
|
232
|
+
import {
|
|
233
|
+
OrbitControls,
|
|
234
|
+
Environment,
|
|
235
|
+
ContactShadows,
|
|
236
|
+
PerspectiveCamera,
|
|
237
|
+
useGLTF,
|
|
238
|
+
Float,
|
|
239
|
+
Text3D,
|
|
240
|
+
Center,
|
|
241
|
+
MeshTransmissionMaterial,
|
|
242
|
+
} from '@react-three/drei';
|
|
243
|
+
import { Suspense, useRef, useState } from 'react';
|
|
244
|
+
import * as THREE from 'three';
|
|
245
|
+
|
|
246
|
+
// Animated box component
|
|
247
|
+
function AnimatedBox({ position }: { position: [number, number, number] }) {
|
|
248
|
+
const meshRef = useRef<THREE.Mesh>(null);
|
|
249
|
+
const [hovered, setHovered] = useState(false);
|
|
250
|
+
const [clicked, setClicked] = useState(false);
|
|
251
|
+
|
|
252
|
+
useFrame((state, delta) => {
|
|
253
|
+
if (meshRef.current) {
|
|
254
|
+
meshRef.current.rotation.x += delta * 0.5;
|
|
255
|
+
meshRef.current.rotation.y += delta * 0.3;
|
|
256
|
+
|
|
257
|
+
// Smooth scale animation
|
|
258
|
+
const targetScale = clicked ? 1.5 : hovered ? 1.2 : 1;
|
|
259
|
+
meshRef.current.scale.lerp(
|
|
260
|
+
new THREE.Vector3(targetScale, targetScale, targetScale),
|
|
261
|
+
0.1
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<mesh
|
|
268
|
+
ref={meshRef}
|
|
269
|
+
position={position}
|
|
270
|
+
onPointerOver={() => setHovered(true)}
|
|
271
|
+
onPointerOut={() => setHovered(false)}
|
|
272
|
+
onClick={() => setClicked(!clicked)}
|
|
273
|
+
>
|
|
274
|
+
<boxGeometry args={[1, 1, 1]} />
|
|
275
|
+
<meshStandardMaterial
|
|
276
|
+
color={hovered ? '#ff6b6b' : '#4ecdc4'}
|
|
277
|
+
metalness={0.5}
|
|
278
|
+
roughness={0.2}
|
|
279
|
+
/>
|
|
280
|
+
</mesh>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// GLTF Model loader
|
|
285
|
+
function Model({ url, scale = 1 }: { url: string; scale?: number }) {
|
|
286
|
+
const { scene } = useGLTF(url);
|
|
287
|
+
const modelRef = useRef<THREE.Group>(null);
|
|
288
|
+
|
|
289
|
+
// Clone materials for unique instances
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
scene.traverse((child) => {
|
|
292
|
+
if (child instanceof THREE.Mesh) {
|
|
293
|
+
child.castShadow = true;
|
|
294
|
+
child.receiveShadow = true;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}, [scene]);
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<primitive
|
|
301
|
+
ref={modelRef}
|
|
302
|
+
object={scene}
|
|
303
|
+
scale={scale}
|
|
304
|
+
dispose={null}
|
|
305
|
+
/>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Glass material sphere
|
|
310
|
+
function GlassSphere() {
|
|
311
|
+
return (
|
|
312
|
+
<mesh position={[2, 1, 0]}>
|
|
313
|
+
<sphereGeometry args={[0.8, 64, 64]} />
|
|
314
|
+
<MeshTransmissionMaterial
|
|
315
|
+
backside
|
|
316
|
+
samples={16}
|
|
317
|
+
thickness={0.5}
|
|
318
|
+
chromaticAberration={0.5}
|
|
319
|
+
anisotropy={0.3}
|
|
320
|
+
distortion={0.2}
|
|
321
|
+
distortionScale={0.5}
|
|
322
|
+
temporalDistortion={0.1}
|
|
323
|
+
iridescence={1}
|
|
324
|
+
iridescenceIOR={1}
|
|
325
|
+
iridescenceThicknessRange={[0, 1400]}
|
|
326
|
+
/>
|
|
327
|
+
</mesh>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 3D Text
|
|
332
|
+
function Text3DComponent({ text }: { text: string }) {
|
|
333
|
+
return (
|
|
334
|
+
<Center position={[0, 2, 0]}>
|
|
335
|
+
<Float speed={2} rotationIntensity={0.5} floatIntensity={1}>
|
|
336
|
+
<Text3D
|
|
337
|
+
font="/fonts/Inter_Bold.json"
|
|
338
|
+
size={0.5}
|
|
339
|
+
height={0.1}
|
|
340
|
+
curveSegments={12}
|
|
341
|
+
>
|
|
342
|
+
{text}
|
|
343
|
+
<meshStandardMaterial color="#ff6b6b" metalness={0.8} roughness={0.2} />
|
|
344
|
+
</Text3D>
|
|
345
|
+
</Float>
|
|
346
|
+
</Center>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Camera rig with smooth following
|
|
351
|
+
function CameraRig({ target }: { target: THREE.Vector3 }) {
|
|
352
|
+
const { camera } = useThree();
|
|
353
|
+
|
|
354
|
+
useFrame(() => {
|
|
355
|
+
camera.position.lerp(
|
|
356
|
+
new THREE.Vector3(target.x + 5, target.y + 3, target.z + 5),
|
|
357
|
+
0.02
|
|
358
|
+
);
|
|
359
|
+
camera.lookAt(target);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Main scene
|
|
366
|
+
export function Scene() {
|
|
367
|
+
return (
|
|
368
|
+
<Canvas
|
|
369
|
+
shadows
|
|
370
|
+
dpr={[1, 2]}
|
|
371
|
+
gl={{
|
|
372
|
+
antialias: true,
|
|
373
|
+
toneMapping: THREE.ACESFilmicToneMapping,
|
|
374
|
+
toneMappingExposure: 1,
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
<PerspectiveCamera makeDefault position={[5, 3, 5]} fov={50} />
|
|
378
|
+
<OrbitControls
|
|
379
|
+
enableDamping
|
|
380
|
+
dampingFactor={0.05}
|
|
381
|
+
minDistance={2}
|
|
382
|
+
maxDistance={20}
|
|
383
|
+
/>
|
|
384
|
+
|
|
385
|
+
{/* Lighting */}
|
|
386
|
+
<ambientLight intensity={0.5} />
|
|
387
|
+
<directionalLight
|
|
388
|
+
position={[10, 10, 5]}
|
|
389
|
+
intensity={1.5}
|
|
390
|
+
castShadow
|
|
391
|
+
shadow-mapSize={[2048, 2048]}
|
|
392
|
+
shadow-camera-far={50}
|
|
393
|
+
shadow-camera-left={-10}
|
|
394
|
+
shadow-camera-right={10}
|
|
395
|
+
shadow-camera-top={10}
|
|
396
|
+
shadow-camera-bottom={-10}
|
|
397
|
+
/>
|
|
398
|
+
|
|
399
|
+
{/* Environment */}
|
|
400
|
+
<Environment preset="city" />
|
|
401
|
+
|
|
402
|
+
{/* Content */}
|
|
403
|
+
<Suspense fallback={null}>
|
|
404
|
+
<AnimatedBox position={[-2, 0.5, 0]} />
|
|
405
|
+
<AnimatedBox position={[0, 0.5, 0]} />
|
|
406
|
+
<GlassSphere />
|
|
407
|
+
<Text3DComponent text="Hello 3D" />
|
|
408
|
+
|
|
409
|
+
{/* Ground */}
|
|
410
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]} receiveShadow>
|
|
411
|
+
<planeGeometry args={[50, 50]} />
|
|
412
|
+
<meshStandardMaterial color="#1a1a2e" />
|
|
413
|
+
</mesh>
|
|
414
|
+
</Suspense>
|
|
415
|
+
|
|
416
|
+
{/* Contact shadows */}
|
|
417
|
+
<ContactShadows
|
|
418
|
+
position={[0, -0.49, 0]}
|
|
419
|
+
opacity={0.5}
|
|
420
|
+
scale={20}
|
|
421
|
+
blur={2}
|
|
422
|
+
far={10}
|
|
423
|
+
/>
|
|
424
|
+
</Canvas>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### 3. Custom Shaders
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
// shaders/GradientMaterial.tsx
|
|
433
|
+
import { shaderMaterial } from '@react-three/drei';
|
|
434
|
+
import { extend, useFrame } from '@react-three/fiber';
|
|
435
|
+
import { useRef } from 'react';
|
|
436
|
+
import * as THREE from 'three';
|
|
437
|
+
|
|
438
|
+
// Vertex shader
|
|
439
|
+
const vertexShader = `
|
|
440
|
+
varying vec2 vUv;
|
|
441
|
+
varying vec3 vPosition;
|
|
442
|
+
varying vec3 vNormal;
|
|
443
|
+
|
|
444
|
+
uniform float uTime;
|
|
445
|
+
uniform float uAmplitude;
|
|
446
|
+
uniform float uFrequency;
|
|
447
|
+
|
|
448
|
+
void main() {
|
|
449
|
+
vUv = uv;
|
|
450
|
+
vPosition = position;
|
|
451
|
+
vNormal = normal;
|
|
452
|
+
|
|
453
|
+
// Wave displacement
|
|
454
|
+
vec3 pos = position;
|
|
455
|
+
float displacement = sin(pos.x * uFrequency + uTime) *
|
|
456
|
+
sin(pos.z * uFrequency + uTime) *
|
|
457
|
+
uAmplitude;
|
|
458
|
+
pos.y += displacement;
|
|
459
|
+
|
|
460
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
|
461
|
+
}
|
|
462
|
+
`;
|
|
463
|
+
|
|
464
|
+
// Fragment shader
|
|
465
|
+
const fragmentShader = `
|
|
466
|
+
varying vec2 vUv;
|
|
467
|
+
varying vec3 vPosition;
|
|
468
|
+
varying vec3 vNormal;
|
|
469
|
+
|
|
470
|
+
uniform float uTime;
|
|
471
|
+
uniform vec3 uColorA;
|
|
472
|
+
uniform vec3 uColorB;
|
|
473
|
+
uniform float uNoiseScale;
|
|
474
|
+
|
|
475
|
+
// Simplex noise function
|
|
476
|
+
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
477
|
+
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
478
|
+
vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
|
|
479
|
+
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
|
|
480
|
+
|
|
481
|
+
float snoise(vec3 v) {
|
|
482
|
+
const vec2 C = vec2(1.0/6.0, 1.0/3.0);
|
|
483
|
+
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
|
484
|
+
|
|
485
|
+
vec3 i = floor(v + dot(v, C.yyy));
|
|
486
|
+
vec3 x0 = v - i + dot(i, C.xxx);
|
|
487
|
+
|
|
488
|
+
vec3 g = step(x0.yzx, x0.xyz);
|
|
489
|
+
vec3 l = 1.0 - g;
|
|
490
|
+
vec3 i1 = min(g.xyz, l.zxy);
|
|
491
|
+
vec3 i2 = max(g.xyz, l.zxy);
|
|
492
|
+
|
|
493
|
+
vec3 x1 = x0 - i1 + C.xxx;
|
|
494
|
+
vec3 x2 = x0 - i2 + C.yyy;
|
|
495
|
+
vec3 x3 = x0 - D.yyy;
|
|
496
|
+
|
|
497
|
+
i = mod289(i);
|
|
498
|
+
vec4 p = permute(permute(permute(
|
|
499
|
+
i.z + vec4(0.0, i1.z, i2.z, 1.0))
|
|
500
|
+
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
|
|
501
|
+
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
|
|
502
|
+
|
|
503
|
+
float n_ = 0.142857142857;
|
|
504
|
+
vec3 ns = n_ * D.wyz - D.xzx;
|
|
505
|
+
|
|
506
|
+
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
507
|
+
|
|
508
|
+
vec4 x_ = floor(j * ns.z);
|
|
509
|
+
vec4 y_ = floor(j - 7.0 * x_);
|
|
510
|
+
|
|
511
|
+
vec4 x = x_ *ns.x + ns.yyyy;
|
|
512
|
+
vec4 y = y_ *ns.x + ns.yyyy;
|
|
513
|
+
vec4 h = 1.0 - abs(x) - abs(y);
|
|
514
|
+
|
|
515
|
+
vec4 b0 = vec4(x.xy, y.xy);
|
|
516
|
+
vec4 b1 = vec4(x.zw, y.zw);
|
|
517
|
+
|
|
518
|
+
vec4 s0 = floor(b0)*2.0 + 1.0;
|
|
519
|
+
vec4 s1 = floor(b1)*2.0 + 1.0;
|
|
520
|
+
vec4 sh = -step(h, vec4(0.0));
|
|
521
|
+
|
|
522
|
+
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;
|
|
523
|
+
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;
|
|
524
|
+
|
|
525
|
+
vec3 p0 = vec3(a0.xy,h.x);
|
|
526
|
+
vec3 p1 = vec3(a0.zw,h.y);
|
|
527
|
+
vec3 p2 = vec3(a1.xy,h.z);
|
|
528
|
+
vec3 p3 = vec3(a1.zw,h.w);
|
|
529
|
+
|
|
530
|
+
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
|
|
531
|
+
p0 *= norm.x;
|
|
532
|
+
p1 *= norm.y;
|
|
533
|
+
p2 *= norm.z;
|
|
534
|
+
p3 *= norm.w;
|
|
535
|
+
|
|
536
|
+
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
|
537
|
+
m = m * m;
|
|
538
|
+
return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
void main() {
|
|
542
|
+
// Animated noise
|
|
543
|
+
float noise = snoise(vec3(vPosition.xz * uNoiseScale, uTime * 0.3));
|
|
544
|
+
noise = noise * 0.5 + 0.5; // Normalize to 0-1
|
|
545
|
+
|
|
546
|
+
// Gradient based on UV and noise
|
|
547
|
+
float gradient = vUv.y + noise * 0.3;
|
|
548
|
+
|
|
549
|
+
// Mix colors
|
|
550
|
+
vec3 color = mix(uColorA, uColorB, gradient);
|
|
551
|
+
|
|
552
|
+
// Add rim lighting
|
|
553
|
+
vec3 viewDirection = normalize(cameraPosition - vPosition);
|
|
554
|
+
float rimLight = 1.0 - max(0.0, dot(viewDirection, vNormal));
|
|
555
|
+
rimLight = pow(rimLight, 3.0);
|
|
556
|
+
color += rimLight * 0.3;
|
|
557
|
+
|
|
558
|
+
gl_FragColor = vec4(color, 1.0);
|
|
559
|
+
}
|
|
560
|
+
`;
|
|
561
|
+
|
|
562
|
+
// Create shader material
|
|
563
|
+
const GradientMaterial = shaderMaterial(
|
|
564
|
+
{
|
|
565
|
+
uTime: 0,
|
|
566
|
+
uColorA: new THREE.Color('#ff6b6b'),
|
|
567
|
+
uColorB: new THREE.Color('#4ecdc4'),
|
|
568
|
+
uAmplitude: 0.2,
|
|
569
|
+
uFrequency: 2.0,
|
|
570
|
+
uNoiseScale: 1.0,
|
|
571
|
+
},
|
|
572
|
+
vertexShader,
|
|
573
|
+
fragmentShader
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
extend({ GradientMaterial });
|
|
577
|
+
|
|
578
|
+
// TypeScript declaration
|
|
579
|
+
declare global {
|
|
580
|
+
namespace JSX {
|
|
581
|
+
interface IntrinsicElements {
|
|
582
|
+
gradientMaterial: any;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Component using custom shader
|
|
588
|
+
export function ShaderPlane() {
|
|
589
|
+
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
|
590
|
+
|
|
591
|
+
useFrame(({ clock }) => {
|
|
592
|
+
if (materialRef.current) {
|
|
593
|
+
materialRef.current.uniforms.uTime.value = clock.getElapsedTime();
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
return (
|
|
598
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>
|
|
599
|
+
<planeGeometry args={[10, 10, 64, 64]} />
|
|
600
|
+
<gradientMaterial
|
|
601
|
+
ref={materialRef}
|
|
602
|
+
uColorA="#ff6b6b"
|
|
603
|
+
uColorB="#4ecdc4"
|
|
604
|
+
uAmplitude={0.3}
|
|
605
|
+
uFrequency={3.0}
|
|
606
|
+
/>
|
|
607
|
+
</mesh>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Post-processing shader
|
|
612
|
+
const GlitchShader = {
|
|
613
|
+
uniforms: {
|
|
614
|
+
tDiffuse: { value: null },
|
|
615
|
+
uTime: { value: 0 },
|
|
616
|
+
uIntensity: { value: 0.5 },
|
|
617
|
+
},
|
|
618
|
+
vertexShader: `
|
|
619
|
+
varying vec2 vUv;
|
|
620
|
+
void main() {
|
|
621
|
+
vUv = uv;
|
|
622
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
623
|
+
}
|
|
624
|
+
`,
|
|
625
|
+
fragmentShader: `
|
|
626
|
+
uniform sampler2D tDiffuse;
|
|
627
|
+
uniform float uTime;
|
|
628
|
+
uniform float uIntensity;
|
|
629
|
+
varying vec2 vUv;
|
|
630
|
+
|
|
631
|
+
float random(vec2 st) {
|
|
632
|
+
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
void main() {
|
|
636
|
+
vec2 uv = vUv;
|
|
637
|
+
|
|
638
|
+
// Random glitch offset
|
|
639
|
+
float glitch = step(0.99, random(vec2(uTime * 0.1, floor(uv.y * 20.0))));
|
|
640
|
+
uv.x += glitch * uIntensity * (random(vec2(uTime)) - 0.5);
|
|
641
|
+
|
|
642
|
+
// RGB split
|
|
643
|
+
float r = texture2D(tDiffuse, uv + vec2(uIntensity * 0.01, 0.0)).r;
|
|
644
|
+
float g = texture2D(tDiffuse, uv).g;
|
|
645
|
+
float b = texture2D(tDiffuse, uv - vec2(uIntensity * 0.01, 0.0)).b;
|
|
646
|
+
|
|
647
|
+
gl_FragColor = vec4(r, g, b, 1.0);
|
|
648
|
+
}
|
|
649
|
+
`,
|
|
650
|
+
};
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### 4. Physics Integration
|
|
654
|
+
|
|
655
|
+
```tsx
|
|
656
|
+
// components/PhysicsScene.tsx
|
|
22
657
|
import { Canvas } from '@react-three/fiber';
|
|
23
|
-
import {
|
|
658
|
+
import { Physics, RigidBody, CuboidCollider, BallCollider } from '@react-three/rapier';
|
|
659
|
+
import { useRef, useState } from 'react';
|
|
660
|
+
import * as THREE from 'three';
|
|
661
|
+
|
|
662
|
+
// Physics-enabled box
|
|
663
|
+
function PhysicsBox({ position }: { position: [number, number, number] }) {
|
|
664
|
+
const [color, setColor] = useState('#ff6b6b');
|
|
665
|
+
|
|
666
|
+
return (
|
|
667
|
+
<RigidBody
|
|
668
|
+
position={position}
|
|
669
|
+
restitution={0.7}
|
|
670
|
+
friction={0.5}
|
|
671
|
+
onCollisionEnter={() => setColor('#4ecdc4')}
|
|
672
|
+
onCollisionExit={() => setColor('#ff6b6b')}
|
|
673
|
+
>
|
|
674
|
+
<mesh castShadow>
|
|
675
|
+
<boxGeometry args={[1, 1, 1]} />
|
|
676
|
+
<meshStandardMaterial color={color} />
|
|
677
|
+
</mesh>
|
|
678
|
+
</RigidBody>
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Bouncing ball
|
|
683
|
+
function BouncingBall({ position }: { position: [number, number, number] }) {
|
|
684
|
+
const rigidBodyRef = useRef(null);
|
|
685
|
+
|
|
686
|
+
const handleClick = () => {
|
|
687
|
+
if (rigidBodyRef.current) {
|
|
688
|
+
// Apply upward impulse on click
|
|
689
|
+
rigidBodyRef.current.applyImpulse({ x: 0, y: 10, z: 0 }, true);
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
return (
|
|
694
|
+
<RigidBody
|
|
695
|
+
ref={rigidBodyRef}
|
|
696
|
+
position={position}
|
|
697
|
+
restitution={0.9}
|
|
698
|
+
friction={0.3}
|
|
699
|
+
colliders="ball"
|
|
700
|
+
>
|
|
701
|
+
<mesh castShadow onClick={handleClick}>
|
|
702
|
+
<sphereGeometry args={[0.5, 32, 32]} />
|
|
703
|
+
<meshStandardMaterial color="#feca57" metalness={0.3} roughness={0.2} />
|
|
704
|
+
</mesh>
|
|
705
|
+
</RigidBody>
|
|
706
|
+
);
|
|
707
|
+
}
|
|
24
708
|
|
|
25
|
-
|
|
709
|
+
// Ground plane
|
|
710
|
+
function Ground() {
|
|
26
711
|
return (
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<boxGeometry />
|
|
32
|
-
<meshStandardMaterial color="orange" />
|
|
712
|
+
<RigidBody type="fixed" friction={1}>
|
|
713
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]} receiveShadow>
|
|
714
|
+
<planeGeometry args={[50, 50]} />
|
|
715
|
+
<meshStandardMaterial color="#1a1a2e" />
|
|
33
716
|
</mesh>
|
|
717
|
+
</RigidBody>
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Walls for containment
|
|
722
|
+
function Walls() {
|
|
723
|
+
return (
|
|
724
|
+
<>
|
|
725
|
+
{/* Back wall */}
|
|
726
|
+
<RigidBody type="fixed">
|
|
727
|
+
<CuboidCollider args={[25, 10, 0.5]} position={[0, 5, -25]} />
|
|
728
|
+
</RigidBody>
|
|
729
|
+
{/* Front wall */}
|
|
730
|
+
<RigidBody type="fixed">
|
|
731
|
+
<CuboidCollider args={[25, 10, 0.5]} position={[0, 5, 25]} />
|
|
732
|
+
</RigidBody>
|
|
733
|
+
{/* Left wall */}
|
|
734
|
+
<RigidBody type="fixed">
|
|
735
|
+
<CuboidCollider args={[0.5, 10, 25]} position={[-25, 5, 0]} />
|
|
736
|
+
</RigidBody>
|
|
737
|
+
{/* Right wall */}
|
|
738
|
+
<RigidBody type="fixed">
|
|
739
|
+
<CuboidCollider args={[0.5, 10, 25]} position={[25, 5, 0]} />
|
|
740
|
+
</RigidBody>
|
|
741
|
+
</>
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Spawner for dynamic objects
|
|
746
|
+
function ObjectSpawner() {
|
|
747
|
+
const [objects, setObjects] = useState<Array<{ id: number; type: 'box' | 'ball'; position: [number, number, number] }>>([]);
|
|
748
|
+
|
|
749
|
+
const spawnObject = () => {
|
|
750
|
+
const id = Date.now();
|
|
751
|
+
const type = Math.random() > 0.5 ? 'box' : 'ball';
|
|
752
|
+
const position: [number, number, number] = [
|
|
753
|
+
(Math.random() - 0.5) * 10,
|
|
754
|
+
10,
|
|
755
|
+
(Math.random() - 0.5) * 10,
|
|
756
|
+
];
|
|
757
|
+
setObjects((prev) => [...prev, { id, type, position }]);
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
return (
|
|
761
|
+
<>
|
|
762
|
+
{objects.map(({ id, type, position }) =>
|
|
763
|
+
type === 'box' ? (
|
|
764
|
+
<PhysicsBox key={id} position={position} />
|
|
765
|
+
) : (
|
|
766
|
+
<BouncingBall key={id} position={position} />
|
|
767
|
+
)
|
|
768
|
+
)}
|
|
769
|
+
|
|
770
|
+
{/* Spawn trigger - invisible clickable plane */}
|
|
771
|
+
<mesh position={[0, 15, 0]} onClick={spawnObject}>
|
|
772
|
+
<planeGeometry args={[20, 20]} />
|
|
773
|
+
<meshBasicMaterial visible={false} />
|
|
774
|
+
</mesh>
|
|
775
|
+
</>
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Main physics scene
|
|
780
|
+
export function PhysicsScene() {
|
|
781
|
+
return (
|
|
782
|
+
<Canvas shadows camera={{ position: [15, 15, 15], fov: 50 }}>
|
|
783
|
+
<ambientLight intensity={0.5} />
|
|
784
|
+
<directionalLight
|
|
785
|
+
position={[10, 20, 10]}
|
|
786
|
+
intensity={1.5}
|
|
787
|
+
castShadow
|
|
788
|
+
shadow-mapSize={[2048, 2048]}
|
|
789
|
+
/>
|
|
790
|
+
|
|
791
|
+
<Physics gravity={[0, -9.81, 0]} debug={false}>
|
|
792
|
+
<Ground />
|
|
793
|
+
<Walls />
|
|
794
|
+
<ObjectSpawner />
|
|
795
|
+
|
|
796
|
+
{/* Initial objects */}
|
|
797
|
+
<PhysicsBox position={[0, 5, 0]} />
|
|
798
|
+
<PhysicsBox position={[0.5, 7, 0.5]} />
|
|
799
|
+
<BouncingBall position={[-2, 5, 0]} />
|
|
800
|
+
<BouncingBall position={[2, 8, 2]} />
|
|
801
|
+
</Physics>
|
|
802
|
+
|
|
34
803
|
<OrbitControls />
|
|
35
804
|
</Canvas>
|
|
36
805
|
);
|
|
37
806
|
}
|
|
38
807
|
```
|
|
39
808
|
|
|
40
|
-
|
|
809
|
+
### 5. Animation System
|
|
810
|
+
|
|
41
811
|
```tsx
|
|
42
|
-
|
|
812
|
+
// lib/three/AnimationManager.ts
|
|
813
|
+
import * as THREE from 'three';
|
|
814
|
+
import gsap from 'gsap';
|
|
815
|
+
|
|
816
|
+
interface AnimationConfig {
|
|
817
|
+
duration?: number;
|
|
818
|
+
ease?: string;
|
|
819
|
+
delay?: number;
|
|
820
|
+
repeat?: number;
|
|
821
|
+
yoyo?: boolean;
|
|
822
|
+
onComplete?: () => void;
|
|
823
|
+
}
|
|
43
824
|
|
|
44
|
-
|
|
45
|
-
|
|
825
|
+
export class AnimationManager {
|
|
826
|
+
private mixer: THREE.AnimationMixer | null = null;
|
|
827
|
+
private actions: Map<string, THREE.AnimationAction> = new Map();
|
|
828
|
+
private timelines: Map<string, gsap.core.Timeline> = new Map();
|
|
46
829
|
|
|
47
|
-
|
|
48
|
-
|
|
830
|
+
// GLTF animation setup
|
|
831
|
+
public setupMixer(model: THREE.Object3D, animations: THREE.AnimationClip[]): void {
|
|
832
|
+
this.mixer = new THREE.AnimationMixer(model);
|
|
833
|
+
|
|
834
|
+
animations.forEach((clip) => {
|
|
835
|
+
const action = this.mixer!.clipAction(clip);
|
|
836
|
+
this.actions.set(clip.name, action);
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
public playAnimation(name: string, options: { loop?: boolean; crossFade?: number } = {}): void {
|
|
841
|
+
const action = this.actions.get(name);
|
|
842
|
+
if (!action) return;
|
|
843
|
+
|
|
844
|
+
const { loop = true, crossFade = 0.3 } = options;
|
|
845
|
+
|
|
846
|
+
// Stop other actions with crossfade
|
|
847
|
+
this.actions.forEach((a, n) => {
|
|
848
|
+
if (n !== name && a.isRunning()) {
|
|
849
|
+
a.fadeOut(crossFade);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
action.reset();
|
|
854
|
+
action.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce, Infinity);
|
|
855
|
+
action.clampWhenFinished = !loop;
|
|
856
|
+
action.fadeIn(crossFade);
|
|
857
|
+
action.play();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
public update(delta: number): void {
|
|
861
|
+
this.mixer?.update(delta);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// GSAP-based object animation
|
|
865
|
+
public animateTo(
|
|
866
|
+
object: THREE.Object3D,
|
|
867
|
+
properties: Partial<{
|
|
868
|
+
position: { x?: number; y?: number; z?: number };
|
|
869
|
+
rotation: { x?: number; y?: number; z?: number };
|
|
870
|
+
scale: { x?: number; y?: number; z?: number };
|
|
871
|
+
}>,
|
|
872
|
+
config: AnimationConfig = {}
|
|
873
|
+
): gsap.core.Tween {
|
|
874
|
+
const { duration = 1, ease = 'power2.out', delay = 0, onComplete } = config;
|
|
875
|
+
|
|
876
|
+
const targets: any[] = [];
|
|
877
|
+
const props: any = {};
|
|
878
|
+
|
|
879
|
+
if (properties.position) {
|
|
880
|
+
targets.push(object.position);
|
|
881
|
+
Object.assign(props, properties.position);
|
|
882
|
+
}
|
|
883
|
+
if (properties.rotation) {
|
|
884
|
+
targets.push(object.rotation);
|
|
885
|
+
Object.assign(props, properties.rotation);
|
|
886
|
+
}
|
|
887
|
+
if (properties.scale) {
|
|
888
|
+
targets.push(object.scale);
|
|
889
|
+
Object.assign(props, properties.scale);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return gsap.to(targets, {
|
|
893
|
+
...props,
|
|
894
|
+
duration,
|
|
895
|
+
ease,
|
|
896
|
+
delay,
|
|
897
|
+
onComplete,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Timeline-based complex animations
|
|
902
|
+
public createTimeline(id: string): gsap.core.Timeline {
|
|
903
|
+
const timeline = gsap.timeline({ paused: true });
|
|
904
|
+
this.timelines.set(id, timeline);
|
|
905
|
+
return timeline;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
public playTimeline(id: string): void {
|
|
909
|
+
this.timelines.get(id)?.play();
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
public dispose(): void {
|
|
913
|
+
this.actions.clear();
|
|
914
|
+
this.timelines.forEach((tl) => tl.kill());
|
|
915
|
+
this.timelines.clear();
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// React hook for animations
|
|
920
|
+
function useAnimation(meshRef: React.RefObject<THREE.Mesh>) {
|
|
921
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
922
|
+
|
|
923
|
+
const animateIn = useCallback(() => {
|
|
924
|
+
if (!meshRef.current || isAnimating) return;
|
|
925
|
+
|
|
926
|
+
setIsAnimating(true);
|
|
927
|
+
|
|
928
|
+
gsap.timeline()
|
|
929
|
+
.fromTo(
|
|
930
|
+
meshRef.current.scale,
|
|
931
|
+
{ x: 0, y: 0, z: 0 },
|
|
932
|
+
{ x: 1, y: 1, z: 1, duration: 0.5, ease: 'back.out(1.7)' }
|
|
933
|
+
)
|
|
934
|
+
.fromTo(
|
|
935
|
+
meshRef.current.rotation,
|
|
936
|
+
{ y: -Math.PI },
|
|
937
|
+
{ y: 0, duration: 0.5, ease: 'power2.out' },
|
|
938
|
+
'<'
|
|
939
|
+
)
|
|
940
|
+
.call(() => setIsAnimating(false));
|
|
941
|
+
}, [isAnimating]);
|
|
942
|
+
|
|
943
|
+
const animateOut = useCallback(() => {
|
|
944
|
+
if (!meshRef.current || isAnimating) return;
|
|
945
|
+
|
|
946
|
+
setIsAnimating(true);
|
|
947
|
+
|
|
948
|
+
gsap.to(meshRef.current.scale, {
|
|
949
|
+
x: 0,
|
|
950
|
+
y: 0,
|
|
951
|
+
z: 0,
|
|
952
|
+
duration: 0.3,
|
|
953
|
+
ease: 'back.in(1.7)',
|
|
954
|
+
onComplete: () => setIsAnimating(false),
|
|
955
|
+
});
|
|
956
|
+
}, [isAnimating]);
|
|
957
|
+
|
|
958
|
+
return { animateIn, animateOut, isAnimating };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Animated component example
|
|
962
|
+
function AnimatedObject() {
|
|
963
|
+
const meshRef = useRef<THREE.Mesh>(null);
|
|
964
|
+
const { animateIn, animateOut, isAnimating } = useAnimation(meshRef);
|
|
965
|
+
|
|
966
|
+
useEffect(() => {
|
|
967
|
+
animateIn();
|
|
968
|
+
}, []);
|
|
969
|
+
|
|
970
|
+
return (
|
|
971
|
+
<mesh
|
|
972
|
+
ref={meshRef}
|
|
973
|
+
onClick={animateOut}
|
|
974
|
+
scale={[0, 0, 0]}
|
|
975
|
+
>
|
|
976
|
+
<boxGeometry />
|
|
977
|
+
<meshStandardMaterial color="#ff6b6b" />
|
|
978
|
+
</mesh>
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
### 6. Performance Optimization
|
|
984
|
+
|
|
985
|
+
```tsx
|
|
986
|
+
// lib/three/PerformanceOptimizer.ts
|
|
987
|
+
import * as THREE from 'three';
|
|
988
|
+
import { useThree, useFrame } from '@react-three/fiber';
|
|
989
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
990
|
+
|
|
991
|
+
// Level of Detail (LOD) component
|
|
992
|
+
function LODMesh({ position }: { position: [number, number, number] }) {
|
|
993
|
+
const lodRef = useRef<THREE.LOD>(null);
|
|
994
|
+
const { camera } = useThree();
|
|
995
|
+
|
|
996
|
+
const geometries = useMemo(() => ({
|
|
997
|
+
high: new THREE.IcosahedronGeometry(1, 4), // 320 triangles
|
|
998
|
+
medium: new THREE.IcosahedronGeometry(1, 2), // 80 triangles
|
|
999
|
+
low: new THREE.IcosahedronGeometry(1, 1), // 20 triangles
|
|
1000
|
+
}), []);
|
|
1001
|
+
|
|
1002
|
+
const material = useMemo(
|
|
1003
|
+
() => new THREE.MeshStandardMaterial({ color: '#4ecdc4' }),
|
|
1004
|
+
[]
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
useEffect(() => {
|
|
1008
|
+
if (!lodRef.current) return;
|
|
1009
|
+
|
|
1010
|
+
const meshHigh = new THREE.Mesh(geometries.high, material);
|
|
1011
|
+
const meshMedium = new THREE.Mesh(geometries.medium, material);
|
|
1012
|
+
const meshLow = new THREE.Mesh(geometries.low, material);
|
|
1013
|
+
|
|
1014
|
+
lodRef.current.addLevel(meshHigh, 0);
|
|
1015
|
+
lodRef.current.addLevel(meshMedium, 10);
|
|
1016
|
+
lodRef.current.addLevel(meshLow, 20);
|
|
1017
|
+
|
|
1018
|
+
return () => {
|
|
1019
|
+
geometries.high.dispose();
|
|
1020
|
+
geometries.medium.dispose();
|
|
1021
|
+
geometries.low.dispose();
|
|
1022
|
+
material.dispose();
|
|
1023
|
+
};
|
|
1024
|
+
}, [geometries, material]);
|
|
1025
|
+
|
|
1026
|
+
useFrame(() => {
|
|
1027
|
+
lodRef.current?.update(camera);
|
|
49
1028
|
});
|
|
50
1029
|
|
|
51
|
-
return <
|
|
1030
|
+
return <lod ref={lodRef} position={position} />;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Instanced mesh for many identical objects
|
|
1034
|
+
function InstancedBoxes({ count = 1000 }: { count?: number }) {
|
|
1035
|
+
const meshRef = useRef<THREE.InstancedMesh>(null);
|
|
1036
|
+
const tempObject = useMemo(() => new THREE.Object3D(), []);
|
|
1037
|
+
const tempColor = useMemo(() => new THREE.Color(), []);
|
|
1038
|
+
|
|
1039
|
+
useEffect(() => {
|
|
1040
|
+
if (!meshRef.current) return;
|
|
1041
|
+
|
|
1042
|
+
// Set up instance matrices and colors
|
|
1043
|
+
for (let i = 0; i < count; i++) {
|
|
1044
|
+
tempObject.position.set(
|
|
1045
|
+
(Math.random() - 0.5) * 50,
|
|
1046
|
+
(Math.random() - 0.5) * 50,
|
|
1047
|
+
(Math.random() - 0.5) * 50
|
|
1048
|
+
);
|
|
1049
|
+
tempObject.rotation.set(
|
|
1050
|
+
Math.random() * Math.PI,
|
|
1051
|
+
Math.random() * Math.PI,
|
|
1052
|
+
Math.random() * Math.PI
|
|
1053
|
+
);
|
|
1054
|
+
tempObject.scale.setScalar(0.5 + Math.random() * 0.5);
|
|
1055
|
+
tempObject.updateMatrix();
|
|
1056
|
+
|
|
1057
|
+
meshRef.current.setMatrixAt(i, tempObject.matrix);
|
|
1058
|
+
meshRef.current.setColorAt(
|
|
1059
|
+
i,
|
|
1060
|
+
tempColor.setHSL(Math.random(), 0.7, 0.5)
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
meshRef.current.instanceMatrix.needsUpdate = true;
|
|
1065
|
+
if (meshRef.current.instanceColor) {
|
|
1066
|
+
meshRef.current.instanceColor.needsUpdate = true;
|
|
1067
|
+
}
|
|
1068
|
+
}, [count, tempObject, tempColor]);
|
|
1069
|
+
|
|
1070
|
+
// Animate instances
|
|
1071
|
+
useFrame(({ clock }) => {
|
|
1072
|
+
if (!meshRef.current) return;
|
|
1073
|
+
|
|
1074
|
+
const time = clock.getElapsedTime();
|
|
1075
|
+
|
|
1076
|
+
for (let i = 0; i < count; i++) {
|
|
1077
|
+
meshRef.current.getMatrixAt(i, tempObject.matrix);
|
|
1078
|
+
tempObject.matrix.decompose(
|
|
1079
|
+
tempObject.position,
|
|
1080
|
+
tempObject.quaternion,
|
|
1081
|
+
tempObject.scale
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
tempObject.rotation.x = time * 0.5 + i * 0.01;
|
|
1085
|
+
tempObject.rotation.y = time * 0.3 + i * 0.01;
|
|
1086
|
+
tempObject.updateMatrix();
|
|
1087
|
+
|
|
1088
|
+
meshRef.current.setMatrixAt(i, tempObject.matrix);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
meshRef.current.instanceMatrix.needsUpdate = true;
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
return (
|
|
1095
|
+
<instancedMesh ref={meshRef} args={[undefined, undefined, count]} castShadow>
|
|
1096
|
+
<boxGeometry args={[1, 1, 1]} />
|
|
1097
|
+
<meshStandardMaterial />
|
|
1098
|
+
</instancedMesh>
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Frustum culling helper
|
|
1103
|
+
function useFrustumCulling(meshRef: React.RefObject<THREE.Mesh>) {
|
|
1104
|
+
const { camera } = useThree();
|
|
1105
|
+
const frustum = useMemo(() => new THREE.Frustum(), []);
|
|
1106
|
+
const projScreenMatrix = useMemo(() => new THREE.Matrix4(), []);
|
|
1107
|
+
|
|
1108
|
+
useFrame(() => {
|
|
1109
|
+
if (!meshRef.current) return;
|
|
1110
|
+
|
|
1111
|
+
projScreenMatrix.multiplyMatrices(
|
|
1112
|
+
camera.projectionMatrix,
|
|
1113
|
+
camera.matrixWorldInverse
|
|
1114
|
+
);
|
|
1115
|
+
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
1116
|
+
|
|
1117
|
+
// Check if mesh is in frustum
|
|
1118
|
+
const isVisible = frustum.intersectsObject(meshRef.current);
|
|
1119
|
+
meshRef.current.visible = isVisible;
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Texture optimization
|
|
1124
|
+
function useOptimizedTexture(url: string) {
|
|
1125
|
+
const texture = useMemo(() => {
|
|
1126
|
+
const loader = new THREE.TextureLoader();
|
|
1127
|
+
const tex = loader.load(url);
|
|
1128
|
+
|
|
1129
|
+
// Optimize texture settings
|
|
1130
|
+
tex.minFilter = THREE.LinearMipmapLinearFilter;
|
|
1131
|
+
tex.magFilter = THREE.LinearFilter;
|
|
1132
|
+
tex.anisotropy = 4; // Balance quality and performance
|
|
1133
|
+
tex.generateMipmaps = true;
|
|
1134
|
+
|
|
1135
|
+
return tex;
|
|
1136
|
+
}, [url]);
|
|
1137
|
+
|
|
1138
|
+
useEffect(() => {
|
|
1139
|
+
return () => texture.dispose();
|
|
1140
|
+
}, [texture]);
|
|
1141
|
+
|
|
1142
|
+
return texture;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// GPU particle system
|
|
1146
|
+
function ParticleSystem({ count = 10000 }: { count?: number }) {
|
|
1147
|
+
const particlesRef = useRef<THREE.Points>(null);
|
|
1148
|
+
|
|
1149
|
+
const [positions, velocities] = useMemo(() => {
|
|
1150
|
+
const positions = new Float32Array(count * 3);
|
|
1151
|
+
const velocities = new Float32Array(count * 3);
|
|
1152
|
+
|
|
1153
|
+
for (let i = 0; i < count; i++) {
|
|
1154
|
+
const i3 = i * 3;
|
|
1155
|
+
positions[i3] = (Math.random() - 0.5) * 20;
|
|
1156
|
+
positions[i3 + 1] = Math.random() * 20;
|
|
1157
|
+
positions[i3 + 2] = (Math.random() - 0.5) * 20;
|
|
1158
|
+
|
|
1159
|
+
velocities[i3] = (Math.random() - 0.5) * 0.02;
|
|
1160
|
+
velocities[i3 + 1] = -Math.random() * 0.05;
|
|
1161
|
+
velocities[i3 + 2] = (Math.random() - 0.5) * 0.02;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
return [positions, velocities];
|
|
1165
|
+
}, [count]);
|
|
1166
|
+
|
|
1167
|
+
useFrame(() => {
|
|
1168
|
+
if (!particlesRef.current) return;
|
|
1169
|
+
|
|
1170
|
+
const positions = particlesRef.current.geometry.attributes.position.array as Float32Array;
|
|
1171
|
+
|
|
1172
|
+
for (let i = 0; i < count; i++) {
|
|
1173
|
+
const i3 = i * 3;
|
|
1174
|
+
|
|
1175
|
+
positions[i3] += velocities[i3];
|
|
1176
|
+
positions[i3 + 1] += velocities[i3 + 1];
|
|
1177
|
+
positions[i3 + 2] += velocities[i3 + 2];
|
|
1178
|
+
|
|
1179
|
+
// Reset particle when it falls below ground
|
|
1180
|
+
if (positions[i3 + 1] < 0) {
|
|
1181
|
+
positions[i3 + 1] = 20;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
particlesRef.current.geometry.attributes.position.needsUpdate = true;
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
return (
|
|
1189
|
+
<points ref={particlesRef}>
|
|
1190
|
+
<bufferGeometry>
|
|
1191
|
+
<bufferAttribute
|
|
1192
|
+
attach="attributes-position"
|
|
1193
|
+
count={count}
|
|
1194
|
+
array={positions}
|
|
1195
|
+
itemSize={3}
|
|
1196
|
+
/>
|
|
1197
|
+
</bufferGeometry>
|
|
1198
|
+
<pointsMaterial
|
|
1199
|
+
size={0.05}
|
|
1200
|
+
color="#ffffff"
|
|
1201
|
+
transparent
|
|
1202
|
+
opacity={0.8}
|
|
1203
|
+
sizeAttenuation
|
|
1204
|
+
/>
|
|
1205
|
+
</points>
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
## Use Cases
|
|
1211
|
+
|
|
1212
|
+
### Interactive Product Viewer
|
|
1213
|
+
|
|
1214
|
+
```tsx
|
|
1215
|
+
// components/ProductViewer.tsx
|
|
1216
|
+
import { Canvas } from '@react-three/fiber';
|
|
1217
|
+
import { useGLTF, OrbitControls, Environment, Html, useProgress } from '@react-three/drei';
|
|
1218
|
+
import { Suspense, useState } from 'react';
|
|
1219
|
+
|
|
1220
|
+
function Loader() {
|
|
1221
|
+
const { progress } = useProgress();
|
|
1222
|
+
return (
|
|
1223
|
+
<Html center>
|
|
1224
|
+
<div className="text-white text-xl">
|
|
1225
|
+
Loading... {progress.toFixed(0)}%
|
|
1226
|
+
</div>
|
|
1227
|
+
</Html>
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
interface ProductProps {
|
|
1232
|
+
modelUrl: string;
|
|
1233
|
+
hotspots: Array<{
|
|
1234
|
+
id: string;
|
|
1235
|
+
position: [number, number, number];
|
|
1236
|
+
label: string;
|
|
1237
|
+
description: string;
|
|
1238
|
+
}>;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function Product({ modelUrl, hotspots }: ProductProps) {
|
|
1242
|
+
const { scene } = useGLTF(modelUrl);
|
|
1243
|
+
const [activeHotspot, setActiveHotspot] = useState<string | null>(null);
|
|
1244
|
+
|
|
1245
|
+
return (
|
|
1246
|
+
<group>
|
|
1247
|
+
<primitive object={scene} scale={1} />
|
|
1248
|
+
|
|
1249
|
+
{hotspots.map((hotspot) => (
|
|
1250
|
+
<group key={hotspot.id} position={hotspot.position}>
|
|
1251
|
+
<mesh onClick={() => setActiveHotspot(hotspot.id)}>
|
|
1252
|
+
<sphereGeometry args={[0.1, 16, 16]} />
|
|
1253
|
+
<meshBasicMaterial
|
|
1254
|
+
color={activeHotspot === hotspot.id ? '#ff6b6b' : '#4ecdc4'}
|
|
1255
|
+
/>
|
|
1256
|
+
</mesh>
|
|
1257
|
+
|
|
1258
|
+
{activeHotspot === hotspot.id && (
|
|
1259
|
+
<Html distanceFactor={5}>
|
|
1260
|
+
<div className="bg-white p-4 rounded-lg shadow-lg min-w-[200px]">
|
|
1261
|
+
<h3 className="font-bold">{hotspot.label}</h3>
|
|
1262
|
+
<p className="text-sm text-gray-600">{hotspot.description}</p>
|
|
1263
|
+
</div>
|
|
1264
|
+
</Html>
|
|
1265
|
+
)}
|
|
1266
|
+
</group>
|
|
1267
|
+
))}
|
|
1268
|
+
</group>
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
export function ProductViewer({ modelUrl, hotspots }: ProductProps) {
|
|
1273
|
+
return (
|
|
1274
|
+
<div className="w-full h-screen">
|
|
1275
|
+
<Canvas camera={{ position: [0, 0, 5], fov: 50 }}>
|
|
1276
|
+
<Suspense fallback={<Loader />}>
|
|
1277
|
+
<Product modelUrl={modelUrl} hotspots={hotspots} />
|
|
1278
|
+
<Environment preset="studio" />
|
|
1279
|
+
</Suspense>
|
|
1280
|
+
|
|
1281
|
+
<OrbitControls
|
|
1282
|
+
enablePan={false}
|
|
1283
|
+
minDistance={2}
|
|
1284
|
+
maxDistance={10}
|
|
1285
|
+
autoRotate
|
|
1286
|
+
autoRotateSpeed={0.5}
|
|
1287
|
+
/>
|
|
1288
|
+
</Canvas>
|
|
1289
|
+
</div>
|
|
1290
|
+
);
|
|
52
1291
|
}
|
|
53
1292
|
```
|
|
54
1293
|
|
|
55
1294
|
## Best Practices
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
- Use
|
|
1295
|
+
|
|
1296
|
+
### Do's
|
|
1297
|
+
|
|
1298
|
+
- Use React Three Fiber for React applications
|
|
1299
|
+
- Dispose of geometries, materials, and textures when unmounting
|
|
1300
|
+
- Use instancing for many identical objects
|
|
1301
|
+
- Implement LOD for complex scenes
|
|
1302
|
+
- Use texture atlases to reduce draw calls
|
|
1303
|
+
- Enable shadow maps only when needed
|
|
1304
|
+
- Use object pooling for frequently created/destroyed objects
|
|
1305
|
+
- Optimize geometry with BufferGeometry
|
|
1306
|
+
- Use compressed textures (KTX2, Basis)
|
|
1307
|
+
- Profile performance with Chrome DevTools and Spector.js
|
|
1308
|
+
|
|
1309
|
+
### Don'ts
|
|
1310
|
+
|
|
1311
|
+
- Don't create new geometries/materials in render loops
|
|
1312
|
+
- Don't use too many lights (limit to 3-4 dynamic lights)
|
|
1313
|
+
- Don't skip frustum culling for large scenes
|
|
1314
|
+
- Don't forget to update instanceMatrix when animating instanced meshes
|
|
1315
|
+
- Don't use transparent materials unless necessary
|
|
1316
|
+
- Don't load uncompressed high-resolution textures
|
|
1317
|
+
- Don't forget to set power-of-two texture dimensions
|
|
1318
|
+
- Don't ignore memory leaks from undisposed resources
|
|
1319
|
+
- Don't use complex shaders without fallbacks
|
|
1320
|
+
- Don't skip mobile optimization and testing
|
|
1321
|
+
|
|
1322
|
+
## References
|
|
1323
|
+
|
|
1324
|
+
- [Three.js Documentation](https://threejs.org/docs/)
|
|
1325
|
+
- [React Three Fiber](https://docs.pmnd.rs/react-three-fiber/)
|
|
1326
|
+
- [Drei Helpers](https://github.com/pmndrs/drei)
|
|
1327
|
+
- [Three.js Fundamentals](https://threejs.org/manual/)
|
|
1328
|
+
- [WebGL Best Practices](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices)
|