Skip to content

射线投射与对象拾取

目的与范围

本文档解释 Three.js 中的射线投射系统,它通过在 3D 场景中投射射线来检测与对象的交点,从而实现空间查询。射线拾取是鼠标拾取、碰撞检测、视线查询和其他空间交互机制的基础。系统由 Raycaster 类、数学射线基元以及利用边界体积优化的对象特定交点算法组成。

有关骨骼动画和网格蒙皮主题,请参阅 5.2

系统架构

射线投射系统遵循委托模式,其中 Raycaster 协调交点测试过程,但各个对象实现自己的 raycast() 方法来根据其几何体类型计算精确交点。

SVG
100%

射线投射系统架构

架构分离关注点:Raycaster 处理射线设置、场景遍历和结果排序,而各个对象类型实现优化的交点测试。边界体积提供早期拒绝以避免昂贵的每三角形单元测试。

Raycaster 类

构造和配置

Raycaster 类定义在 src/core/Raycaster.js13-228 中,提供执行基于射线的空间查询的主要 API。

SVG
100%

Raycaster 类结构

参数配置

params 对象允许每种对象类型的交点配置:

|| 对象类型 | 参数 | 默认值 | 目的 || ||-------------|-----------|---------|---------------------------------------------------|| || Mesh | - | {} | 无可配置参数 || || Line | threshold |1 | 从线到射线的世界单位最大距离 || || Points | threshold |1 | 从点到射线的世界单位最大距离 || || Sprite | - | {} | 无可配置参数 || || LOD | - | {} | 无可配置参数 ||

阈值值使基于容差的交点成为可能,否则很难精确拾取线和点等薄对象。

射线初始化方法

setFromCamera()

将 2D 屏幕坐标转换为用于鼠标拾取的 3D 射线:

SVG
100%

基于摄像机的射线设置

对于透视摄像机,射线起源于摄像机位置并指向反向投影的屏幕坐标。对于正交摄像机,所有射线都是平行的,因此原点在近平面上,方向是摄像机的前向向量。

setFromXRController()

从 WebXR 控制器的世界空间位置和前向方向设置射线:

射线数学

Ray 类(src/math/Ray.js18-472)封装了射线-几何体交点算法。射线由原点和一个归一化方向向量定义。

核心交点方法

SVG
100%

射线交点 API

来源: src/math/Ray.js18-472

三角形交点算法

核心网格交点使用在 src/math/Ray.js297-380 中实现的 Möller-Trumbore 算法。该算法直接计算重心坐标和距离,而无需显式构造平面方程。

