{T}

交互基础

Three.js 提供了多种方式实现用户交互,其中 Raycaster(射线投射器)是实现鼠标拾取和交互的核心工具。

概述

交互是 3D 应用的关键功能,通过射线检测、碰撞检测等技术,可以实现物体选择、拖拽、点击等交互操作。本文档详细介绍 Three.js 中的交互实现机制。

核心概念

射线投射原理

射线投射(Raycasting)是从一个点沿某个方向发射一条无限长的射线,检测该射线与场景中几何体的相交情况。

plaintext
相机位置 (Camera)


    ┌─────────────────────────────┐
    │         屏幕空间            │
    │    ┌───────────────────┐   │
    │    │    鼠标位置 (x,y)  │   │
    │    │         ●         │   │
    │    │         │         │   │
    │    └─────────│─────────┘   │
    │              │              │
    │              ▼              │
    │    射线 (Raycaster)         │
    │              │              │
    │              ▼              │
    │    ┌───────────────────┐   │
    │    │   相交物体 (Mesh)  │   │
    │    └───────────────────┘   │
    └─────────────────────────────┘

坐标系统

坐标系范围说明
屏幕坐标0 ~ width/height鼠标在屏幕上的像素位置
标准化设备坐标 (NDC)-1 ~ 1OpenGL 标准坐标,原点在中心
世界坐标无限制3D 场景中的实际位置

坐标转换

javascript
// 屏幕坐标 → NDC
const ndcX = (screenX / window.innerWidth) * 2 - 1;
const ndcY = -(screenY / window.innerHeight) * 2 + 1;
 
// NDC → 屏幕坐标
const screenX = (ndcX + 1) / 2 * window.innerWidth;
const screenY = -(ndcY - 1) / 2 * window.innerHeight;

Raycaster 基础

创建 Raycaster

javascript
import * as THREE from 'three';
 
// 创建射线投射器
const raycaster = new THREE.Raycaster();
 
// 设置射线参数
raycaster.set(
  origin,      // 起点(Vector3)
  direction,   // 方向(Vector3,需要归一化)
  near,        // 近裁剪距离(可选,默认 0)
  far          // 远裁剪距离(可选,默认 Infinity)
);
 
// 从相机和鼠标位置设置射线
raycaster.setFromCamera(mouse, camera);

Raycaster 属性

属性类型默认值说明
rayRay-射线对象,包含 origin 和 direction
nearnumber0近裁剪距离
farnumberInfinity远裁剪距离
cameraCameranull关联的相机(用于 setFromCamera)
layersLayers-检测的图层掩码
paramsObject-检测参数配置

检测参数配置

javascript
// 配置检测参数
raycaster.params = {
  Mesh: {
    threshold: 0.1  // 点云检测阈值
  },
  Line: {
    threshold: 0.1  // 线段检测阈值
  },
  LOD: {
    threshold: 0.1
  },
  Points: {
    threshold: 0.1  // 点检测阈值
  }
};

鼠标交互

javascript
// 创建射线和鼠标向量
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
 
// 监听鼠标移动
window.addEventListener('mousemove', (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) {
    console.log('相交对象:', intersects[0].object);
  }
});

intersectObjects 方法

javascript
// 检测单个或多个对象
const intersects = raycaster.intersectObjects(objects);
 
// 完整参数
raycaster.intersectObjects(
  objects,           // Object3D[] - 要检测的对象数组
  recursive,         // boolean - 是否递归检测子对象(默认 false)
  optionalTarget     // Array - 可选的目标数组,用于存储结果
);

相交结果结构

javascript
// 返回值(按距离排序)
// [
//   {
//     distance: 10,           // 从射线起点到相交点的距离
//     distanceToRay: 0,       // 相交点到射线的距离(仅点云)
//     point: Vector3,         // 世界坐标中的相交点
//     index: 0,               // 几何体索引(索引几何体)
//     face: Face3,            // 相交面(三角形面)
//     faceIndex: 0,           // 面索引
//     object: Object3D,       // 相交的 3D 对象
//     uv: Vector2,            // 相交点的 UV 坐标
//     uv2: Vector2,           // 第二套 UV 坐标
//     instanceId: 0           // 实例化对象的实例 ID
//   },
//   ...
// ]

其他检测方法

javascript
// 检测场景中的所有对象
const intersects = raycaster.intersectObjects(scene.children, true);
 
// 检测单个对象
const intersects = raycaster.intersectObject(mesh);
 
