跳到主要内容
3D Playground
Three.js 核心第 8 / 13 节

射线拾取

进阶WebGL

问题:屏幕上的一个点对应 3D 里的什么?

鼠标只给出 2D 像素坐标,而物体在 3D 空间里。解决办法是从相机出发,穿过鼠标所在的点,发射一条射线,看它先碰到谁。这就是 THREE.Raycaster。

第一步:像素 → NDC

GPU 用标准化设备坐标(NDC)描述屏幕:x、y 都在 -1 到 +1 之间,(0,0) 是画布中心,+y 朝上。而 CSS 像素的 y 是朝下的,所以要翻转:

js
const r = canvas.getBoundingClientRect();
pointer.x = ((e.clientX - r.left) / r.width) * 2 - 1;
pointer.y = -((e.clientY - r.top) / r.height) * 2 + 1;

一定要用 getBoundingClientRect(),因为画布不一定占满整个窗口。

第二步:射线 + 求交

raycaster.setFromCamera(pointer, camera) 根据相机的投影把 NDC 点“反投影”成一条世界空间中的射线。然后 raycaster.intersectObjects(list) 返回所有交点,按距离从近到远排序。每个结果包含:

  • object:被击中的物体
  • point:世界坐标中的交点
  • distance:到相机的距离
  • face / faceIndex 与 normal:被击中的三角形和它的法线(局部空间,需要用 transformDirection(object.matrixWorld) 转到世界空间)

示例用白点标出交点,用黄色箭头画出世界空间法线。

悬停与点击

  • 悬停:每帧在渲染前做一次射线检测,比较结果和上一帧是否不同,再修改材质的 emissive。注意每个物体要有自己的材质,共享材质会让所有物体一起变色。
  • 点击:OrbitControls 同样使用指针,拖拽旋转时不应误选。示例记录 pointerdown 的位置,只有在 pointerup 时移动不超过 4 像素才算一次点击。

性能提示

射线检测在 CPU 上逐个三角形测试,物体多、面数高时会变慢。只传入需要拾取的物体列表;对复杂模型可以用简化的不可见代理几何体,或者用 BVH 加速结构。

移动鼠标观察状态栏:NDC 坐标、命中数量和最近物体的名称会实时更新。

核心 API

THREE.Raycasterraycaster.setFromCameraraycaster.intersectObjectscanvas.getBoundingClientRectTHREE.Vector2Vector3.transformDirectionmaterial.emissiveTHREE.ArrowHelper

动手试试

  1. 1把 intersectObjects 改为也传入 floor,在地板上移动鼠标,看看命中点和法线。
  2. 2点击时把选中的物体移动到命中点上方 1 个单位,做一个简单的“拾取并抬起”。
  3. 3把 hits.length 打印出来:为什么有时一条射线会命中 2 个物体?
  4. 4去掉 pointer.y 前面的负号,体会 NDC 的 y 轴方向。
运行中