本篇文章主要介绍在 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)
}
效果演示
最终代码及效果演示如下: