Three.js 射线拾取原理:像素世界的侦探故事

想象你在画廊里欣赏一幅 3D 立体画,突然想知道鼻尖正对着的是哪片云彩 ------ 在数字世界里,这个动作就叫 "射线拾取"。Three.js 就像一位训练有素的侦探,能顺着你的目光(鼠标点击)在三维迷宫中找到那个被选中的物体。今天我们就来拆解这位侦探的破案手法,从像素到矩阵,揭开射线拾取的底层密码。

从屏幕点击到三维射线:坐标的跨界旅行

当你在屏幕上点击鼠标时,浏览器会告诉你一个二维坐标:(clientX, clientY)。这就像在说 "我在电影院第 5 排第 3 座",但 Three.js 需要的是 "在宇宙中哪个星球正好在这条视线延长线上"。这个转换过程,需要三场精妙的坐标变换。

首先,我们要把屏幕坐标转换成标准化设备坐标(NDC)。这一步相当于把任何尺寸的屏幕都映射到一个边长为 2 的立方体里,左下角是 (-1,-1),右上角是 (1,1)。就像无论电影院多大,都统一换算成 "从左到右占 0.3,从前到后占 0.5" 的相对位置。

arduino 复制代码
// 把屏幕坐标转换为标准化设备坐标
function getNDC(clientX, clientY, canvas) {
  const rect = canvas.getBoundingClientRect();
  const x = (clientX - rect.left) / rect.width * 2 - 1;
  const y = -(clientY - rect.top) / rect.height * 2 + 1;
  return { x, y };
}

注意 y 轴的计算多了个负号,这是因为在屏幕坐标系里 y 轴向下为正,而在 Three.js 的世界里 y 轴向上为正 ------ 就像两个国家用不同的行车方向,需要一个交通规则转换器。

视图矩阵与投影矩阵:三维世界的哈哈镜

现在我们有了标准化设备坐标,但这还只是在 "镜子外面" 观察。要进入三维世界,必须经过两个关键的矩阵转换:视图矩阵(Camera.matrixWorldInverse)和投影矩阵(Camera.projectionMatrix)。

视图矩阵就像你的视角转换 ------ 当你歪着头看物体时,世界在你眼中会倾斜,这个矩阵记录了这种倾斜关系。投影矩阵则像哈哈镜:正交投影是平面镜,保持物体真实比例;透视投影是漏斗镜,远处的物体看起来更小。

javascript 复制代码
// 创建射线投射器
const raycaster = new THREE.Raycaster();
// 更新射线:从相机位置到点击点
function updateRay(camera, mouse) {
  // 这行代码背后正在进行矩阵乘法的魔法
  raycaster.setFromCamera(mouse, camera);
}

setFromCamera方法内部正在执行一个精彩的数学舞蹈:它先将标准化设备坐标扩展成齐次坐标(x,y,1,1),然后用投影矩阵的逆矩阵乘以这个坐标,得到观察空间中的方向向量,最后再用视图矩阵的逆矩阵转换到世界空间。简单说,就是把 "镜子里的像" 还原成 "真实物体" 的位置。

射线与物体的碰撞检测:三维世界的撞球游戏

有了从相机出发的射线,接下来就是检查这条射线与哪些物体相交。这就像在漆黑的房间里挥舞一根竹竿,通过触碰感判断碰到了什么家具。

Three.js 采用的是分层检测策略,就像安检过程:

  1. 边界盒检测:先快速检查射线是否与物体的包围盒相交,排除明显不相关的物体 ------ 相当于先看包裹大小是否可疑
  1. 几何体面检测:对通过第一关的物体,检查射线是否与它的任何一个三角形面相交 ------ 相当于打开包裹仔细检查内容
javascript 复制代码
// 检测射线与场景中物体的交点
function detectIntersections(raycaster, scene) {
  // 获取所有相交对象,按距离排序
  const intersects = raycaster.intersectObjects(scene.children, true);
  
  if (intersects.length > 0) {
    console.log(`命中目标:${intersects[0].object.name}`);
    // 最近的交点距离
    console.log(`距离:${intersects[0].distance.toFixed(2)}单位`);
  }
}

