在 Web 3D 项目中经常会碰到和鼠标交互相关的需求,例如用户可以通过鼠标来探索和操控场景中的对象,如旋转、放大缩小、拖动等操作,使用户更加沉浸于虚拟的三维环境中;点击一个对象后触发特定的事件、显示对象的详细信息等,增加场景的交互性和实用性;控制场景的导航,比如通过鼠标拖动实现场景的漫游和平移操作,这样用户可以更方便地探索和浏览场景中的对象。
我们之前已经用 Three.js 内置的相机轨道控制器OrbitControls
来添加鼠标交互,本篇章将介绍如何手动去实现自己的鼠标场景交互。
射线
在讲解鼠标交互的具体内容前,需要先了解有关 射线 的相关概念。
射线可以看作是一条无限延伸的直线,它有一个起点和一个方向。通过射线的走向,我们可以判断射线是否与场景中的对象相交。它在计算机图形学和游戏开发中非常常见,用于实现鼠标交互、碰撞检测、拾取物体等功能。
射线投射器Raycaster
这么一个工具,可以通过指定起点和方向,发射一条射线到场景中。有了它我们就能实现与场景中的对象进行交互的功能。
实现鼠标场景交互
监听鼠标事件
首先绑定click
事件监听
renderer.domElement.addEventListener("click", onClick);
function onClick() {
// ...
}
创建射线投射器
在事件监听函数中,创建射线投射器。即鼠标每次点击场景时,都会根据当时点击的坐标,动态创建射线投射器用于进行相交检测
function onClick(event) {
// 创建射线投射器
const raycaster = new THREE.Raycaster();
// html元素使用的坐标,是以左上角为坐标原点
const px = event.offsetX;
const py = event.offsetY;
// 将html元素坐标px、py转换成WebGL三维空间坐标x、y
// width height为渲染容器宽高
const x = (px / width) * 2 - 1;
const y = -(py / height) * 2 + 1;
// 通过转换后的坐标来设置射线的位置和方向
raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera);
}
需要注意的是这里有一个坐标系转换的过程
射线相交检测
射线投射器Raycaster
提供了intersectObject()
方法,用于检测所有在射线与物体之间的相交关系。返回结果时,相交部分将按距离进行排序,最近的位于第一个。
function onClick(event) {
// 创建射线投射器
const raycaster = new THREE.Raycaster();
// html元素使用的坐标,是以左上角为坐标原点
const px = event.offsetX;
const py = event.offsetY;
// 将html元素坐标px、py转换成WebGL三维空间坐标x、y
// width height为渲染容器宽高
const x = (px / width) * 2 - 1;
const y = -(py / height) * 2 + 1;
// 通过转换后的坐标来设置射线的位置和方向
raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera);
// 将射线与场景内的全部对象进行相交检测
const intersects = raycaster.intersectObjects(scene.children);
}
检测返回的结果intersects
是一个数组,里面包含所有与射线相交的对象,因此可以这样判断本次点击是否有选中模型
function onClick(event) {
// 创建射线投射器
const raycaster = new THREE.Raycaster();
// html元素使用的坐标,是以左上角为坐标原点
const px = event.offsetX;
const py = event.offsetY;
// 将html元素坐标px、py转换成WebGL三维空间坐标x、y
// width height为渲染容器宽高
const x = (px / width) * 2 - 1;
const y = -(py / height) * 2 + 1;
// 通过转换后的坐标来设置射线的位置和方向
raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera);
// 将射线与场景内的全部对象进行相交检测
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
console.log("选中的模型", intersects);
} else {
console.log("本次点击未选中模型");
}
}
在实际使用中往往只把第一个与射线相交的认为是被选中的那个,由于检测结果intersects
是又近到远排序后的数组,因此取出数组的第一项就是被选中的模型
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
// 只关注第一个与射线相交的
console.log("选中的模型", intersects[0]);
} else {
console.log("本次点击未选中模型");
}
在 Three.js 的场景中有不同类型的对象,除了模型对象之外,其他类型的对象会干扰射线的检测结果。因此可以通过添加过滤器的方式,排除干扰项
const intersects = raycaster.intersectObjects(scene.children);
// 过滤掉非模型对象
const meshIntersects = intersects.filter((item) => item.isMesh);
if (meshIntersects.length > 0) {
console.log("选中的模型", meshIntersects[0]);
} else {
console.log("本次点击未选中模型");
}
完整代码如下
function onClick(event) {
// 创建射线投射器
const raycaster = new THREE.Raycaster();
// html元素使用的坐标,是以左上角为坐标原点
const px = event.offsetX;
const py = event.offsetY;
// 将html元素坐标px、py转换成WebGL三维空间坐标x、y
// width height为渲染容器宽高
const x = (px / width) * 2 - 1;
const y = -(py / height) * 2 + 1;
// 通过转换后的坐标来设置射线的位置和方向
raycaster.setFromCamera(new THREE.Vector2(x, y), this.camera);
// 进行射线相交检测
const intersects = raycaster.intersectObjects(scene.children);
// 过滤掉非模型对象
const meshIntersects = intersects.filter((item) => item.isMesh);
if (meshIntersects.length > 0) {
// 只关注第一个与射线相交的
console.log("选中的模型", meshIntersects[0]);
} else {
console.log("本次点击未选中模型");
}
}