射线投射与对象拾取
目的与范围
本文档解释 Three.js 中的射线投射系统,它通过在 3D 场景中投射射线来检测与对象的交点,从而实现空间查询。射线拾取是鼠标拾取、碰撞检测、视线查询和其他空间交互机制的基础。系统由 Raycaster 类、数学射线基元以及利用边界体积优化的对象特定交点算法组成。
有关骨骼动画和网格蒙皮主题,请参阅 5.2。
系统架构
射线投射系统遵循委托模式,其中 Raycaster 协调交点测试过程,但各个对象实现自己的 raycast() 方法来根据其几何体类型计算精确交点。
射线投射系统架构
架构分离关注点:Raycaster 处理射线设置、场景遍历和结果排序,而各个对象类型实现优化的交点测试。边界体积提供早期拒绝以避免昂贵的每三角形单元测试。
Raycaster 类
构造和配置
Raycaster 类定义在 src/core/Raycaster.js13-228 中,提供执行基于射线的空间查询的主要 API。
Raycaster 类结构
参数配置
params 对象允许每种对象类型的交点配置:
|| 对象类型 | 参数 | 默认值 | 目的 || ||-------------|-----------|---------|---------------------------------------------------|| || Mesh | - | {} | 无可配置参数 || || Line | threshold |1 | 从线到射线的世界单位最大距离 || || Points | threshold |1 | 从点到射线的世界单位最大距离 || || Sprite | - | {} | 无可配置参数 || || LOD | - | {} | 无可配置参数 ||
阈值值使基于容差的交点成为可能,否则很难精确拾取线和点等薄对象。
射线初始化方法
setFromCamera()
将 2D 屏幕坐标转换为用于鼠标拾取的 3D 射线:
基于摄像机的射线设置
对于透视摄像机,射线起源于摄像机位置并指向反向投影的屏幕坐标。对于正交摄像机,所有射线都是平行的,因此原点在近平面上,方向是摄像机的前向向量。
setFromXRController()
从 WebXR 控制器的世界空间位置和前向方向设置射线:
射线数学
Ray 类(src/math/Ray.js18-472)封装了射线-几何体交点算法。射线由原点和一个归一化方向向量定义。
核心交点方法
射线交点 API
三角形交点算法
核心网格交点使用在 src/math/Ray.js297-380 中实现的 Möller-Trumbore 算法。该算法直接计算重心坐标和距离,而无需显式构造平面方程。
算法:
- 计算边向量
edge1 = b - a和edge2 = c - a - 从叉积计算行列式以检查射线是否平行于三角形
- 计算重心坐标
u和v - 验证点在三角形内(
u >= 0、v >= 0、u + v <= 1) - 沿射线计算距离
t - 如果有效则返回交点
对象特定交点算法
每个可渲染对象类型实现一个 raycast(raycaster, intersects) 方法,该方法计算交点并将结果追加到 intersects 数组。
Mesh 射线投射
Mesh 交点(src/objects/Mesh.js226-405)使用多阶段优化策略:
Mesh 射线投射管线
两阶段边界体积测试(世界空间中的球体,然后局部空间中的盒体)在昂贵的每三角形单元测试之前提供早期拒绝。对于具有多个材质的网格,算法尊重几何体组,仅对适当材质测试相关三角形单元。
边界体积优化
世界空间中的边界球体测试(src/objects/Mesh.js236-251):
- 从几何体计算或检索缓存的边界球体
- 使用
matrixWorld将球体变换到世界空间 - 测试射线原点是否在球体内或射线是否与球体相交
- 如果无交点则提前退出
局部空间中的边界盒体测试(src/objects/Mesh.js260-264):
- 将射线从世界空间变换到局部空间
- 使用分离轴定理测试射线-盒体交点
- 为细长网格提供比球体更紧密的边界
三角形交点细节
checkGeometryIntersection() 辅助函数(src/objects/Mesh.js440-494):
- 顶点检索:调用处理变形目标混合的
getVertexPosition()(src/objects/Mesh.js176-218) - 面朝向检查:通过测试三角形单元缠绕方向来尊重
material.side(FrontSide、BackSide、DoubleSide) - 重心坐标:使用
Triangle.getBarycoord()计算三角形单元内的点位置 - 属性插值:使用重心权重插值 UV 坐标和法线
- 法线方向:如果射线击中背面,则翻转插值法线
Line 射线投射
Line 交点(src/objects/Line.js97-184)测试从射线到线段的距离:
Line 射线投射算法
线使用基于阈值的方法,因为无限细线将无法拾取。该算法在每个线段上找到最接近射线的点,并测试它是否在阈值距离内。
Points 射线投射
Points 交点(src/objects/Points.js60-160)测试从射线到每个点的距离:
算法:
- 测试边界球体以进行早期拒绝
- 将射线变换到局部空间
- 从
raycaster.params.Points检索阈值 - 对于几何体中的每个点:
- 测试从射线到点的距离
- 如果在阈值内,则记录带有
distanceToRay属性的交点
- 返回阈值内的所有点,默认情况下未排序
Sprite 射线投射
Sprite 交点(src/objects/Sprite.js129-202)将 Sprite 视为由两个三角形单元形成的广告牌四边形:
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)实现遍历逻辑:
带有图层过滤的场景遍历
图层系统允许选择性射线投射。每个对象都有一个 Layers 实例,仅测试图层与 raycaster.layers 重叠的对象。对象可以通过从 raycast() 返回 false 来阻止对其子对象的遍历。
性能特征
优化策略
|| 阶段 | 复杂度 | 优化 || ||----------------------|-------------------------------|--------------------------------|| || 边界球体测试 | O(1) | 世界空间中的早期拒绝 || || 边界盒体测试 | O(1) | 局部空间中的早期拒绝 || || 每三角形单元测试 | O(n) 其中 n = 三角形单元数 | 需要边界体积通道 || || 材质组 | O(m) 其中 m = 组数 | 仅测试相关三角形单元 || || 顶点变形 | O(k) 其中 k = 变形目标 | 按需混合位置 ||
分层边界体积方法将平均情况降低到 O(log n),适用于分布良好的场景,尽管密集几何体的最坏情况仍为 O(n)。
内存效率
射线投射重用在模块范围分配的临时对象以避免垃圾回收压力:
- src/objects/Mesh.js12-14 中的
_inverseMatrix、_ray、_sphere - src/objects/Mesh.js17-19 中用于三角形单元顶点的
_vA、_vB、_vC - src/objects/Mesh.js24-25 中的
_intersectionPoint、_intersectionPointWorld
此模式在所有可射线投射对象类型中保持一致。
常用用法模式
鼠标拾取
// 设置(一次)
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);