// 检测点云中的点
const intersects = raycaster.intersectObject(points);
// intersects[0].index - 点的索引
// intersects[0].distanceToRay - 点到射线的距离

鼠标拾取示例

基础拾取

javascript
import * as THREE from 'three';
 
// 场景设置
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
 
// 创建多个立方体
const cubes = [];
for (let i = 0; i < 10; i++) {
  const geometry = new THREE.BoxGeometry(1, 1, 1);
  const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
  const cube = new THREE.Mesh(geometry, material);
  
  cube.position.x = (Math.random() - 0.5) * 10;
  cube.position.y = (Math.random() - 0.5) * 10;
  cube.position.z = (Math.random() - 0.5) * 10;
  
  scene.add(cube);
  cubes.push(cube);
}
 
// 光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);
 
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
 
// 射线和鼠标
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedObject = null;
 
// 鼠标移动事件
window.addEventListener('mousemove', (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(cubes);
  
  // 重置之前选中的对象
  if (selectedObject) {
    selectedObject.material.color.setHex(0x00ff00);
  }
  
  // 高亮新选中的对象
  if (intersects.length > 0) {
    selectedObject = intersects[0].object;
    selectedObject.material.color.setHex(0xff0000);
  } else {
    selectedObject = null;
  }
});
 
// 动画循环
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
 
animate();

点击选择

javascript
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedObjects = [];
 
// 点击事件
window.addEventListener('click', (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(cubes);
  
  if (intersects.length > 0) {
    const object = intersects[0].object;
    
    // 切换选中状态
    const index = selectedObjects.indexOf(object);
    if (index > -1) {
      // 取消选中
      selectedObjects.splice(index, 1);
      object.material.color.setHex(0x00ff00);
    } else {
      // 选中
      selectedObjects.push(object);
      object.material.color.setHex(0xff0000);
    }
  }
});
 
// Ctrl + 点击多选
window.addEventListener('click', (event) => {
  if (!event.ctrlKey) {
    // 清除之前的选择
    selectedObjects.forEach(obj => obj.material.color.setHex(0x00ff00));
    selectedObjects = [];
  }
  // ... 其余逻辑
});

拖拽交互

基础拖拽

javascript
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let isDragging = false;
let draggedObject = null;
let plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
let intersection = new THREE.Vector3();
let offset = new THREE.Vector3();
 
// 鼠标按下
window.addEventListener('mousedown', (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(cubes);
  
  if (intersects.length > 0) {
    isDragging = true;
    draggedObject = intersects[0].object;
    
    // 设置拖拽平面(垂直于相机方向,经过物体位置)
    plane.setFromNormalAndCoplanarPoint(
      camera.getWorldDirection(new THREE.Vector3()),
      draggedObject.position
    );
    
    // 计算偏移量
    raycaster.ray.intersectPlane(plane, intersection);
    offset.copy(intersection).sub(draggedObject.position);
    
    // 防止穿透
    renderer.domElement.style.cursor = 'grabbing';
  }
});
 
// 鼠标移动
window.addEventListener('mousemove', (event) => {
  if (!isDragging) return;
  
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  
  raycaster.setFromCamera(mouse, camera);
  
  // 计算射线与平面的交点
  if (raycaster.ray.intersectPlane(plane, intersection)) {
    draggedObject.position.copy(intersection.sub(offset));
  }
});
 
// 鼠标释放
window.addEventListener('mouseup', () => {
  isDragging = false;
  draggedObject = null;
  renderer.domElement.style.cursor = 'default';
});

平面拖拽

在特定平面上进行拖拽操作。

javascript
// XY 平面拖拽(Z 轴固定)
const xyPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
 
// XZ 平面拖拽(Y 轴固定)
const xzPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
 
// YZ 平面拖拽(X 轴固定)
const yzPlane = new THREE.Plane(new THREE.Vector3(1, 0, 0), 0);
 
// 使用示例:在地面上拖拽物体
let dragPlane = xzPlane;
 
window.addEventListener('mousemove', (event) => {
  if (!isDragging) return;
  
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
  
  raycaster.setFromCamera(mouse, camera);
  
  const intersection = new THREE.Vector3();
  if (raycaster.ray.intersectPlane(dragPlane, intersection)) {
    draggedObject.position.x = intersection.x;
    draggedObject.position.z = intersection.z;
    // Y 坐标保持不变
  }
});

轴向拖拽

沿特定轴进行拖拽。

