create-threejs-game 1.0.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 +97 -0
- package/bin/cli.js +370 -0
- package/package.json +29 -0
- package/template/.claude/skills/threejs-animation/SKILL.md +552 -0
- package/template/.claude/skills/threejs-fundamentals/SKILL.md +488 -0
- package/template/.claude/skills/threejs-geometry/SKILL.md +548 -0
- package/template/.claude/skills/threejs-interaction/SKILL.md +660 -0
- package/template/.claude/skills/threejs-lighting/SKILL.md +481 -0
- package/template/.claude/skills/threejs-loaders/SKILL.md +623 -0
- package/template/.claude/skills/threejs-materials/SKILL.md +520 -0
- package/template/.claude/skills/threejs-postprocessing/SKILL.md +602 -0
- package/template/.claude/skills/threejs-shaders/SKILL.md +642 -0
- package/template/.claude/skills/threejs-textures/SKILL.md +628 -0
- package/template/.codex/skills/threejs-animation/SKILL.md +552 -0
- package/template/.codex/skills/threejs-fundamentals/SKILL.md +488 -0
- package/template/.codex/skills/threejs-geometry/SKILL.md +548 -0
- package/template/.codex/skills/threejs-interaction/SKILL.md +660 -0
- package/template/.codex/skills/threejs-lighting/SKILL.md +481 -0
- package/template/.codex/skills/threejs-loaders/SKILL.md +623 -0
- package/template/.codex/skills/threejs-materials/SKILL.md +520 -0
- package/template/.codex/skills/threejs-postprocessing/SKILL.md +602 -0
- package/template/.codex/skills/threejs-shaders/SKILL.md +642 -0
- package/template/.codex/skills/threejs-textures/SKILL.md +628 -0
- package/template/README.md +352 -0
- package/template/docs/.gitkeep +0 -0
- package/template/plans/.gitkeep +0 -0
- package/template/prompts/01-mockup-generation.md +44 -0
- package/template/prompts/02-prd-generation.md +119 -0
- package/template/prompts/03-tdd-generation.md +127 -0
- package/template/prompts/04-execution-plan.md +89 -0
- package/template/prompts/05-implementation.md +61 -0
- package/template/public/assets/.gitkeep +0 -0
- package/template/scripts/config.example.json +12 -0
- package/template/scripts/generate-assets-json.js +149 -0
- package/template/scripts/generate-mockup.js +197 -0
- package/template/scripts/generate-plan.js +295 -0
- package/template/scripts/generate-prd.js +297 -0
- package/template/scripts/generate-tdd.js +283 -0
- package/template/scripts/pipeline.js +229 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: threejs-interaction
|
|
3
|
+
description: Three.js interaction - raycasting, controls, mouse/touch input, object selection. Use when handling user input, implementing click detection, adding camera controls, or creating interactive 3D experiences.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Three.js Interaction
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
```javascript
|
|
11
|
+
import * as THREE from "three";
|
|
12
|
+
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
13
|
+
|
|
14
|
+
// Camera controls
|
|
15
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
16
|
+
controls.enableDamping = true;
|
|
17
|
+
|
|
18
|
+
// Raycasting for click detection
|
|
19
|
+
const raycaster = new THREE.Raycaster();
|
|
20
|
+
const mouse = new THREE.Vector2();
|
|
21
|
+
|
|
22
|
+
function onClick(event) {
|
|
23
|
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
24
|
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
25
|
+
|
|
26
|
+
raycaster.setFromCamera(mouse, camera);
|
|
27
|
+
const intersects = raycaster.intersectObjects(scene.children);
|
|
28
|
+
|
|
29
|
+
if (intersects.length > 0) {
|
|
30
|
+
console.log("Clicked:", intersects[0].object);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
window.addEventListener("click", onClick);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Raycaster
|
|
38
|
+
|
|
39
|
+
### Basic Raycasting
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
const raycaster = new THREE.Raycaster();
|
|
43
|
+
|
|
44
|
+
// From camera (mouse picking)
|
|
45
|
+
raycaster.setFromCamera(mousePosition, camera);
|
|
46
|
+
|
|
47
|
+
// From any origin and direction
|
|
48
|
+
raycaster.set(origin, direction); // origin: Vector3, direction: normalized Vector3
|
|
49
|
+
|
|
50
|
+
// Get intersections
|
|
51
|
+
const intersects = raycaster.intersectObjects(objects, recursive);
|
|
52
|
+
|
|
53
|
+
// intersects array contains:
|
|
54
|
+
// {
|
|
55
|
+
// distance: number, // Distance from ray origin
|
|
56
|
+
// point: Vector3, // Intersection point in world coords
|
|
57
|
+
// face: Face3, // Intersected face
|
|
58
|
+
// faceIndex: number, // Face index
|
|
59
|
+
// object: Object3D, // Intersected object
|
|
60
|
+
// uv: Vector2, // UV coordinates at intersection
|
|
61
|
+
// uv1: Vector2, // Second UV channel
|
|
62
|
+
// normal: Vector3, // Interpolated face normal
|
|
63
|
+
// instanceId: number // For InstancedMesh
|
|
64
|
+
// }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Mouse Position Conversion
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
const mouse = new THREE.Vector2();
|
|
71
|
+
|
|
72
|
+
function updateMouse(event) {
|
|
73
|
+
// For full window
|
|
74
|
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
75
|
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// For specific canvas element
|
|
79
|
+
function updateMouseCanvas(event, canvas) {
|
|
80
|
+
const rect = canvas.getBoundingClientRect();
|
|
81
|
+
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
82
|
+
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Touch Support
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
function onTouchStart(event) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
|
|
92
|
+
if (event.touches.length === 1) {
|
|
93
|
+
const touch = event.touches[0];
|
|
94
|
+
mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
|
|
95
|
+
mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
|
|
96
|
+
|
|
97
|
+
raycaster.setFromCamera(mouse, camera);
|
|
98
|
+
const intersects = raycaster.intersectObjects(clickableObjects);
|
|
99
|
+
|
|
100
|
+
if (intersects.length > 0) {
|
|
101
|
+
handleSelection(intersects[0]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
renderer.domElement.addEventListener("touchstart", onTouchStart);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Raycaster Options
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
const raycaster = new THREE.Raycaster();
|
|
113
|
+
|
|
114
|
+
// Near/far clipping (default: 0, Infinity)
|
|
115
|
+
raycaster.near = 0;
|
|
116
|
+
raycaster.far = 100;
|
|
117
|
+
|
|
118
|
+
// Line/Points precision
|
|
119
|
+
raycaster.params.Line.threshold = 0.1;
|
|
120
|
+
raycaster.params.Points.threshold = 0.1;
|
|
121
|
+
|
|
122
|
+
// Layers (only intersect objects on specific layers)
|
|
123
|
+
raycaster.layers.set(1);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Efficient Raycasting
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
// Only check specific objects
|
|
130
|
+
const clickables = [mesh1, mesh2, mesh3];
|
|
131
|
+
const intersects = raycaster.intersectObjects(clickables, false);
|
|
132
|
+
|
|
133
|
+
// Use layers for filtering
|
|
134
|
+
mesh1.layers.set(1); // Clickable layer
|
|
135
|
+
raycaster.layers.set(1);
|
|
136
|
+
|
|
137
|
+
// Throttle raycast for hover effects
|
|
138
|
+
let lastRaycast = 0;
|
|
139
|
+
function onMouseMove(event) {
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
if (now - lastRaycast < 50) return; // 20fps max
|
|
142
|
+
lastRaycast = now;
|
|
143
|
+
|
|
144
|
+
// Raycast here
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Camera Controls
|
|
149
|
+
|
|
150
|
+
### OrbitControls
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
154
|
+
|
|
155
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
156
|
+
|
|
157
|
+
// Damping (smooth movement)
|
|
158
|
+
controls.enableDamping = true;
|
|
159
|
+
controls.dampingFactor = 0.05;
|
|
160
|
+
|
|
161
|
+
// Rotation limits
|
|
162
|
+
controls.minPolarAngle = 0; // Top
|
|
163
|
+
controls.maxPolarAngle = Math.PI / 2; // Horizon
|
|
164
|
+
controls.minAzimuthAngle = -Math.PI / 4; // Left
|
|
165
|
+
controls.maxAzimuthAngle = Math.PI / 4; // Right
|
|
166
|
+
|
|
167
|
+
// Zoom limits
|
|
168
|
+
controls.minDistance = 2;
|
|
169
|
+
controls.maxDistance = 50;
|
|
170
|
+
|
|
171
|
+
// Enable/disable features
|
|
172
|
+
controls.enableRotate = true;
|
|
173
|
+
controls.enableZoom = true;
|
|
174
|
+
controls.enablePan = true;
|
|
175
|
+
|
|
176
|
+
// Auto-rotate
|
|
177
|
+
controls.autoRotate = true;
|
|
178
|
+
controls.autoRotateSpeed = 2.0;
|
|
179
|
+
|
|
180
|
+
// Target (orbit point)
|
|
181
|
+
controls.target.set(0, 1, 0);
|
|
182
|
+
|
|
183
|
+
// Update in animation loop
|
|
184
|
+
function animate() {
|
|
185
|
+
controls.update(); // Required for damping and auto-rotate
|
|
186
|
+
renderer.render(scene, camera);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### FlyControls
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
import { FlyControls } from "three/addons/controls/FlyControls.js";
|
|
194
|
+
|
|
195
|
+
const controls = new FlyControls(camera, renderer.domElement);
|
|
196
|
+
controls.movementSpeed = 10;
|
|
197
|
+
controls.rollSpeed = Math.PI / 24;
|
|
198
|
+
controls.dragToLook = true;
|
|
199
|
+
|
|
200
|
+
// Update with delta
|
|
201
|
+
function animate() {
|
|
202
|
+
controls.update(clock.getDelta());
|
|
203
|
+
renderer.render(scene, camera);
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### FirstPersonControls
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
import { FirstPersonControls } from "three/addons/controls/FirstPersonControls.js";
|
|
211
|
+
|
|
212
|
+
const controls = new FirstPersonControls(camera, renderer.domElement);
|
|
213
|
+
controls.movementSpeed = 10;
|
|
214
|
+
controls.lookSpeed = 0.1;
|
|
215
|
+
controls.lookVertical = true;
|
|
216
|
+
controls.constrainVertical = true;
|
|
217
|
+
controls.verticalMin = Math.PI / 4;
|
|
218
|
+
controls.verticalMax = (Math.PI * 3) / 4;
|
|
219
|
+
|
|
220
|
+
function animate() {
|
|
221
|
+
controls.update(clock.getDelta());
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### PointerLockControls
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
import { PointerLockControls } from "three/addons/controls/PointerLockControls.js";
|
|
229
|
+
|
|
230
|
+
const controls = new PointerLockControls(camera, document.body);
|
|
231
|
+
|
|
232
|
+
// Lock pointer on click
|
|
233
|
+
document.addEventListener("click", () => {
|
|
234
|
+
controls.lock();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
controls.addEventListener("lock", () => {
|
|
238
|
+
console.log("Pointer locked");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
controls.addEventListener("unlock", () => {
|
|
242
|
+
console.log("Pointer unlocked");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Movement
|
|
246
|
+
const velocity = new THREE.Vector3();
|
|
247
|
+
const direction = new THREE.Vector3();
|
|
248
|
+
const moveForward = false;
|
|
249
|
+
const moveBackward = false;
|
|
250
|
+
|
|
251
|
+
document.addEventListener("keydown", (event) => {
|
|
252
|
+
switch (event.code) {
|
|
253
|
+
case "KeyW":
|
|
254
|
+
moveForward = true;
|
|
255
|
+
break;
|
|
256
|
+
case "KeyS":
|
|
257
|
+
moveBackward = true;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
function animate() {
|
|
263
|
+
if (controls.isLocked) {
|
|
264
|
+
direction.z = Number(moveForward) - Number(moveBackward);
|
|
265
|
+
direction.normalize();
|
|
266
|
+
|
|
267
|
+
velocity.z -= direction.z * 0.1;
|
|
268
|
+
velocity.z *= 0.9; // Friction
|
|
269
|
+
|
|
270
|
+
controls.moveForward(-velocity.z);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### TrackballControls
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
import { TrackballControls } from "three/addons/controls/TrackballControls.js";
|
|
279
|
+
|
|
280
|
+
const controls = new TrackballControls(camera, renderer.domElement);
|
|
281
|
+
controls.rotateSpeed = 2.0;
|
|
282
|
+
controls.zoomSpeed = 1.2;
|
|
283
|
+
controls.panSpeed = 0.8;
|
|
284
|
+
controls.staticMoving = true;
|
|
285
|
+
|
|
286
|
+
function animate() {
|
|
287
|
+
controls.update();
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### MapControls
|
|
292
|
+
|
|
293
|
+
```javascript
|
|
294
|
+
import { MapControls } from "three/addons/controls/MapControls.js";
|
|
295
|
+
|
|
296
|
+
const controls = new MapControls(camera, renderer.domElement);
|
|
297
|
+
controls.enableDamping = true;
|
|
298
|
+
controls.dampingFactor = 0.05;
|
|
299
|
+
controls.screenSpacePanning = false;
|
|
300
|
+
controls.maxPolarAngle = Math.PI / 2;
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## TransformControls
|
|
304
|
+
|
|
305
|
+
Gizmo for moving/rotating/scaling objects.
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
import { TransformControls } from "three/addons/controls/TransformControls.js";
|
|
309
|
+
|
|
310
|
+
const transformControls = new TransformControls(camera, renderer.domElement);
|
|
311
|
+
scene.add(transformControls);
|
|
312
|
+
|
|
313
|
+
// Attach to object
|
|
314
|
+
transformControls.attach(selectedMesh);
|
|
315
|
+
|
|
316
|
+
// Switch modes
|
|
317
|
+
transformControls.setMode("translate"); // 'translate', 'rotate', 'scale'
|
|
318
|
+
|
|
319
|
+
// Change space
|
|
320
|
+
transformControls.setSpace("local"); // 'local', 'world'
|
|
321
|
+
|
|
322
|
+
// Size
|
|
323
|
+
transformControls.setSize(1);
|
|
324
|
+
|
|
325
|
+
// Events
|
|
326
|
+
transformControls.addEventListener("dragging-changed", (event) => {
|
|
327
|
+
// Disable orbit controls while dragging
|
|
328
|
+
orbitControls.enabled = !event.value;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
transformControls.addEventListener("change", () => {
|
|
332
|
+
renderer.render(scene, camera);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Keyboard shortcuts
|
|
336
|
+
window.addEventListener("keydown", (event) => {
|
|
337
|
+
switch (event.key) {
|
|
338
|
+
case "g":
|
|
339
|
+
transformControls.setMode("translate");
|
|
340
|
+
break;
|
|
341
|
+
case "r":
|
|
342
|
+
transformControls.setMode("rotate");
|
|
343
|
+
break;
|
|
344
|
+
case "s":
|
|
345
|
+
transformControls.setMode("scale");
|
|
346
|
+
break;
|
|
347
|
+
case "Escape":
|
|
348
|
+
transformControls.detach();
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## DragControls
|
|
355
|
+
|
|
356
|
+
Drag objects directly.
|
|
357
|
+
|
|
358
|
+
```javascript
|
|
359
|
+
import { DragControls } from "three/addons/controls/DragControls.js";
|
|
360
|
+
|
|
361
|
+
const draggableObjects = [mesh1, mesh2, mesh3];
|
|
362
|
+
const dragControls = new DragControls(
|
|
363
|
+
draggableObjects,
|
|
364
|
+
camera,
|
|
365
|
+
renderer.domElement,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
dragControls.addEventListener("dragstart", (event) => {
|
|
369
|
+
orbitControls.enabled = false;
|
|
370
|
+
event.object.material.emissive.set(0xaaaaaa);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
dragControls.addEventListener("drag", (event) => {
|
|
374
|
+
// Constrain to ground plane
|
|
375
|
+
event.object.position.y = 0;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
dragControls.addEventListener("dragend", (event) => {
|
|
379
|
+
orbitControls.enabled = true;
|
|
380
|
+
event.object.material.emissive.set(0x000000);
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Selection System
|
|
385
|
+
|
|
386
|
+
### Click to Select
|
|
387
|
+
|
|
388
|
+
```javascript
|
|
389
|
+
const raycaster = new THREE.Raycaster();
|
|
390
|
+
const mouse = new THREE.Vector2();
|
|
391
|
+
let selectedObject = null;
|
|
392
|
+
|
|
393
|
+
function onMouseDown(event) {
|
|
394
|
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
395
|
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
396
|
+
|
|
397
|
+
raycaster.setFromCamera(mouse, camera);
|
|
398
|
+
const intersects = raycaster.intersectObjects(selectableObjects);
|
|
399
|
+
|
|
400
|
+
// Deselect previous
|
|
401
|
+
if (selectedObject) {
|
|
402
|
+
selectedObject.material.emissive.set(0x000000);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Select new
|
|
406
|
+
if (intersects.length > 0) {
|
|
407
|
+
selectedObject = intersects[0].object;
|
|
408
|
+
selectedObject.material.emissive.set(0x444444);
|
|
409
|
+
} else {
|
|
410
|
+
selectedObject = null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Box Selection
|
|
416
|
+
|
|
417
|
+
```javascript
|
|
418
|
+
import { SelectionBox } from "three/addons/interactive/SelectionBox.js";
|
|
419
|
+
import { SelectionHelper } from "three/addons/interactive/SelectionHelper.js";
|
|
420
|
+
|
|
421
|
+
const selectionBox = new SelectionBox(camera, scene);
|
|
422
|
+
const selectionHelper = new SelectionHelper(renderer, "selectBox"); // CSS class
|
|
423
|
+
|
|
424
|
+
document.addEventListener("pointerdown", (event) => {
|
|
425
|
+
selectionBox.startPoint.set(
|
|
426
|
+
(event.clientX / window.innerWidth) * 2 - 1,
|
|
427
|
+
-(event.clientY / window.innerHeight) * 2 + 1,
|
|
428
|
+
0.5,
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
document.addEventListener("pointermove", (event) => {
|
|
433
|
+
if (selectionHelper.isDown) {
|
|
434
|
+
selectionBox.endPoint.set(
|
|
435
|
+
(event.clientX / window.innerWidth) * 2 - 1,
|
|
436
|
+
-(event.clientY / window.innerHeight) * 2 + 1,
|
|
437
|
+
0.5,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
document.addEventListener("pointerup", (event) => {
|
|
443
|
+
selectionBox.endPoint.set(
|
|
444
|
+
(event.clientX / window.innerWidth) * 2 - 1,
|
|
445
|
+
-(event.clientY / window.innerHeight) * 2 + 1,
|
|
446
|
+
0.5,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const selected = selectionBox.select();
|
|
450
|
+
console.log("Selected objects:", selected);
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Hover Effects
|
|
455
|
+
|
|
456
|
+
```javascript
|
|
457
|
+
const raycaster = new THREE.Raycaster();
|
|
458
|
+
const mouse = new THREE.Vector2();
|
|
459
|
+
let hoveredObject = null;
|
|
460
|
+
|
|
461
|
+
function onMouseMove(event) {
|
|
462
|
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
463
|
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
464
|
+
|
|
465
|
+
raycaster.setFromCamera(mouse, camera);
|
|
466
|
+
const intersects = raycaster.intersectObjects(hoverableObjects);
|
|
467
|
+
|
|
468
|
+
// Reset previous hover
|
|
469
|
+
if (hoveredObject) {
|
|
470
|
+
hoveredObject.material.color.set(hoveredObject.userData.originalColor);
|
|
471
|
+
document.body.style.cursor = "default";
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Apply new hover
|
|
475
|
+
if (intersects.length > 0) {
|
|
476
|
+
hoveredObject = intersects[0].object;
|
|
477
|
+
if (!hoveredObject.userData.originalColor) {
|
|
478
|
+
hoveredObject.userData.originalColor =
|
|
479
|
+
hoveredObject.material.color.getHex();
|
|
480
|
+
}
|
|
481
|
+
hoveredObject.material.color.set(0xff6600);
|
|
482
|
+
document.body.style.cursor = "pointer";
|
|
483
|
+
} else {
|
|
484
|
+
hoveredObject = null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
window.addEventListener("mousemove", onMouseMove);
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Keyboard Input
|
|
492
|
+
|
|
493
|
+
```javascript
|
|
494
|
+
const keys = {};
|
|
495
|
+
|
|
496
|
+
document.addEventListener("keydown", (event) => {
|
|
497
|
+
keys[event.code] = true;
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
document.addEventListener("keyup", (event) => {
|
|
501
|
+
keys[event.code] = false;
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
function update() {
|
|
505
|
+
const speed = 0.1;
|
|
506
|
+
|
|
507
|
+
if (keys["KeyW"]) player.position.z -= speed;
|
|
508
|
+
if (keys["KeyS"]) player.position.z += speed;
|
|
509
|
+
if (keys["KeyA"]) player.position.x -= speed;
|
|
510
|
+
if (keys["KeyD"]) player.position.x += speed;
|
|
511
|
+
if (keys["Space"]) player.position.y += speed;
|
|
512
|
+
if (keys["ShiftLeft"]) player.position.y -= speed;
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## World-Screen Coordinate Conversion
|
|
517
|
+
|
|
518
|
+
### World to Screen
|
|
519
|
+
|
|
520
|
+
```javascript
|
|
521
|
+
function worldToScreen(position, camera) {
|
|
522
|
+
const vector = position.clone();
|
|
523
|
+
vector.project(camera);
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
x: ((vector.x + 1) / 2) * window.innerWidth,
|
|
527
|
+
y: (-(vector.y - 1) / 2) * window.innerHeight,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Position HTML element over 3D object
|
|
532
|
+
const screenPos = worldToScreen(mesh.position, camera);
|
|
533
|
+
element.style.left = screenPos.x + "px";
|
|
534
|
+
element.style.top = screenPos.y + "px";
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Screen to World
|
|
538
|
+
|
|
539
|
+
```javascript
|
|
540
|
+
function screenToWorld(screenX, screenY, camera, targetZ = 0) {
|
|
541
|
+
const vector = new THREE.Vector3(
|
|
542
|
+
(screenX / window.innerWidth) * 2 - 1,
|
|
543
|
+
-(screenY / window.innerHeight) * 2 + 1,
|
|
544
|
+
0.5,
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
vector.unproject(camera);
|
|
548
|
+
|
|
549
|
+
const dir = vector.sub(camera.position).normalize();
|
|
550
|
+
const distance = (targetZ - camera.position.z) / dir.z;
|
|
551
|
+
|
|
552
|
+
return camera.position.clone().add(dir.multiplyScalar(distance));
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### Ray-Plane Intersection
|
|
557
|
+
|
|
558
|
+
```javascript
|
|
559
|
+
function getRayPlaneIntersection(mouse, camera, plane) {
|
|
560
|
+
const raycaster = new THREE.Raycaster();
|
|
561
|
+
raycaster.setFromCamera(mouse, camera);
|
|
562
|
+
|
|
563
|
+
const intersection = new THREE.Vector3();
|
|
564
|
+
raycaster.ray.intersectPlane(plane, intersection);
|
|
565
|
+
|
|
566
|
+
return intersection;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Ground plane
|
|
570
|
+
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
571
|
+
const worldPos = getRayPlaneIntersection(mouse, camera, groundPlane);
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## Event Handling Best Practices
|
|
575
|
+
|
|
576
|
+
```javascript
|
|
577
|
+
class InteractionManager {
|
|
578
|
+
constructor(camera, renderer, scene) {
|
|
579
|
+
this.camera = camera;
|
|
580
|
+
this.renderer = renderer;
|
|
581
|
+
this.scene = scene;
|
|
582
|
+
this.raycaster = new THREE.Raycaster();
|
|
583
|
+
this.mouse = new THREE.Vector2();
|
|
584
|
+
this.clickables = [];
|
|
585
|
+
|
|
586
|
+
this.bindEvents();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
bindEvents() {
|
|
590
|
+
const canvas = this.renderer.domElement;
|
|
591
|
+
|
|
592
|
+
canvas.addEventListener("click", (e) => this.onClick(e));
|
|
593
|
+
canvas.addEventListener("mousemove", (e) => this.onMouseMove(e));
|
|
594
|
+
canvas.addEventListener("touchstart", (e) => this.onTouchStart(e));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
updateMouse(event) {
|
|
598
|
+
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
599
|
+
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
600
|
+
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
getIntersects() {
|
|
604
|
+
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
605
|
+
return this.raycaster.intersectObjects(this.clickables, true);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
onClick(event) {
|
|
609
|
+
this.updateMouse(event);
|
|
610
|
+
const intersects = this.getIntersects();
|
|
611
|
+
|
|
612
|
+
if (intersects.length > 0) {
|
|
613
|
+
const object = intersects[0].object;
|
|
614
|
+
if (object.userData.onClick) {
|
|
615
|
+
object.userData.onClick(intersects[0]);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
addClickable(object, callback) {
|
|
621
|
+
this.clickables.push(object);
|
|
622
|
+
object.userData.onClick = callback;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
dispose() {
|
|
626
|
+
// Remove event listeners
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Usage
|
|
631
|
+
const interaction = new InteractionManager(camera, renderer, scene);
|
|
632
|
+
interaction.addClickable(mesh, (intersect) => {
|
|
633
|
+
console.log("Clicked at:", intersect.point);
|
|
634
|
+
});
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## Performance Tips
|
|
638
|
+
|
|
639
|
+
1. **Limit raycasts**: Throttle mousemove handlers
|
|
640
|
+
2. **Use layers**: Filter raycast targets
|
|
641
|
+
3. **Simple collision meshes**: Use invisible simpler geometry for raycasting
|
|
642
|
+
4. **Disable controls when not needed**: `controls.enabled = false`
|
|
643
|
+
5. **Batch updates**: Group interaction checks
|
|
644
|
+
|
|
645
|
+
```javascript
|
|
646
|
+
// Use simpler geometry for raycasting
|
|
647
|
+
const complexMesh = loadedModel;
|
|
648
|
+
const collisionMesh = new THREE.Mesh(
|
|
649
|
+
new THREE.BoxGeometry(1, 1, 1),
|
|
650
|
+
new THREE.MeshBasicMaterial({ visible: false }),
|
|
651
|
+
);
|
|
652
|
+
collisionMesh.userData.target = complexMesh;
|
|
653
|
+
clickables.push(collisionMesh);
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
## See Also
|
|
657
|
+
|
|
658
|
+
- `threejs-fundamentals` - Camera and scene setup
|
|
659
|
+
- `threejs-animation` - Animating interactions
|
|
660
|
+
- `threejs-shaders` - Visual feedback effects
|