Skip to main content 홈 크리에이터 afovea game-dev-skills threejs-interaction
threejs-interaction Three.js user interaction — camera controls (OrbitControls, FirstPersonControls, PointerLockControls, FlyControls, MapControls), raycasting for click and hover detection, TransformControls, DragControls, SelectionBox, keyboard input, and touch handling. Use when implementing player input, object picking, camera movement, or any user-driven 3D interaction. Adapted from CloudAI-X/threejs-skills (MIT).
설치로 이동 Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/afovea/game-dev-skills --skill threejs-interaction명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... 이 저장소의 다른 Skills Art Director persona for visual style, pillars, art bible, cross-discipline visual consistency, art critique, and art QA. Use when a task needs visual direction, style enforcement, or cohesion across concept, environment, character, VFX, and lighting.
Audio Director persona for sound design, music direction, mix, audio implementation patterns, and audio pipeline. Use when a task needs reasoning about audio intent, audio-mechanic integration, mix balance, or the audio toolchain.
Post-pipeline cleanup and verification pass for game-development work. Regenerate / re-bake any derived content, rebuild the project, verify the editor / engine opens cleanly, run automated test suites, take a perf snapshot against budgets, and confirm save / network / cert / pipeline surfaces are untouched (or correctly versioned) against the pre-run baseline.
name threejs-interaction description Three.js user interaction — camera controls (OrbitControls, FirstPersonControls, PointerLockControls, FlyControls, MapControls), raycasting for click and hover detection, TransformControls, DragControls, SelectionBox, keyboard input, and touch handling. Use when implementing player input, object picking, camera movement, or any user-driven 3D interaction. Adapted from CloudAI-X/threejs-skills (MIT). license MIT compatibility Portable reference skill for agents that support markdown skills or prompt files. Works best alongside project Three.js source files. Requires three/addons controls from the same Three.js version. disable-model-invocation true metadata {"owner":"game-delivery","version":"2.0.0","language":"en-GB","category":"web-rendering","upstream_references":["https://github.com/CloudAI-X/threejs-skills (MIT — see NOTICE.md)"],"tags":["three-js","interaction","raycasting","camera-controls","orbit-controls","pointer-lock","input","game-input"],"intents":["camera-controls","raycasting","object-picking","transform-gizmos","keyboard-input","touch-input"],"output_types":["code-example","api-reference","input-scheme"]}
Three.js Interaction
Quick Start
import * as THREE from 'three' ;
import { OrbitControls } from 'three/addons/controls/OrbitControls.js' ;
const controls = new OrbitControls (camera, renderer.domElement );
controls.enableDamping = true ;
controls.dampingFactor = 0.05 ;
function animate ( ) {
requestAnimationFrame (animate);
controls.update ();
renderer.render (scene, camera);
}
Camera Controls
OrbitControls — Rotate Around a Target
Best for: editors, product viewers, model inspection.
import { OrbitControls } from 'three/addons/controls/OrbitControls.js' ;
const controls = new OrbitControls (camera, renderer.domElement );
controls.enableDamping = true ;
controls.dampingFactor = 0.05 ;
controls.minDistance = 1 ;
controls.maxDistance = 50 ;
controls. = ;
controls. = . * ;
controls. = - . / ;
controls. = . / ;
controls. = ;
controls. = ;
controls. = ;
controls. = ;
controls. = ;
controls. = ;
controls. . ( , , );
controls. ();
( ) {
controls. ();
renderer. (scene, camera);
}
minPolarAngle
0
maxPolarAngle
Math
PI
0.9
minAzimuthAngle
Math
PI
4
maxAzimuthAngle
Math
PI
4
enablePan
false
enableZoom
false
enableRotate
false
panSpeed
1.0
zoomSpeed
1.0
rotateSpeed
1.0
target
set
0
1
0
update
function
animate
update
render
PointerLockControls — First-Person Game Controls Best for: FPS games, immersive first-person experiences.
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js' ;
const controls = new PointerLockControls (camera, document .body );
document .addEventListener ('click' , () => controls.lock ());
controls.addEventListener ('lock' , () => console .log ('Pointer locked' ));
controls.addEventListener ('unlock' , () => console .log ('Pointer unlocked' ));
const keys = {};
document .addEventListener ('keydown' , e => keys[e.code ] = true );
document .addEventListener ('keyup' , e => keys[e.code ] = false );
const velocity = new THREE .Vector3 ();
const direction = new THREE .Vector3 ();
const moveSpeed = 10 ;
function animate ( ) {
const delta = clock.getDelta ();
direction.z = Number (keys['KeyW' ]) - Number (keys['KeyS' ]);
direction.x = Number (keys['KeyD' ]) - Number (keys['KeyA' ]);
direction.normalize ();
if (keys['KeyW' ] || keys['KeyS' ]) velocity.z -= direction.z * moveSpeed * delta;
if (keys['KeyA' ] || keys['KeyD' ]) velocity.x -= direction.x * moveSpeed * delta;
velocity.x -= velocity.x * 10 * delta;
velocity.z -= velocity.z * 10 * delta;
controls.moveRight (-velocity.x * delta);
controls.moveForward (-velocity.z * delta);
requestAnimationFrame (animate);
renderer.render (scene, camera);
}
FlyControls — Free-Flying Camera Best for: fly-through, level design views.
import { FlyControls } from 'three/addons/controls/FlyControls.js' ;
const controls = new FlyControls (camera, renderer.domElement );
controls.movementSpeed = 10 ;
controls.rollSpeed = Math .PI / 12 ;
controls.autoForward = false ;
controls.dragToLook = true ;
function animate ( ) {
controls.update (clock.getDelta ());
renderer.render (scene, camera);
}
MapControls — Top-Down Map View Best for: strategy games, overhead editors.
import { MapControls } from 'three/addons/controls/MapControls.js' ;
const controls = new MapControls (camera, renderer.domElement );
controls.enableDamping = true ;
controls.screenSpacePanning = false ;
controls.minDistance = 5 ;
controls.maxDistance = 100 ;
controls.maxPolarAngle = Math .PI / 2 ;
FirstPersonControls — Mouse-Look Camera Best for: walkthroughs, cut-scenes.
import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js' ;
const controls = new FirstPersonControls (camera, renderer.domElement );
controls.movementSpeed = 5 ;
controls.lookSpeed = 0.1 ;
controls.lookVertical = true ;
controls.constrainVertical = true ;
controls.verticalMin = 1.0 ;
controls.verticalMax = 2.0 ;
function animate ( ) {
controls.update (clock.getDelta ());
renderer.render (scene, camera);
}
Raycasting Convert screen coordinates to 3D ray and test for intersections.
Basic Click Detection const raycaster = new THREE .Raycaster ();
const mouse = new THREE .Vector2 ();
function onClick (event ) {
mouse.x = (event.clientX / window .innerWidth ) * 2 - 1 ;
mouse.y = -(event.clientY / window .innerHeight ) * 2 + 1 ;
raycaster.setFromCamera (mouse, camera);
const intersects = raycaster.intersectObjects (scene.children , true );
if (intersects.length > 0 ) {
const hit = intersects[0 ];
console .log ('Hit:' , hit.object .name );
console .log ('Point:' , hit.point );
console .log ('Distance:' , hit.distance );
console .log ('Face:' , hit.face );
console .log ('UV:' , hit.uv );
}
}
renderer.domElement .addEventListener ('click' , onClick);
Hover / Highlight let hovered = null ;
function onMouseMove (event ) {
mouse.x = (event.clientX / window .innerWidth ) * 2 - 1 ;
mouse.y = -(event.clientY / window .innerHeight ) * 2 + 1 ;
raycaster.setFromCamera (mouse, camera);
const intersects = raycaster.intersectObjects (interactiveObjects);
if (intersects.length > 0 ) {
const obj = intersects[0 ].object ;
if (hovered !== obj) {
if (hovered) hovered.material .emissive .set (0x000000 );
hovered = obj;
hovered.material .emissive .set (0x444444 );
}
} else {
if (hovered) hovered.material .emissive .set (0x000000 );
hovered = null ;
}
}
let lastMove = 0 ;
renderer.domElement .addEventListener ('mousemove' , event => {
if (Date .now () - lastMove > 16 ) {
onMouseMove (event);
lastMove = Date .now ();
}
});
Limit Raycast to Specific Layers
raycaster.layers .set (1 );
interactiveObject.layers .enable (1 );
nonInteractiveObject.layers .disable (1 );
Invisible Collision Meshes
const collider = new THREE .Mesh (
new THREE .BoxGeometry (1 , 1 , 1 ),
new THREE .MeshBasicMaterial ({ visible : false }),
);
collider.userData .isCollider = true ;
scene.add (collider);
const intersects = raycaster.intersectObjects ([collider]);
TransformControls — Move/Rotate/Scale Gizmo import { TransformControls } from 'three/addons/controls/TransformControls.js' ;
const transformControls = new TransformControls (camera, renderer.domElement );
scene.add (transformControls);
transformControls.attach (selectedObject);
transformControls.setMode ('translate' );
transformControls.setTranslationSnap (0.5 );
transformControls.setRotationSnap (THREE .MathUtils .degToRad (15 ));
transformControls.setScaleSnap (0.1 );
transformControls.setSpace ('world' );
transformControls.detach ();
transformControls.addEventListener ('dragging-changed' , event => {
orbitControls.enabled = !event.value ;
});
transformControls.addEventListener ('objectChange' , () => {
console .log ('Position:' , selectedObject.position );
});
DragControls — Direct Object Dragging import { DragControls } from 'three/addons/controls/DragControls.js' ;
const dragControls = new DragControls (draggableObjects, camera, renderer.domElement );
dragControls.addEventListener ('dragstart' , event => {
orbitControls.enabled = false ;
event.object .material .opacity = 0.5 ;
});
dragControls.addEventListener ('drag' , event => {
event.object .position .x = Math .round (event.object .position .x );
event.object .position .z = Math .round (event.object .position .z );
});
dragControls.addEventListener ('dragend' , event => {
orbitControls.enabled = true ;
event.object .material .opacity = 1.0 ;
});
SelectionBox — Marquee Selection import { SelectionBox } from 'three/addons/interactive/SelectionBox.js' ;
import { SelectionHelper } from 'three/addons/interactive/SelectionHelper.js' ;
const selectionBox = new SelectionBox (camera, scene);
const selectionHelper = new SelectionHelper (renderer, 'selectBox' );
renderer.domElement .addEventListener ('pointerdown' , event => {
selectionBox.startPoint .set (
(event.clientX / window .innerWidth ) * 2 - 1 ,
-(event.clientY / window .innerHeight ) * 2 + 1 ,
0.5 ,
);
});
renderer.domElement .addEventListener ('pointermove' , event => {
if (selectionHelper.isDown ) {
selectionBox.endPoint .set (
(event.clientX / window .innerWidth ) * 2 - 1 ,
-(event.clientY / window .innerHeight ) * 2 + 1 ,
0.5 ,
);
const selected = selectionBox.select ();
selected.forEach (obj => obj.material .emissive .set (0x333333 ));
}
});
renderer.domElement .addEventListener ('pointerup' , () => {
const allSelected = selectionBox.collection ;
console .log ('Selected:' , allSelected);
});
Keyboard Input
const keysDown = new Set ();
document .addEventListener ('keydown' , e => keysDown.add (e.code ));
document .addEventListener ('keyup' , e => keysDown.delete (e.code ));
function handleMovement (delta ) {
const speed = 5 ;
if (keysDown.has ('KeyW' )) player.position .z -= speed * delta;
if (keysDown.has ('KeyS' )) player.position .z += speed * delta;
if (keysDown.has ('KeyA' )) player.position .x -= speed * delta;
if (keysDown.has ('KeyD' )) player.position .x += speed * delta;
if (keysDown.has ('Space' )) jump ();
if (keysDown.has ('ShiftLeft' )) sprint ();
}
document .addEventListener ('keydown' , e => {
if (e.code === 'KeyE' ) interact ();
if (e.code === 'KeyF' ) toggleFlashlight ();
if (e.code === 'Tab' ) { e.preventDefault (); openInventory (); }
});
Touch Input function getTouchNDC (touch ) {
return new THREE .Vector2 (
(touch.clientX / window .innerWidth ) * 2 - 1 ,
-(touch.clientY / window .innerHeight ) * 2 + 1 ,
);
}
renderer.domElement .addEventListener ('touchstart' , event => {
event.preventDefault ();
const touch = event.touches [0 ];
const ndc = getTouchNDC (touch);
raycaster.setFromCamera (ndc, camera);
const hits = raycaster.intersectObjects (scene.children , true );
if (hits.length > 0 ) handleTap (hits[0 ]);
}, { passive : false });
let lastPinchDist = 0 ;
renderer.domElement .addEventListener ('touchmove' , event => {
if (event.touches .length === 2 ) {
const dx = event.touches [0 ].clientX - event.touches [1 ].clientX ;
const dy = event.touches [0 ].clientY - event.touches [1 ].clientY ;
const dist = Math .sqrt (dx * dx + dy * dy);
if (lastPinchDist > 0 ) {
const delta = (lastPinchDist - dist) * 0.01 ;
camera.position .z = THREE .MathUtils .clamp (camera.position .z + delta, 1 , 50 );
}
lastPinchDist = dist;
}
}, { passive : true });
renderer.domElement .addEventListener ('touchend' , () => { lastPinchDist = 0 ; });
Screen-to-World and World-to-Screen
function getWorldRayFromScreen (x, y ) {
const ndc = new THREE .Vector2 (
(x / window .innerWidth ) * 2 - 1 ,
-(y / window .innerHeight ) * 2 + 1 ,
);
raycaster.setFromCamera (ndc, camera);
return { origin : raycaster.ray .origin , direction : raycaster.ray .direction };
}
function worldToScreen (worldPos ) {
const pos = worldPos.clone ().project (camera);
return {
x : (pos.x * 0.5 + 0.5 ) * window .innerWidth ,
y : (pos.y * -0.5 + 0.5 ) * window .innerHeight ,
};
}
Performance Tips
Throttle mousemove raycasts — test at ~60fps max, not every mouse event
Use layer filtering — raycaster.layers.set(n) to skip non-interactive objects
Use invisible simple colliders — simpler geometry for raycasting
Dispose controls — call controls.dispose() on teardown
Avoid raycasting against everything — maintain an explicit interactiveObjects array
See Also
threejs-fundamentals — coordinate system, camera setup
threejs-animation — animating objects driven by input
three-js-best-practices — raycasting performance optimisation