ThreeJS学习笔记 - 4.射线拾取

666 阅读5分钟

在ThreeJS中,射线拾取(Raycasting)是用来进行鼠标点击或悬停检测、选择场景中的对象的常用技术。射线拾取模型基于几何学中的射线(Ray),通过在三维场景中从一个起点沿着特定方向发射一条射线来检测射线与场景中的物体是否相交

前置知识

在学习射线前,首先得知道两个概念:

  1. HTML的坐标系原点
  2. 归一化设备坐标/标准化设备坐标(Normalized Device Coordinates,简称 NDC)

HTML的坐标系原点

在HTML中,坐标系的原点通常位于元素的左上角,对于整个网页来说,原点是浏览器窗口的左上角

归一化设备坐标

归一化设备坐标是三维计算机图形学中一种用于表示物体位置的坐标系。在WebGL和Three.js中,归一化设备坐标用于将三维空间中的坐标投影到屏幕上的二维平面,且这些坐标被归一化到一个固定的范围内

在ThreeJS中,NDC坐标系的原点始终在对应元素的中心

归一化设备坐标的特点

归一化设备坐标的范围:

  • x 轴:从 -1 到 1(对应元素的左右边界)
  • y 轴:从 -1 到 1(对应元素的上下边界)
  • z 轴:从 -1 到 1(对应场景的前后深度,射线拾取不考虑z轴)

也就是说,在归一化设备坐标中:

  • (x, y) = (-1, -1)​ 对应元素的左下角。
  • (x, y) = (1, 1)​ 对应元素的右上角。
  • z = -1​ 对应近裁剪面(即视口的最前方),z = 1​ 对应远裁剪面(即视口的最远端)。

归一化设备坐标可以确保无论设备的分辨率或窗口大小如何,坐标值始终在统一的范围内,从而使得处理窗口大小变化时,几何体的相对位置和比例保持一致。


射线

射线(Ray)是一条从起点沿某个方向延伸的直线,一般在ThreeJS中用不到 THREE.Ray

// 创建射线对象Ray
const ray = new THREE.Ray()

Ray的常用属性和常用方法

常用属性

  • oringin​:THREE.Vector3

    射线的起点

  • direction​:THREE.Vector3

    射线的方向,direction​通用用一个三维向量 Vector3​ 表示,向量长度保证为1,也就是单位向量。

    // 表示射线沿着x轴正方向
    ray.direction = new THREE.Vector3(1, 0, 0);
    // 表示射线沿着x轴负方向
    ray.direction = new THREE.Vector3(-1, 0, 0);
    // 表示射线沿着y方向
    ray.direction = new THREE.Vector3(0, 1, 0);
    

    注意 direction​ 的值需要是单位向量,不是的话可以执行 normalize()​ 归一化

    ray.direction = new THREE.Vector3(5,0,0).normalize();
    

常用方法

  • intersectTriangle()

    用于三角形射线交叉计算,简单说,就是计算一个射线和一个三角形在3D空间中是否交叉


射线拾取

射线拾取用于检测射线与三维空间中的物体(如网格 THREE.Mesh​)是否相交,通常用于场景交互,例如鼠标点击对象、高亮显示、选择对象等

当我们点击某个物体时,ThreeJS会从相机位置往点击处发射一条射线,然后ThreeJS会自动执行射线穿过物体的算法,并将穿过的物体以数组形式返回

射线拾取步骤

1. 绑定鼠标点击事件

window.addEventListener('click', onMouseClick);

const onMouseClick = (e: MouseEvent) => {
};

2. 将鼠标点击坐标转换为NDC坐标

ThreeJS会将三维场景渲染为二维图像并绘制到HTML页面上的 canvas​ 元素,根据上面的前置知识中我们知道:在HTML中元素的坐标系的原点是在元素的左上角,而在NDC系统中原点位于元素中心。

无标题-2024-10-11-1452.png

所以我们就需要公式将鼠标点击在元素上的坐标通过公式转换成NDC坐标系中的坐标,因为ThreeJS的射线拾取是基于NDC坐标系的。​​

NDC转换公式:

NDC.x = (clientX - canvas.left) / canvas.width * 2 - 1;
NDC.y = -((clientY - canvas.top) / canvas.height) * 2 + 1;

其中:

  • clientX​ 和 clientY​ 是鼠标点击在浏览器视口中的坐标
  • canvas.left​ 和 canvas.top​ 是 canvas​ 元素相对于浏览器视口左边界和顶边界的坐标
  • canvas.width​ 和 canvas.height​ 是 canvas​ 元素的宽和高
  • clientX - canvas.left​ 和 clientY - canvas.top​ 是鼠标点击位置相对于在 canvas​ 元素上的坐标
  • / canvas.width​ 和 / canvas.height​ 目的是像素坐标归一化到 [0, 1]​ 范围

而后面的运算:

  • 对于 NDC.x​ 来说

    * 2​的目的是将范围扩展到 [0, 2]​ 范围

  • 对于 NDC.y​ 来说

    负号是因为NDC坐标系的Y轴方向和HTML坐标系的Y轴方向是相反的(如上面图)

    * 2​ 的目的是将范围扩展到 [-2, 0]​ 范围

  • + 1​ 的目的是将范围平移到 [-1, 1]​ 的范围

本质上后面的运算就是把元素的坐标系的原点转移到NDC坐标系的原点,然后将元素的坐标范围从 [0, 1]​ 映射到 [-1, 1]

代码实现:

const canvas = renderer.value.domElement; // 获取渲染器的canvas元素
const rect = canvas.getBoundingClientRect(); // 获取canvas元素的尺寸和位置

// 创建二维向量
const mouse = new THREE.Vector2(); 
// 将鼠标位置转换为NDC坐标,并赋值给mouse
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
  • 有一个特殊情况,即canvas的大小等于浏览器的视口大小

    因为大小相同,也就代表可以省略计算偏移量的步骤

    const onMouseClick = (e: MouseEvent) => {
      // 创建二维向量
      const mouse = new THREE.Vector2(); 
      // 将鼠标位置转换为NDC坐标,并赋值给mouse
      mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
      mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
    }
    

3. 创建射线

THREE.Raycaster​ 类提供了处理射线拾取的机制。

几个重要的属性和方法:

  • Raycaster.set(origin, direction)​:设置射线的起点(origin)和方向(direction)。
  • Raycaster.intersectObject(object, recursive)​:检测射线与特定对象是否相交,recursive​ 参数指定是否递归检测子对象。
  • Raycaster.intersectObjects(objects, recursive)​:检测射线与多个对象是否相交。
// 创建射线
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);

4. 获取相交的物体并处理

// 计算射线与物体的交点
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
  console.log(intersects);
}

这样就能获取到与射线相交的所有物体了,正常来说数组第一个就是你点击的物体

射线拾取扩展

只拾取想要的元素

比如有4个Mesh:Mesh1、Mesh2、Mesh3、Mesh4

如果只想拾取Mesh2和Mesh3,可以这样做:

const intersects = raycaster.intersectObjects([Mesh2, Mesh3]);