Three.js 点云矩形框选:屏幕投影与 SelectionBox

三种方案概览

方案适合场景复杂度
屏幕空间投影通用标注、框选
SelectionBox(Frustum)需要真 3D 选取(不受遮挡)
GPU Picking超大点云(百万级)性能优先

方案一:屏幕空间投影(推荐)

原理:把每个 3D 点投影到屏幕坐标,判断是否落在鼠标拖出的矩形内。

绘制选择框

const box = document.createElement('div');
box.style.cssText = `
  position: fixed;
  border: 1px solid #00aaff;
  background: rgba(0, 170, 255, 0.1);
  pointer-events: none;
`;
document.body.appendChild(box);

let startX = 0, startY = 0, endX = 0, endY = 0;
let dragging = false;

window.addEventListener('mousedown', e => {
  dragging = true;
  startX = endX = e.clientX;
  startY = endY = e.clientY;
  box.style.display = 'block';
});

window.addEventListener('mousemove', e => {
  if (!dragging) return;
  endX = e.clientX;
  endY = e.clientY;

  const left = Math.min(startX, endX);
  const top  = Math.min(startY, endY);
  box.style.left   = left + 'px';
  box.style.top    = top  + 'px';
  box.style.width  = Math.abs(endX - startX) + 'px';
  box.style.height = Math.abs(endY - startY) + 'px';
});

window.addEventListener('mouseup', () => {
  dragging = false;
  box.style.display = 'none';
  selectPoints();
});

框选核心逻辑

function selectPoints() {
  const minX = Math.min(startX, endX);
  const maxX = Math.max(startX, endX);
  const minY = Math.min(startY, endY);
  const maxY = Math.max(startY, endY);

  const positions = pointCloud.geometry.attributes.position;
  const selected  = [];
  const vector    = new THREE.Vector3();

  for (let i = 0; i < positions.count; i++) {
    vector.fromBufferAttribute(positions, i);

    // 应用模型变换
    vector.applyMatrix4(pointCloud.matrixWorld);

    // 投影到 NDC(Normalized Device Coordinates)
    vector.project(camera);

    // NDC [-1,1] 转屏幕像素
    const sx = (vector.x *  0.5 + 0.5) * window.innerWidth;
    const sy = (vector.y * -0.5 + 0.5) * window.innerHeight;

    if (sx >= minX && sx <= maxX && sy >= minY && sy <= maxY) {
      selected.push(i);
    }
  }

  console.log('选中点数:', selected.length);
  highlightPoints(selected);
}

高亮选中点

function highlightPoints(indices) {
  const colors = pointCloud.geometry.attributes.color;

  // 重置所有点为白色
  for (let i = 0; i < colors.count; i++) {
    colors.setXYZ(i, 1, 1, 1);
  }

  // 选中点设为红色
  for (const idx of indices) {
    colors.setXYZ(idx, 1, 0, 0);
  }

  colors.needsUpdate = true;
}

方案二:官方 SelectionBox(3D Frustum)

Three.js examples 提供了现成的框选工具:

import { SelectionBox }    from 'three/addons/interactive/SelectionBox.js';
import { SelectionHelper } from 'three/addons/interactive/SelectionHelper.js';

const selectionBox = new SelectionBox(camera, scene);
const helper       = new SelectionHelper(renderer, 'selectBox');

// CSS:.selectBox { border: 1px solid #55aaff; background: rgba(75,160,255,.1); }
document.addEventListener('pointerdown', e => {
  selectionBox.startPoint.set(
     (e.clientX / window.innerWidth)  * 2 - 1,
    -(e.clientY / window.innerHeight) * 2 + 1,
    0.5
  );
});

document.addEventListener('pointermove', e => {
  if (!helper.isDown) return;
  selectionBox.endPoint.set(
     (e.clientX / window.innerWidth)  * 2 - 1,
    -(e.clientY / window.innerHeight) * 2 + 1,
    0.5
  );
  // 实时更新选中(可选,性能开销大)
  // selectionBox.select();
});

document.addEventListener('pointerup', () => {
  const allSelected = selectionBox.select();
  // allSelected 是 Object3D 数组(Mesh/Points 等)
  console.log(allSelected);
});

SelectionBox 用 Frustum 判断,不受相机遮挡影响,适合需要选取被遮挡点的场景。

方案三:GPU Picking

对百万级点云,CPU 遍历太慢,改用 Render-To-Texture(RTT):

  1. 每个点用唯一颜色(编码点 index)渲染到离屏 FBO
  2. 读取矩形区域的像素颜色
  3. 解码颜色得到点 index
// 伪代码
renderer.setRenderTarget(pickingTexture);
renderer.render(scene, camera);  // 用 picking shader 渲染

const pixels = new Uint8Array(width * height * 4);
renderer.readRenderTargetPixels(pickingTexture, x, y, width, height, pixels);

const selected = decodeIndices(pixels);
renderer.setRenderTarget(null);

适合:自动驾驶标注平台、大规模点云编辑器等专业场景。

注意事项

  • 矩形太小时endX - startX < 2 可跳过框选避免误触
  • 相机控件冲突:拖动框选时需要禁用 OrbitControlscontrols.enabled = false
  • 性能:方案一遍历所有点,百万级点云需要 Worker 或 GPU 方案