Three.js 之鼠标选中物体/对象

本篇文章主要介绍在 threejs 创建的 3 维空间中,如何通过鼠标点击,选取某个物体/对象。前期的准备工作中,我已经创建好了如下所示的一个球体和一个立方体,如果你不清楚它们是如何创建出来的,可以移步《vite + ts 渲染一个旋转的立方体》。

坐标的转换

因为设置了 canvas 画布的宽高是占满整个浏览器窗口的,所以不妨直接在全局添加一个监听鼠标点击的事件,打印鼠标点击位置的 offsetX 与 offsetY:

javascript 复制代码
// 代码片段 1
addEventListener('click', event => {
  const offsetX = event.offsetX
  const offsetY = event.offsetY
  console.log(offsetX, offsetY)
})

结果如下:

由上动图可知,假设 canvas 画布的宽高为 400px * 300px,则鼠标点击时,触发 click 事件时回传给我们的 event 的 offsetX 与 offsetY,所依据的坐标系统应该如下所示,左上角为 (0, 0) 点;右下角的 offsetX 等于画布的宽度,offsetY 则为画布的高度:

这与如下在 《从画一个点入手》中介绍的 webgl 中的标准设备坐标系并不相同 ------ y 轴方向相反,(0, 0) 点位置不一致,且在标准设备坐标系中,取值范围都是从 -1 到 1 的:

所以我们需要对点击得到的坐标进行转换,比如点击得到的 (0, 0),(200, 150),(400, 300),就要分别转换为(-1, 1),(0, 0),(1, -1),其实就是将两个坐标系中,x 轴与 y 轴上的点,一一做个映射。 转换公式如下:

javascript 复制代码
// 代码片段 1.1
x = (offsetX / width) * 2 - 1
y = -(offsetY / height) * 2 + 1

其中,width、height 分别是 canvas 画布的宽高,x、y 就是转换后的标准设备坐标的分量值。

光线投射(Raycaster)

在 threejs 中,我们使用光线投射(Raycaster),来实现对鼠标经过的 3 维空间中的物体的拾取:

typescript 复制代码
const raycaster = new THREE.Raycaster()

射线(Ray)

Raycaster 类型的实例有个属性为 ray,它是用于光线投射的射线,类型为 Ray,打印查看 ray 属性:

javascript 复制代码
console.log(raycaster.ray)

结果如下:

其 2 个属性的值虽然都是 Vector3 类型的 3 维向量,但是 direction 表示的是射线的方向,origin 表示的是射线的起始点。

射线拥有我从官方文档截取的下图所示的可以检测是否与某类对象相交的一系列方法:

如果相交,则会返回一个值复制到我们提前准备好的 Vector3 类型的变量(target)中;如果不相交,则返回空值 null。

ray 可以直接通过 new THREE.Ray() 生成:

javascript 复制代码
const ray = new THREE.Ray()

setFromCamera 方法

我们的目的是通过鼠标点击来拾取物体,所以我们应该让 raycaster.ray 代表的这条射线,自相机发出,射向鼠标所点击的位置,raycaster 有个 setFromCamera 方法,恰能帮我们实现这一目标:

typescript 复制代码
raycaster.setFromCamera(new THREE.Vector2(x, y), camera)

传入的第 1 个参数为标准化设备坐标中鼠标的二维坐标,即代码片段 1.1 中获取的 x 和 y,它们都是在 -1 到 1 之间的值;

第 2 个参数就是射线所来源的摄像机了,传入的是场景里创建的相机 camera。

在代码片段 1 的 click 事件监听回调函数内部生成 raycaster 实例并调用如上 setFromCamera 方法,点击球体,执行 console.log(raycaster.ray) 查看生成的射线实例:

此时射线的起始点 origin,就是之前定义的相机位置:

typescript 复制代码
camera.position.set(0, 0, 10)

intersectObjects 方法

raycaster 拥有一个 intersectObjects 方法,可以用来获取与其 ray 属性所表示的射线相交的物体:

typescript 复制代码
const intersects = raycaster.intersectObjects([cube, sphere])

传入的第 1 个参数是个数组,数组里放的就是我们要检测是否与射线相交的物体:cube 与 sphere。

方法的返回值 intersects 是一个数组,射线与多少个物体相交,则数组中就会有几个元素,其中第 1 个元素表示第 1 个相交的物体,每个元素中会包含一些信息:

其中 object 属性的值就是相交的物体,我们可以改变该物体的材质的颜色:

typescript 复制代码
if (intersects.length > 0) {
  intersects[0].object.material.color.set(0xFB3F63)
}

效果演示

最终代码及效果演示如下:

相关推荐
IT_陈寒8 小时前
Vite的这个问题坑了我三天,原来如此解决!
前端·人工智能·后端
网易云信8 小时前
大模型能上岗做销售吗?我们让 12 款国产模型交了 420 份作业
前端·后端
蜗牛互联网9 小时前
Python Agent工具调用的白名单、参数校验与审批门禁
java·服务器·前端·人工智能·后端·python
计算机魔术师9 小时前
Perplexity:每个 Project 都是一个 Git 仓库
前端
Andrew5519 小时前
让 AI 写页面不再“一眼假”:我把设计品味做成了一个 Claude Code Skill 流水线
前端
乐享qicheng9 小时前
14-AI智能管家JSBridge深度问答
前端
10年前端老司机9 小时前
我用 Vue3 + LangGraph 做了个心理咨询 AI,踩了 9 个坑后总结出来的实战经验
前端·langchain·agent
数据掘金10 小时前
A/B 实验分组参数回滚后还在生效?一次参数校验体系的重构复盘
前端
bmxy小明同学10 小时前
2026-10-09-token与成本
java·前端·数据库
neoooo11 小时前
🚀Claude Code 小技巧(二):找回会话,撤销修改
前端·人工智能·后端