Three.js 交互 #
交互让 3D 场景变得可操作,用户可以通过鼠标、键盘、触摸等方式与场景中的物体进行交互。
交互概述 #
text
┌─────────────────────────────────────────────────────────────┐
│ Three.js 交互方式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 鼠标交互 │ │ 键盘交互 │ │ 触摸交互 │ │
│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │
│ │ 点击、悬停 │ │ 按键检测 │ │ 手势识别 │ │
│ │ 拖拽、滚轮 │ │ 快捷键 │ │ 多点触控 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 核心技术:Raycaster(射线检测) │
│ │
└─────────────────────────────────────────────────────────────┘
射线检测(Raycaster) #
射线检测是 Three.js 交互的核心技术,用于检测鼠标/触摸点与 3D 物体的交叉。
基本用法 #
javascript
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
function checkIntersection() {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
const intersected = intersects[0];
console.log('点击的物体:', intersected.object);
console.log('交点位置:', intersected.point);
console.log('距离:', intersected.distance);
}
}
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('click', checkIntersection);
Raycaster 属性 #
| 属性 | 类型 | 描述 |
|---|---|---|
| ray | Ray | 射线对象 |
| camera | Camera | 相机对象 |
| near | Number | 近距离 |
| far | Number | 远距离 |
Raycaster 方法 #
javascript
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(objects);
const intersects = raycaster.intersectObjects(objects, true);
const intersects = raycaster.intersectObject(object);
const intersects = raycaster.intersectObjects([mesh1, mesh2]);
raycaster.set(origin, direction);
const intersects = raycaster.intersectObjects(objects);
Intersect 对象 #
javascript
const intersect = intersects[0];
intersect.distance;
intersect.point;
intersect.face;
intersect.faceIndex;
intersect.object;
intersect.uv;
鼠标交互 #
点击选择 #
javascript
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedObject = null;
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);
if (intersects.length > 0) {
if (selectedObject) {
selectedObject.material.emissive.setHex(0x000000);
}
selectedObject = intersects[0].object;
selectedObject.material.emissive.setHex(0x555555);
}
}
window.addEventListener('click', onClick);
悬停效果 #
javascript
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hoveredObject = 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(scene.children);
if (hoveredObject) {
hoveredObject.material.emissive.setHex(0x000000);
hoveredObject = null;
}
if (intersects.length > 0) {
hoveredObject = intersects[0].object;
hoveredObject.material.emissive.setHex(0x333333);
document.body.style.cursor = 'pointer';
} else {
document.body.style.cursor = 'default';
}
}
window.addEventListener('mousemove', onMouseMove);
拖拽物体 #
javascript
import { DragControls } from 'three/addons/controls/DragControls.js';
const objects = [];
for (let i = 0; i < 5; i++) {
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: Math.random() * 0xffffff })
);
mesh.position.set(Math.random() * 4 - 2, 0.5, Math.random() * 4 - 2);
scene.add(mesh);
objects.push(mesh);
}
const dragControls = new DragControls(objects, camera, renderer.domElement);
dragControls.addEventListener('dragstart', (event) => {
event.object.material.emissive.setHex(0x333333);
});
dragControls.addEventListener('drag', (event) => {
event.object.position.y = 0.5;
});
dragControls.addEventListener('dragend', (event) => {
event.object.material.emissive.setHex(0x000000);
});
自定义拖拽 #
javascript
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const intersection = new THREE.Vector3();
let selectedObject = null;
let isDragging = false;
function onMouseDown(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(objects);
if (intersects.length > 0) {
isDragging = true;
selectedObject = intersects[0].object;
controls.enabled = false;
}
}
function onMouseMove(event) {
if (!isDragging || !selectedObject) return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
raycaster.ray.intersectPlane(plane, intersection);
selectedObject.position.x = intersection.x;
selectedObject.position.z = intersection.z;
}
function onMouseUp() {
isDragging = false;
selectedObject = null;
controls.enabled = true;
}
window.addEventListener('mousedown', onMouseDown);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
鼠标滚轮 #
javascript
function onWheel(event) {
event.preventDefault();
camera.position.z += event.deltaY * 0.01;
}
window.addEventListener('wheel', onWheel, { passive: false });
键盘交互 #
基本按键检测 #
javascript
const keys = {};
window.addEventListener('keydown', (event) => {
keys[event.code] = true;
});
window.addEventListener('keyup', (event) => {
keys[event.code] = false;
});
function animate() {
requestAnimationFrame(animate);
const speed = 0.1;
if (keys['KeyW'] || keys['ArrowUp']) {
mesh.position.z -= speed;
}
if (keys['KeyS'] || keys['ArrowDown']) {
mesh.position.z += speed;
}
if (keys['KeyA'] || keys['ArrowLeft']) {
mesh.position.x -= speed;
}
if (keys['KeyD'] || keys['ArrowRight']) {
mesh.position.x += speed;
}
if (keys['Space']) {
mesh.position.y += speed;
}
if (keys['ShiftLeft']) {
mesh.position.y -= speed;
}
renderer.render(scene, camera);
}
animate();
快捷键 #
javascript
const shortcuts = {
'KeyR': () => mesh.rotation.set(0, 0, 0),
'KeyG': () => mesh.position.set(0, 0, 0),
'KeyS': () => mesh.scale.set(1, 1, 1),
'Delete': () => scene.remove(mesh),
'Escape': () => deselectAll()
};
window.addEventListener('keydown', (event) => {
if (shortcuts[event.code]) {
shortcuts[event.code]();
}
});
组合键 #
javascript
const keys = {};
window.addEventListener('keydown', (event) => {
keys[event.code] = true;
if (keys['ControlLeft'] && keys['KeyC']) {
console.log('复制');
}
if (keys['ControlLeft'] && keys['KeyV']) {
console.log('粘贴');
}
if (keys['ShiftLeft'] && keys['KeyA']) {
console.log('全选');
}
});
window.addEventListener('keyup', (event) => {
keys[event.code] = false;
});
触摸交互 #
触摸事件 #
javascript
function onTouchStart(event) {
event.preventDefault();
const touch = event.touches[0];
mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(objects);
if (intersects.length > 0) {
console.log('触摸到物体');
}
}
function onTouchMove(event) {
event.preventDefault();
const touch = event.touches[0];
mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
}
function onTouchEnd(event) {
console.log('触摸结束');
}
renderer.domElement.addEventListener('touchstart', onTouchStart, { passive: false });
renderer.domElement.addEventListener('touchmove', onTouchMove, { passive: false });
renderer.domElement.addEventListener('touchend', onTouchEnd);
双指缩放 #
javascript
let initialDistance = 0;
let initialZoom = 1;
function onTouchStart(event) {
if (event.touches.length === 2) {
initialDistance = getDistance(event.touches[0], event.touches[1]);
initialZoom = camera.position.z;
}
}
function onTouchMove(event) {
if (event.touches.length === 2) {
const currentDistance = getDistance(event.touches[0], event.touches[1]);
const scale = initialDistance / currentDistance;
camera.position.z = initialZoom * scale;
}
}
function getDistance(touch1, touch2) {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
renderer.domElement.addEventListener('touchstart', onTouchStart);
renderer.domElement.addEventListener('touchmove', onTouchMove);
双指旋转 #
javascript
let initialAngle = 0;
function onTouchStart(event) {
if (event.touches.length === 2) {
initialAngle = getAngle(event.touches[0], event.touches[1]);
}
}
function onTouchMove(event) {
if (event.touches.length === 2) {
const currentAngle = getAngle(event.touches[0], event.touches[1]);
const deltaAngle = currentAngle - initialAngle;
camera.rotation.y += deltaAngle * 0.01;
initialAngle = currentAngle;
}
}
function getAngle(touch1, touch2) {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.atan2(dy, dx);
}
交互控制器 #
OrbitControls 交互 #
javascript
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 2;
controls.maxDistance = 20;
controls.minPolarAngle = 0;
controls.maxPolarAngle = Math.PI / 2;
controls.enablePan = true;
controls.panSpeed = 1;
controls.enableZoom = true;
controls.zoomSpeed = 1;
controls.enableRotate = true;
controls.rotateSpeed = 1;
controls.addEventListener('change', () => {
console.log('相机改变');
});
controls.addEventListener('start', () => {
console.log('开始交互');
});
controls.addEventListener('end', () => {
console.log('结束交互');
});
TransformControls #
javascript
import { TransformControls } from 'three/addons/controls/TransformControls.js';
const transformControls = new TransformControls(camera, renderer.domElement);
scene.add(transformControls);
transformControls.attach(mesh);
transformControls.addEventListener('dragging-changed', (event) => {
controls.enabled = !event.value;
});
window.addEventListener('keydown', (event) => {
switch (event.code) {
case 'KeyT':
transformControls.setMode('translate');
break;
case 'KeyR':
transformControls.setMode('rotate');
break;
case 'KeyS':
transformControls.setMode('scale');
break;
}
});
高级交互 #
点击高亮 #
javascript
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const objects = [];
let selectedObjects = [];
const highlightMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.5
});
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(objects);
if (intersects.length > 0) {
const object = intersects[0].object;
if (event.shiftKey) {
const index = selectedObjects.indexOf(object);
if (index > -1) {
selectedObjects.splice(index, 1);
object.material = object.userData.originalMaterial;
} else {
object.userData.originalMaterial = object.material;
object.material = highlightMaterial;
selectedObjects.push(object);
}
} else {
selectedObjects.forEach(obj => {
obj.material = obj.userData.originalMaterial;
});
selectedObjects = [];
object.userData.originalMaterial = object.material;
object.material = highlightMaterial;
selectedObjects.push(object);
}
}
}
window.addEventListener('click', onClick);
信息提示 #
javascript
const tooltip = document.createElement('div');
tooltip.style.position = 'absolute';
tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
tooltip.style.color = 'white';
tooltip.style.padding = '5px 10px';
tooltip.style.borderRadius = '4px';
tooltip.style.pointerEvents = 'none';
tooltip.style.display = 'none';
document.body.appendChild(tooltip);
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(objects);
if (intersects.length > 0) {
const object = intersects[0].object;
tooltip.textContent = object.userData.name || '未命名';
tooltip.style.left = event.clientX + 10 + 'px';
tooltip.style.top = event.clientY + 10 + 'px';
tooltip.style.display = 'block';
} else {
tooltip.style.display = 'none';
}
}
window.addEventListener('mousemove', onMouseMove);
完整示例 #
javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(5, 5, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
const objects = [];
const colors = [0xff6b6b, 0x6bffb8, 0x6bb8ff, 0xffb86b, 0xb86bff];
for (let i = 0; i < 5; i++) {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: colors[i] });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set((i - 2) * 2, 0.5, 0);
mesh.userData.name = `立方体 ${i + 1}`;
mesh.userData.originalColor = colors[i];
scene.add(mesh);
objects.push(mesh);
}
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hoveredObject = null;
let selectedObject = 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(objects);
if (hoveredObject && hoveredObject !== selectedObject) {
hoveredObject.material.emissive.setHex(0x000000);
}
if (intersects.length > 0) {
hoveredObject = intersects[0].object;
if (hoveredObject !== selectedObject) {
hoveredObject.material.emissive.setHex(0x333333);
}
document.body.style.cursor = 'pointer';
} else {
hoveredObject = null;
document.body.style.cursor = 'default';
}
}
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(objects);
if (selectedObject) {
selectedObject.material.emissive.setHex(0x000000);
}
if (intersects.length > 0) {
selectedObject = intersects[0].object;
selectedObject.material.emissive.setHex(0x555555);
console.log('选中:', selectedObject.userData.name);
} else {
selectedObject = null;
}
}
const keys = {};
function onKeyDown(event) {
keys[event.code] = true;
if (event.code === 'Delete' && selectedObject) {
scene.remove(selectedObject);
const index = objects.indexOf(selectedObject);
if (index > -1) objects.splice(index, 1);
selectedObject = null;
}
}
function onKeyUp(event) {
keys[event.code] = false;
}
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('click', onClick);
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
function animate() {
requestAnimationFrame(animate);
const speed = 0.05;
if (selectedObject) {
if (keys['ArrowUp']) selectedObject.position.z -= speed;
if (keys['ArrowDown']) selectedObject.position.z += speed;
if (keys['ArrowLeft']) selectedObject.position.x -= speed;
if (keys['ArrowRight']) selectedObject.position.x += speed;
}
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
下一步 #
现在你已经掌握了交互系统,接下来学习 纹理,为物体添加丰富的纹理效果!
最后更新:2026-03-28