Showing Posts From
Three.js
Three.js 点云矩形框选:屏幕空间投影法
用 Three.js 显示点云(THREE.Points)后,做"鼠标拖矩形框选一批点"这类交互,标准做法是屏幕空间投影——把每个 3D 点 project 到 2D 屏幕,判是否落在鼠标矩形里。 为什么不用 Raycaster Three.js 的 Raycaster 主要针对射线,判"鼠标下选中哪个物体"用得多。但做矩形框选用它效率极差——要么在 CPU 上做 AABB,要么写 GPU shader,两条路都比"直接投影"复杂得多。 生产上标注工具、CAD、自动驾驶点云可视化基本都用屏幕空间法。 整体流程 鼠标 mousedown → 记录 (startX, startY) mousemove → 更新矩形 DOM mouseup → 遍历 points.geometry ├─ 每个点 → applyMatrix4(worldMatrix) ├─ → .project(camera) 转 NDC ├─ → NDC → 屏幕坐标 └─ 判断是否落在矩形内完整最小实现 先做一个 DOM 矩形跟着鼠标: const box = document.createElement("div"); box.style.cssText = ` position: fixed; border: 1px solid #00aaff; background: rgba(0,170,255,0.1); pointer-events: none; display: none; `; document.body.appendChild(box);let startX = 0, startY = 0, endX = 0, endY = 0, dragging = false;window.addEventListener("mousedown", e => { dragging = true; startX = e.clientX; startY = e.clientY; Object.assign(box.style, { left: startX + "px", top: startY + "px", width: "0px", height: "0px", display: "block", }); });window.addEventListener("mousemove", e => { if (!dragging) return; endX = e.clientX; endY = e.clientY; Object.assign(box.style, { left: Math.min(startX, endX) + "px", top: Math.min(startY, endY) + "px", width: Math.abs(endX - startX) + "px", 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 attr = pointCloud.geometry.attributes.position; const vec = new THREE.Vector3(); const selected = []; pointCloud.updateMatrixWorld(); for (let i = 0; i < attr.count; i++) { vec.fromBufferAttribute(attr, i); vec.applyMatrix4(pointCloud.matrixWorld); // 本地 → 世界 vec.project(camera); // 世界 → NDC (-1 ~ 1) // NDC → 屏幕 const sx = (vec.x * 0.5 + 0.5) * window.innerWidth; const sy = (-vec.y * 0.5 + 0.5) * window.innerHeight; if (sx >= minX && sx <= maxX && sy >= minY && sy <= maxY) { selected.push(i); } } console.log("选中", selected.length, "个点"); highlight(selected); }高亮选中的点 改颜色最简单的做法:给点云加一个 color attribute。 const geo = pointCloud.geometry; if (!geo.attributes.color) { const colors = new Float32Array(geo.attributes.position.count * 3); colors.fill(1); // 全白 geo.setAttribute("color", new THREE.BufferAttribute(colors, 3)); pointCloud.material.vertexColors = true; }function highlight(indices) { const colors = geo.attributes.color; // 先全部还原 for (let i = 0; i < colors.count; i++) colors.setXYZ(i, 1, 1, 1); // 选中的染红 for (const i of indices) colors.setXYZ(i, 1, 0, 0); colors.needsUpdate = true; }剔除相机背后的点 vec.project(camera) 对相机背后的点 z 会 > 1,也可能被误判进屏幕矩形。加一道过滤: if (vec.z < -1 || vec.z > 1) continue; // 相机视锥体外性能 十万点级别这套办法很流畅(一次 mouseup 内做完)。到一百万点就要考虑:用 Worker 并发做投影 GPU picking:把点索引编码到颜色渲染到 offscreen canvas,读像素判索引 空间分区(Octree/KDTree)预筛选一般标注场景十万级足够。 一句话总结 点云框选 = 每个点 project 到屏幕 + 矩形内包含判定。做完投影后加"背后点剔除",用 color attribute 高亮,是最直接可用的方案。