算法:

  1. 计算边向量 edge1 = b - aedge2 = c - a
  2. 从叉积计算行列式以检查射线是否平行于三角形
  3. 计算重心坐标 uv
  4. 验证点在三角形内(u >= 0v >= 0u + v <= 1
  5. 沿射线计算距离 t
  6. 如果有效则返回交点

对象特定交点算法

每个可渲染对象类型实现一个 raycast(raycaster, intersects) 方法,该方法计算交点并将结果追加到 intersects 数组。

Mesh 射线投射

Mesh 交点(src/objects/Mesh.js226-405)使用多阶段优化策略:

SVG
100%

Mesh 射线投射管线

两阶段边界体积测试(世界空间中的球体,然后局部空间中的盒体)在昂贵的每三角形单元测试之前提供早期拒绝。对于具有多个材质的网格,算法尊重几何体组,仅对适当材质测试相关三角形单元。

边界体积优化

世界空间中的边界球体测试(src/objects/Mesh.js236-251):

  1. 从几何体计算或检索缓存的边界球体
  2. 使用 matrixWorld 将球体变换到世界空间
  3. 测试射线原点是否在球体内或射线是否与球体相交
  4. 如果无交点则提前退出

局部空间中的边界盒体测试(src/objects/Mesh.js260-264):

  1. 将射线从世界空间变换到局部空间
  2. 使用分离轴定理测试射线-盒体交点
  3. 为细长网格提供比球体更紧密的边界

三角形交点细节

checkGeometryIntersection() 辅助函数(src/objects/Mesh.js440-494):

  1. 顶点检索:调用处理变形目标混合的 getVertexPosition()src/objects/Mesh.js176-218
  2. 面朝向检查:通过测试三角形单元缠绕方向来尊重 material.side(FrontSide、BackSide、DoubleSide)
  3. 重心坐标:使用 Triangle.getBarycoord() 计算三角形单元内的点位置
  4. 属性插值:使用重心权重插值 UV 坐标和法线
  5. 法线方向:如果射线击中背面,则翻转插值法线

Line 射线投射

Line 交点(src/objects/Line.js97-184)测试从射线到线段的距离:

SVG
100%

Line 射线投射算法

线使用基于阈值的方法,因为无限细线将无法拾取。该算法在每个线段上找到最接近射线的点,并测试它是否在阈值距离内。

Points 射线投射

Points 交点(src/objects/Points.js60-160)测试从射线到每个点的距离:

算法:

  1. 测试边界球体以进行早期拒绝
  2. 将射线变换到局部空间
  3. raycaster.params.Points 检索阈值
  4. 对于几何体中的每个点:
    • 测试从射线到点的距离
    • 如果在阈值内,则记录带有 distanceToRay 属性的交点
  5. 返回阈值内的所有点,默认情况下未排序

Sprite 射线投射

Sprite 交点(src/objects/Sprite.js129-202)将 Sprite 视为由两个三角形单元形成的广告牌四边形:

SVG
100%

Sprite 射线投射算法

Sprite 需要特殊处理,因为它们是依赖视图的广告牌。该算法基于摄像机的视图矩阵变换四边形顶点,并测试四边形的两个三角形单元。

交点结果结构

intersectObject()intersectObjects() 方法返回按距离排序的交点对象数组(最近的在前)。每个交点对象具有以下结构:

|| 属性 | 类型 | 描述 || ||---------------|----------|---------------------------------------------------------------|| || distance | number | 从射线原点到交点的距离 || || distanceToRay | number? | 对于 Points:从点到射线的距离(其他类型为 undefined) || || point | Vector3 | 世界坐标中的交点 || || face | Object? | 面定义:{a, b, c, normal, materialIndex} || || faceIndex | number? | 几何体中相交面的索引 || || object | Object3D | 相交的 3D 对象 || || uv | Vector2? | 交点处的 UV 坐标 || || uv1 | Vector2? | 交点处的次级 UV 坐标 || || normal | Vector3? | 交点处的插值法线向量 || || instanceId | number? | 对于 InstancedMesh:相交实例的索引 || || barycoord | Vector3? | 交点的重心坐标 ||

标记有 ? 的属性仅对某些对象类型或配置存在。

场景遍历和图层过滤

intersectObject() 方法(src/core/Raycaster.js194-202)和 intersectObjects() 方法(src/core/Raycaster.js214-226)处理带有可选递归的场景图遍历。

内部 intersect() 函数(src/core/Raycaster.js236-259)实现遍历逻辑:

SVG
100%

带有图层过滤的场景遍历

图层系统允许选择性射线投射。每个对象都有一个 Layers 实例,仅测试图层与 raycaster.layers 重叠的对象。对象可以通过从 raycast() 返回 false 来阻止对其子对象的遍历。

性能特征

优化策略

|| 阶段 | 复杂度 | 优化 || ||----------------------|-------------------------------|--------------------------------|| || 边界球体测试 | O(1) | 世界空间中的早期拒绝 || || 边界盒体测试 | O(1) | 局部空间中的早期拒绝 || || 每三角形单元测试 | O(n) 其中 n = 三角形单元数 | 需要边界体积通道 || || 材质组 | O(m) 其中 m = 组数 | 仅测试相关三角形单元 || || 顶点变形 | O(k) 其中 k = 变形目标 | 按需混合位置 ||

分层边界体积方法将平均情况降低到 O(log n),适用于分布良好的场景,尽管密集几何体的最坏情况仍为 O(n)。

内存效率

射线投射重用在模块范围分配的临时对象以避免垃圾回收压力:

此模式在所有可射线投射对象类型中保持一致。

常用用法模式

鼠标拾取

// 设置(一次)
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

// 在鼠标移动时
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

// 从摄像机投射射线
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);

if (intersects.length > 0) {
    const firstHit = intersects[0];
    console.log('Hit:', firstHit.object.name, 'at', firstHit.point);
}

选择性图层射线投射

// 设置拾取图层
const pickingLayer = 1;
raycaster.layers.set(pickingLayer);

// 仅在交互对象上启用图层
interactiveObject.layers.enable(pickingLayer);

基于阈值的线拾取

// 增加线拾取容差
raycaster.params.Line.threshold = 5; // 5 个世界单位
const intersects = raycaster.intersectObjects(lineObjects);