ThreeJS中的光线投射(Raycasting)是实现3D场景交互的核心技术。简单来说,它就像在3D空间中发射一束虚拟激光,检测这束光线与哪些物体发生了碰撞。这种机制在游戏开发、VR/AR交互、数据可视化等领域应用广泛。
光线投射的数学原理基于向量运算。当我们在屏幕上点击时,ThreeJS会将2D屏幕坐标转换为3D场景中的射线。这个转换过程涉及相机矩阵的逆变换和投影矩阵的计算。具体实现时,Raycaster类会处理以下关键参数:
实际开发中常见误区是忘记更新射线方向。当相机移动或旋转后,必须重新计算raycaster.ray属性,否则检测结果会出错。
实现基础物体交互需要五个关键步骤:
javascript复制const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
javascript复制function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
javascript复制function checkIntersection() {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
// 处理相交物体
}
}
javascript复制window.addEventListener('mousemove', onMouseMove, false);
javascript复制function animate() {
requestAnimationFrame(animate);
checkIntersection();
renderer.render(scene, camera);
}
基础实现存在性能瓶颈,当场景物体过多时会出现卡顿。以下是三个优化方案:
javascript复制import { Octree } from 'three/examples/jsm/math/Octree';
const octree = new Octree();
octree.fromGraphNode(scene);
javascript复制const interactiveObjects = new THREE.Group();
scene.add(interactiveObjects);
// 只检测特定组
raycaster.intersectObjects(interactiveObjects.children, true);
我们通过一个电商3D展示案例演示高级交互。该案例需要实现:
关键实现代码:
javascript复制let currentIntersect = null;
function handleIntersection(intersects) {
if (intersects.length > 0) {
if (currentIntersect !== intersects[0].object) {
// 鼠标移入新物体
intersects[0].object.material.emissive.setHex(0x888888);
currentIntersect = intersects[0].object;
}
} else if (currentIntersect) {
// 鼠标移出物体
currentIntersect.material.emissive.setHex(0x000000);
currentIntersect = null;
}
}
使用stats.js监控性能:
javascript复制import Stats from 'three/examples/jsm/libs/stats.module';
const stats = new Stats();
document.body.appendChild(stats.dom);
function animate() {
stats.update();
// ...其他动画逻辑
}
优化前后性能对比:
| 优化措施 | 帧率提升 | 内存占用降低 |
|---|---|---|
| Octree空间分割 | 45% | 30% |
| 分层检测策略 | 60% | 15% |
| 交互物体分组 | 25% | 50% |
javascript复制// 提高检测精度
raycaster.params = {
Line: { threshold: 0.1 },
Points: { threshold: 5 }
};
javascript复制// 触摸事件处理
function onTouchStart(event) {
event.preventDefault();
mouse.x = (event.touches[0].clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.touches[0].clientY / window.innerHeight) * 2 + 1;
}
javascript复制raycaster.params.Points.threshold = 0.5;
material.alphaTest = 0.5;
javascript复制// 每帧更新物体矩阵
object.updateMatrixWorld();
javascript复制const raycaster1 = new THREE.Raycaster();
const raycaster2 = new THREE.Raycaster();
function handleTouches(touches) {
// 为每个触摸点创建独立射线
}
使用GPU加速的射线检测可以大幅提升性能。通过自定义着色器实现:
glsl复制// 片段着色器中检测碰撞
uniform vec3 rayOrigin;
uniform vec3 rayDirection;
void main() {
vec3 p = position;
vec3 r = rayDirection;
vec3 s = rayOrigin;
float t = dot(p - s, p - s) / dot(p - s, r);
vec3 projection = s + t * r;
if (distance(projection, p) < threshold) {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
}
不同空间分割方案性能对比:
| 数据结构 | 构建时间 | 查询速度 | 适用场景 |
|---|---|---|---|
| BVH | 中 | 快 | 静态场景 |
| Octree | 慢 | 中 | 动态场景 |
| k-d树 | 快 | 慢 | 点云数据 |
实现示例:
javascript复制import { MeshBVH } from 'three-mesh-bvh';
geometry.boundsTree = new MeshBVH(geometry);
VR设备需要特殊处理:
javascript复制// 控制器射线检测
controller.addEventListener('selectstart', (event) => {
const raycaster = new THREE.Raycaster();
raycaster.setFromController(event);
const intersects = raycaster.intersectObjects(scene.children);
// 处理交互
});
结合Cannon.js实现物理反馈:
javascript复制import * as CANNON from 'cannon-es';
const world = new CANNON.World();
const physicsBody = new CANNON.Body({ mass: 0 });
// 同步ThreeJS物体与物理体
function updatePhysics() {
object.position.copy(physicsBody.position);
object.quaternion.copy(physicsBody.quaternion);
}
在复杂项目中,我习惯将交互系统分为三层架构:表现层(ThreeJS渲染)、逻辑层(交互处理)、数据层(状态管理)。这种架构虽然初期开发成本略高,但后期维护和扩展会轻松很多。特别是在需要支持多平台时,只需替换表现层实现即可。