三种方案概览
| 方案 | 适合场景 | 复杂度 |
|---|---|---|
| 屏幕空间投影 | 通用标注、框选 | 低 |
| 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):
- 每个点用唯一颜色(编码点 index)渲染到离屏 FBO
- 读取矩形区域的像素颜色
- 解码颜色得到点 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可跳过框选避免误触 - 相机控件冲突:拖动框选时需要禁用
OrbitControls(controls.enabled = false) - 性能:方案一遍历所有点,百万级点云需要 Worker 或 GPU 方案