这里的intersectObjects方法会返回一个交点数组,按距离相机的远近排序。想象一下这是雷达扫描结果,最近的目标会排在最前面。每个交点对象还包含了精确的撞击位置(point)、所在的三角形索引(faceIndex)等细节,就像事故报告里记录的 "撞击点坐标北纬 30 度,东经 120 度"。

三角形相交算法:射线与平面的爱情故事

为什么 Three.js 对三角形情有独钟?因为任何复杂的 3D 模型最终都能分解成无数个三角形 ------ 就像数字世界的原子。判断射线是否与三角形相交,用的是著名的 Möller-Trumbore 算法,这个算法就像月老牵线,通过计算射线与三角形是否 "有缘相会" 来判定相交。

简单来说,算法会做三件事:

  • 检查射线是否与三角形所在平面相交
  • 检查交点是否在三角形内部(通过 barycentric 坐标判断,类似 "点是否在三角形内" 的几何问题)
  • 计算交点距离射线起点的距离,用于排序

性能优化:射线拾取的高速公路

当场景中有成千上万个物体时,逐个检测会让浏览器累得气喘吁吁。Three.js 提供了几个加速技巧:

  1. 层级检测:intersectObjects的第二个参数设置为 true 时,会递归检测子物体,否则只检测顶层物体 ------ 相当于只查快递外包装还是拆开所有盒子
  1. 射线范围限制:设置raycaster.near和raycaster.far可以限定检测范围,就像只在 100 米内搜索目标
  1. 空间分区:配合 Three.js 的 Octree 等空间数据结构,能大幅减少检测数量 ------ 相当于在图书馆按分类查找书籍
ini 复制代码
// 优化射线检测范围
raycaster.near = 0.1; // 忽略10厘米内的物体(避免相机自己)
raycaster.far = 1000; // 只检测1000米内的物体

完整的射线拾取流程:从点击到响应

把所有步骤串联起来,就构成了完整的射线拾取流程:

ini 复制代码
// 初始化
const canvas = renderer.domElement;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// 监听鼠标点击
canvas.addEventListener('click', onCanvasClick);
function onCanvasClick(event) {
  // 1. 计算标准化设备坐标
  const rect = canvas.getBoundingClientRect();
  mouse.x = (event.clientX - rect.left) / rect.width * 2 - 1;
  mouse.y = -(event.clientY - rect.top) / rect.height * 2 + 1;
  
  // 2. 更新射线
  raycaster.setFromCamera(mouse, camera);
  
  // 3. 检测相交
  const intersects = raycaster.intersectObjects(scene.children, true);
  
  // 4. 处理结果
  if (intersects.length > 0) {
    // 给选中的物体添加高亮效果
    intersects[0].object.material.color.set(0xff0000);
  }
}

底层原理总结:像素侦探的工作手册

回顾整个过程,射线拾取本质上是在解决一个几何问题:在三维空间中,找到与从相机出发、穿过屏幕点击点的射线相交的物体。这个过程涉及:

  • 坐标系统的转换(屏幕坐标→标准化设备坐标→世界坐标)
  • 矩阵运算(逆矩阵用于还原世界空间)
  • 几何相交检测(射线与三角形的碰撞计算)
  • 性能优化策略(范围限制、层级检测等)

下次当你在 Three.js 场景中点击物体时,不妨想象一下这个过程:你的点击穿过层层矩阵的哈哈镜,化作一道无形的射线,在三角形构成的数字森林中穿梭,最终找到那个与你 "对视" 的物体。这就是像素世界的浪漫 ------ 每一次点击,都是一次跨越维度的握手。

相关推荐
像风一样自由20202 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem3 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊3 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术3 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing3 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止3 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall3 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴3 小时前
简单入门Python装饰器
前端·python
袁煦丞4 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作
天天扭码4 小时前
从图片到语音:我是如何用两大模型API打造沉浸式英语学习工具的
前端·人工智能·github