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.
Files changed (39) hide show
  1. package/README.md +97 -0
  2. package/bin/cli.js +370 -0
  3. package/package.json +29 -0
  4. package/template/.claude/skills/threejs-animation/SKILL.md +552 -0
  5. package/template/.claude/skills/threejs-fundamentals/SKILL.md +488 -0
  6. package/template/.claude/skills/threejs-geometry/SKILL.md +548 -0
  7. package/template/.claude/skills/threejs-interaction/SKILL.md +660 -0
  8. package/template/.claude/skills/threejs-lighting/SKILL.md +481 -0
  9. package/template/.claude/skills/threejs-loaders/SKILL.md +623 -0
  10. package/template/.claude/skills/threejs-materials/SKILL.md +520 -0
  11. package/template/.claude/skills/threejs-postprocessing/SKILL.md +602 -0
  12. package/template/.claude/skills/threejs-shaders/SKILL.md +642 -0
  13. package/template/.claude/skills/threejs-textures/SKILL.md +628 -0
  14. package/template/.codex/skills/threejs-animation/SKILL.md +552 -0
  15. package/template/.codex/skills/threejs-fundamentals/SKILL.md +488 -0
  16. package/template/.codex/skills/threejs-geometry/SKILL.md +548 -0
  17. package/template/.codex/skills/threejs-interaction/SKILL.md +660 -0
  18. package/template/.codex/skills/threejs-lighting/SKILL.md +481 -0
  19. package/template/.codex/skills/threejs-loaders/SKILL.md +623 -0
  20. package/template/.codex/skills/threejs-materials/SKILL.md +520 -0
  21. package/template/.codex/skills/threejs-postprocessing/SKILL.md +602 -0
  22. package/template/.codex/skills/threejs-shaders/SKILL.md +642 -0
  23. package/template/.codex/skills/threejs-textures/SKILL.md +628 -0
  24. package/template/README.md +352 -0
  25. package/template/docs/.gitkeep +0 -0
  26. package/template/plans/.gitkeep +0 -0
  27. package/template/prompts/01-mockup-generation.md +44 -0
  28. package/template/prompts/02-prd-generation.md +119 -0
  29. package/template/prompts/03-tdd-generation.md +127 -0
  30. package/template/prompts/04-execution-plan.md +89 -0
  31. package/template/prompts/05-implementation.md +61 -0
  32. package/template/public/assets/.gitkeep +0 -0
  33. package/template/scripts/config.example.json +12 -0
  34. package/template/scripts/generate-assets-json.js +149 -0
  35. package/template/scripts/generate-mockup.js +197 -0
  36. package/template/scripts/generate-plan.js +295 -0
  37. package/template/scripts/generate-prd.js +297 -0
  38. package/template/scripts/generate-tdd.js +283 -0
  39. 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