问题:屏幕上的一个点对应 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把 intersectObjects 改为也传入 floor,在地板上移动鼠标,看看命中点和法线。
- 2点击时把选中的物体移动到命中点上方 1 个单位,做一个简单的“拾取并抬起”。
- 3把 hits.length 打印出来:为什么有时一条射线会命中 2 个物体?
- 4去掉 pointer.y 前面的负号,体会 NDC 的 y 轴方向。
还没有输出。console.log 的内容会显示在这里。
运行中