javascript
// 轴向拖拽辅助函数
function createAxisDragHelper(axis) {
  const axisVector = {
    x: new THREE.Vector3(1, 0, 0),
    y: new THREE.Vector3(0, 1, 0),
    z: new THREE.Vector3(0, 0, 1)
  };
  
  return {
    plane: new THREE.Plane(),
    axis: axisVector[axis],
    
    updatePlane(object) {
      // 创建垂直于其他两个轴的平面
      const normal = new THREE.Vector3();
      if (axis === 'x') normal.set(0, 1, 0);
      else if (axis === 'y') normal.set(1, 0, 0);
      else normal.set(1, 0, 0);
      
      this.plane.setFromNormalAndCoplanarPoint(normal, object.position);
    },
    
    move(object, intersection) {
      const dot = intersection.dot(this.axis);
      object.position[axis] = dot;
    }
  };
}
 
// 使用示例
const xDragger = createAxisDragHelper('x');

高级交互

轮廓高亮

使用后处理实现高质量的轮廓高亮效果。

javascript
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js';
 
// 创建后处理器
const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
 
// 轮廓通道
const outlinePass = new OutlinePass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  scene,
  camera
);
composer.addPass(outlinePass);
 
// 轮廓效果参数
outlinePass.edgeStrength = 5;      // 边缘强度
outlinePass.edgeGlow = 1;          // 边缘发光
outlinePass.edgeThickness = 2;     // 边缘厚度
outlinePass.pulsePeriod = 2;       // 脉冲周期(0 表示无脉冲)
outlinePass.visibleEdgeColor.set('#ffffff');   // 可见边缘颜色
outlinePass.hiddenEdgeColor.set('#ffffff');    // 遮挡边缘颜色
 
// 更新选中对象
function updateOutline(objects) {
  outlinePass.selectedObjects = objects;
}
 
// 使用
window.addEventListener('mousemove', (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(cubes);
  
  if (intersects.length > 0) {
    updateOutline([intersects[0].object]);
  } else {
    updateOutline([]);
  }
});
 
// 渲染循环使用 composer
function animate() {
  requestAnimationFrame(animate);
  composer.render();
}

悬停提示

javascript
// 创建提示元素
const tooltip = document.createElement('div');
tooltip.style.cssText = `
  position: absolute;
  padding: 10px;
  background: rgba(0, 0, 0, 0.7);
  color: #fff;
  border-radius: 5px;
  pointer-events: none;
  display: none;
  font-size: 14px;
  z-index: 1000;
`;
document.body.appendChild(tooltip);
 
// 显示提示
function showTooltip(object, x, y) {
  tooltip.innerHTML = object.userData.name || 'Object';
  tooltip.style.left = (x + 15) + 'px';
  tooltip.style.top = (y + 15) + 'px';
  tooltip.style.display = 'block';
}
 
// 隐藏提示
function hideTooltip() {
  tooltip.style.display = 'none';
}
 
// 鼠标移动
window.addEventListener('mousemove', (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(cubes);
  
  if (intersects.length > 0) {
    showTooltip(intersects[0].object, event.clientX, event.clientY);
    renderer.domElement.style.cursor = 'pointer';
  } else {
    hideTooltip();
    renderer.domElement.style.cursor = 'default';
  }
});

框选

实现矩形框选多个对象。

javascript
// 框选功能
const selectionBox = document.createElement('div');
selectionBox.style.cssText = `
  position: absolute;
  border: 1px solid #00ff00;
  background: rgba(0, 255, 0, 0.1);
  pointer-events: none;
  display: none;
`;
document.body.appendChild(selectionBox);
 
let isSelecting = false;
let startPoint = { x: 0, y: 0 };
let selectedByBox = [];
 
window.addEventListener('mousedown', (event) => {
  if (event.button === 0 && event.shiftKey) {
    isSelecting = true;
    startPoint = { x: event.clientX, y: event.clientY };
    selectionBox.style.display = 'block';
    updateSelectionBox(startPoint, startPoint);
  }
});
 
window.addEventListener('mousemove', (event) => {
  if (!isSelecting) return;
  updateSelectionBox(startPoint, { x: event.clientX, y: event.clientY });
});
 
window.addEventListener('mouseup', (event) => {
  if (!isSelecting) return;
  
  isSelecting = false;
  selectionBox.style.display = 'none';
  
  // 执行框选检测
  performBoxSelection(
    startPoint,
    { x: event.clientX, y: event.clientY }
  );
});
 
