射线拾取
六十个站点悬停即高亮,单击落到地面并读出世界坐标,两种拾取方式一键切换。
说明
指针位置先换算成归一化设备坐标,再交给
raycaster.setFromCamera 生成一条从相机出发的射线。
站点模式用 intersectObjects 检测锥体,
地面模式则与一个数学平面求交,后者完全没有几何参与,开销更低。
命中后把锥体的自发光颜色改成蓝色并放大,指针移开即还原。 落点用一枚贴地圆环表示,坐标以场景单位显示在工具栏, 保留两位小数便于核对。
| 设置 | 数值 | 说明 |
|---|---|---|
| 站点数量 | 60 | 黄金角散点,半径从 4 递增 |
| 站点几何 | 锥体,半径 0.7、高 2.2 | 共享几何体,材质各自独立 |
| 拾取模式 | 站点 / 地面 | 按钮切换,互不干扰 |
| 递归检测 | false | 只检测列表内的对象本身 |
| 地面平面 | y = 0 | Plane 法线朝上,常数为 0 |
| 相机 | 固定,位于 (0, 26, 34) | 不接轨道控制,避免拖拽与拾取冲突 |
坐标换算
function updatePointer(event) {
const rect = renderer.domElement.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
}
raycaster.setFromCamera(pointer, camera);
getBoundingClientRect() 返回的是元素在视口中的实际区域,
比 clientWidth 更可靠:画布被 CSS 缩放、页面滚动、
或嵌在 iframe 里时都不会算错。Y 轴的负号是必须的,
设备坐标原点在左下,屏幕坐标原点在左上。
与平面求交
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const hit = new THREE.Vector3();
if (raycaster.ray.intersectPlane(groundPlane, hit)) {
ring.position.set(hit.x, 0.04, hit.z);
}
intersectPlane 返回布尔值,命中时把结果写入传入的向量,
不命中时该向量保持不变。平面常数项 constant 表示
normal · x + constant = 0 的偏移,所以法线朝上、常数为 0
恰好是水平面 y = 0。
多点标记的位置
站点在动画里轻微起伏,拾取使用的是它们的当前世界位置, 因此悬停判定始终与画面一致。如果在动画中改变了对象层级或缩放, 射线检测会在下一次拾取时自动跟上,不需要手动刷新。
性能边界
每次移动都做一次射线检测在这个规模下没有负担。 换成十万级三角面的地形后,逐帧检测会明显占用主线程, 常见做法是先用数学平面算出大致位置,再在局部范围内做精确检测。
拾取结果只有几何信息。命中项给出坐标、面索引与对象引用,
业务属性需要自己挂在 userData 上。
three.js 不是地图引擎,不会把屏幕点击自动换算成经纬度,
这一步必须由项目里的坐标转换完成。