function updateSelectionBox(start, end) {
  const left = Math.min(start.x, end.x);
  const top = Math.min(start.y, end.y);
  const width = Math.abs(end.x - start.x);
  const height = Math.abs(end.y - start.y);
  
  selectionBox.style.left = left + 'px';
  selectionBox.style.top = top + 'px';
  selectionBox.style.width = width + 'px';
  selectionBox.style.height = height + 'px';
}
 
function performBoxSelection(start, end) {
  // 计算框选的 NDC 坐标
  const left = Math.min(start.x, end.x) / window.innerWidth * 2 - 1;
  const right = Math.max(start.x, end.x) / window.innerWidth * 2 - 1;
  const top = -(Math.min(start.y, end.y) / window.innerHeight * 2 - 1);
  const bottom = -(Math.max(start.y, end.y) / window.innerHeight * 2 - 1);
  
  // 创建框选视锥体
  const frustum = new THREE.Frustum();
  const projMatrix = new THREE.Matrix4().makePerspective(
    left, right, top, bottom, 
    camera.near, camera.far
  );
  projMatrix.multiply(camera.matrixWorldInverse);
  frustum.setFromProjectionMatrix(projMatrix);
  
  // 检测对象
  selectedByBox = [];
  cubes.forEach(cube => {
    // 使用包围盒检测
    const box = new THREE.Box3().setFromObject(cube);
    if (frustum.intersectsBox(box)) {
      selectedByBox.push(cube);
      cube.material.color.setHex(0xff0000);
    }
  });
  
  console.log(`选中 ${selectedByBox.length} 个对象`);
}

性能优化

减少检测频率

使用节流减少射线检测频率。

javascript
// 节流函数
function throttle(func, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}
 
// 应用节流
const throttledIntersect = throttle((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);
  // ...
}, 50);  // 每 50ms 最多执行一次
 
window.addEventListener('mousemove', throttledIntersect);

限制检测对象

只检测可交互的对象,减少检测范围。

javascript
// 使用 userData 标记可交互对象
mesh.userData.interactive = true;
 
// 只检测可交互对象
const interactiveObjects = [];
scene.traverse((obj) => {
  if (obj.userData.interactive) {
    interactiveObjects.push(obj);
  }
});
 
// 或使用图层
const INTERACTIVE_LAYER = 1;
interactiveMesh.layers.set(INTERACTIVE_LAYER);
 
raycaster.layers.set(INTERACTIVE_LAYER);
const intersects = raycaster.intersectObjects(scene.children);

使用包围盒检测

先进行包围盒检测,再进行精确检测。

javascript
// 快速包围盒检测
function quickSelect(mouse, camera, objects) {
  raycaster.setFromCamera(mouse, camera);
  
  // 先用包围盒筛选
  const candidates = objects.filter(obj => {
    const box = new THREE.Box3().setFromObject(obj);
    return raycaster.ray.intersectsBox(box);
  });
  
  // 再进行精确检测
  return raycaster.intersectObjects(candidates);
}

八叉树加速

对于大量对象,使用八叉树空间划分加速检测。

javascript
import { Octree } from 'three/addons/math/Octree.js';
 
// 创建八叉树
const octree = new Octree();
octree.fromGraphNode(scene);
 
// 使用八叉树进行射线检测
const octreeRaycaster = new THREE.Raycaster();
const result = [];
octreeRaycaster.intersectOctree(octree, result);
 
console.log('检测结果:', result);

按需更新

只在需要时进行检测。

javascript
let needsIntersectCheck = false;
 
// 鼠标移动时标记需要检测
window.addEventListener('mousemove', () => {
  needsIntersectCheck = true;
});
 
// 在渲染循环中检测
function animate() {
  requestAnimationFrame(animate);
  
  if (needsIntersectCheck) {
    performIntersection();
    needsIntersectCheck = false;
  }
  
  renderer.render(scene, camera);
}

API 参考

Raycaster 类

构造函数

javascript
new THREE.Raycaster(origin, direction, near, far)
参数类型默认值说明
originVector3(0,0,0)射线起点
directionVector3(0,0,-1)射线方向(需归一化)
nearnumber0近裁剪距离
farnumberInfinity远裁剪距离

主要方法

方法参数返回值说明
set(origin, direction)Vector3, Vector3void设置射线起点和方向
setFromCamera(coords, camera)Vector2, Cameravoid从相机和 NDC 坐标设置射线
intersectObject(object, recursive, target)Object3D, boolean, ArrayArray检测单个对象
intersectObjects(objects, recursive, target)Array, boolean, ArrayArray检测多个对象

Ray 类属性

属性类型说明
originVector3射线起点
directionVector3射线方向(归一化)

Ray 类方法

方法说明
at(t, target)获取射线上距离 t 处的点
lookAt(v)设置射线方向指向 v
closestPointToPoint(point, target)射线上距离点最近的点
distanceToPoint(point)射线到点的距离
distanceSqToPoint(point)射线到点距离的平方
intersectsSphere(sphere)是否与球相交
intersectsBox(box)是否与包围盒相交
intersectsPlane(plane)是否与平面相交
intersectPoint(point, plane, target)与平面的交点

Plane 类

javascript
// 创建平面
const plane = new THREE.Plane(normal, constant);
 
// 从法线和点创建
plane.setFromNormalAndCoplanarPoint(normal, point);
 
// 计算射线与平面的交点
ray.intersectPlane(plane, target);

常见问题

Q: 射线检测不到对象?

A: 检查以下几点:

javascript
// 1. 确认对象在场景中
console.log('对象是否在场景:', scene.children.includes(mesh));
 
// 2. 检查相机视锥
const frustum = new THREE.Frustum();
frustum.setFromProjectionMatrix(
  new THREE.Matrix4().multiplyMatrices(
    camera.projectionMatrix, 
    camera.matrixWorldInverse
  )
);
console.log('对象是否在视锥内:', frustum.containsPoint(mesh.position));
 
// 3. 检查材质是否可见
console.log('材质可见:', mesh.material.visible);
 
// 4. 检查是否开启了递归检测
raycaster.intersectObjects(objects, true);  // 递归检测子对象
 
// 5. 检查射线方向
console.log('射线:', raycaster.ray);

Q: 拖拽时物体跳动?

A: 需要计算鼠标点击位置与物体中心之间的偏移:

javascript
let offset = new THREE.Vector3();
 
// mousedown 时计算偏移
raycaster.ray.intersectPlane(plane, intersection);
offset.copy(intersection).sub(draggedObject.position);
 
// mousemove 时应用偏移
if (raycaster.ray.intersectPlane(plane, intersection)) {
  draggedObject.position.copy(intersection.sub(offset));
}

Q: 检测精度不够?

A: 调整检测参数或使用包围盒:

javascript
// 对于线或点云
raycaster.params.Line.threshold = 0.1;  // 增大检测阈值
raycaster.params.Points.threshold = 0.5;
 
// 对于细小物体,使用包围球
const sphere = new THREE.Sphere();
mesh.geometry.computeBoundingSphere();
sphere.copy(mesh.geometry.boundingSphere);
sphere.applyMatrix4(mesh.matrixWorld);
 
if (raycaster.ray.intersectsSphere(sphere)) {
  // 检测到
}

Q: 多个对象重叠时如何选择?

A: 使用距离排序或图层优先级:

javascript
// 按距离排序(默认行为)
const intersects = raycaster.intersectObjects(objects);
// intersects[0] 是最近的
 
// 按优先级选择
const prioritized = intersects.sort((a, b) => {
  const priorityA = a.object.userData.priority || 0;
  const priorityB = b.object.userData.priority || 0;
  return priorityB - priorityA;
});

Q: 性能问题:太多对象导致卡顿?

A: 使用空间划分或减少检测频率:

javascript
// 1. 使用八叉树
import { Octree } from 'three/addons/math/Octree.js';
const octree = new Octree();
octree.fromGraphNode(scene);
 
// 2. 使用图层分组
const nearObjects = objects.filter(obj => obj.position.distanceTo(camera.position) < 50);
raycaster.intersectObjects(nearObjects);
 
// 3. 节流检测
const throttledCheck = throttle(() => {
  // 检测逻辑
}, 100);

Q: 如何检测透明物体的背面?

A: 默认情况下射线会穿透透明材质:

javascript
// 方法1:设置 material.side
material.side = THREE.DoubleSide;  // 双面渲染
 
// 方法2:检测所有相交点
const intersects = raycaster.intersectObjects(objects);
intersects.forEach((intersect, index) => {
  console.log(`第${index}个相交点:`, intersect.object.name);
});

最佳实践

  1. 合理使用节流:对于频繁触发的事件(如 mousemove),使用节流控制检测频率
  2. 限制检测范围:只检测可交互的对象,使用图层或 userData 标记
  3. 空间划分:大量对象时使用八叉树或 BVH 加速
  4. 包围盒预检:先用包围盒快速筛选,再进行精确检测
  5. 及时清理:移除对象时同时从检测数组中移除
  6. 用户体验:添加视觉反馈(光标变化、高亮效果)

相关链接