Three.js Raycaster(射线投射器)鼠标点击选中模型笔记
一、Raycaster 简介
Raycaster(射线投射器)是Three.js中用于实现**鼠标拾取(3D对象选择)**的核心类。它通过从相机位置发射一条射线,检测与哪些3D对象相交,从而实现点击选中功能。
二、实现步骤详解
1. 监听点击事件
javascript
addEventListener("click", (e) => {
// 获取鼠标在浏览器窗口中的像素坐标
const px = e.offsetX; // 鼠标X坐标
const py = e.offsetY; // 鼠标Y坐标
});
2. 坐标转换(关键步骤)
将屏幕像素坐标转换为Three.js的归一化设备坐标(NDC)
- NDC范围:X轴[-1, 1],Y轴[-1, 1],原点在屏幕中心
- 屏幕坐标范围:X轴[0, window.innerWidth],Y轴[0, window.innerHeight],原点在左上角
javascript
const x = (px / window.innerWidth) * 2 - 1; // 转换到[-1, 1]
const y = -(py / window.innerHeight) * 2 + 1; // 转换到[-1, 1],Y轴需要取反
3. 创建并设置Raycaster
javascript
// 创建射线投射器
const raycaster = new THREE.Raycaster();
// 设置射线的起点和方向
// setFromCamera()方法:从相机位置发射射线,方向指向鼠标点击的3D空间点
raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
4. 检测相交物体
javascript
// intersectObjects()方法检测射线与哪些物体相交
// 参数:要检测的物体数组
const intersects = raycaster.intersectObjects([cube, cube2, cube3]);
// intersects返回一个数组,包含所有相交的物体信息,按距离从近到远排序
5. 处理选中结果
javascript
if (intersects.length > 0) {
// 获取第一个(最近)相交的物体
const selectedObject = intersects[0].object;
// 修改物体材质颜色
selectedObject.material.color.set(Math.random() * 0xffffff);
}
console.log(intersects); // 查看详细的相交信息
三、intersects 对象详解
intersects数组中的每个元素包含以下属性:
javascript
{
distance: 距离相机的距离,
point: 相交点的3D坐标(Vector3),
face: 相交的面(Face3),
faceIndex: 面的索引,
object: 相交的3D对象,
uv: 纹理坐标
}
四、完整代码流程
- 事件监听 → 监听鼠标点击事件
- 坐标获取 → 获取鼠标在屏幕上的像素位置
- 坐标转换 → 像素坐标 → 归一化设备坐标(NDC)
- 射线创建 → 实例化Raycaster
- 射线设置 → 从相机向鼠标点击的3D方向发射射线
- 相交检测 → 检测射线与指定物体的交点
- 结果处理 → 处理选中的物体(如改变颜色)
五、注意事项
- 坐标转换:Y轴需要取反,因为屏幕坐标原点在左上角,而Three.js坐标原点在中心
- 性能优化 :如果场景中有大量物体,应避免检测所有物体,可以:
- 分组检测
- 使用BVH(包围盒层次结构)加速
- 多物体选择 :
intersects数组包含所有相交物体,可以支持多选功能 - 相机类型:适用于PerspectiveCamera(透视相机)和OrthographicCamera(正交相机)
六、实际应用场景
- 3D编辑器中的对象选择
- 游戏中的物品拾取
- 交互式3D可视化
- VR/AR中的手势选择
七、示例代码片段
javascript
// 简化的Raycaster使用示例
function onMouseClick(event) {
// 1. 获取鼠标位置
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
// 2. 创建射线
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
// 3. 检测相交
const intersects = raycaster.intersectObjects(scene.children);
// 4. 处理结果
if (intersects.length > 0) {
const selectedObject = intersects[0].object;
// 执行选中操作...
}
}
<template>
<div class="container" ref="containerRef"></div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const containerRef = ref(null);
// 创建3D场景对象Scene
const scene = new THREE.Scene();
// 实例化一个透视投影相机对象
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// 实例化一个WebGL渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
//设置Canvas画布尺寸.setSize()
renderer.setSize(window.innerWidth, window.innerHeight);
//创建一个长方体几何对象Geometry
const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
//材质Material
const material = new THREE.MeshLambertMaterial({
color: 0x0000ff, //设置材质颜色
});
//物体:网格模型Mesh
const cube = new THREE.Mesh(geometry, material);
//设置网格模型在三维空间中的位置坐标,默认是坐标原点
cube.position.set(0, 0, 0);
//在threejs中你创建了一个表示物体的虚拟对象Mesh,需要通过.add()方法,把网格模型mesh添加到三维场景scene中。
scene.add(cube);
//再创建两个立方体
const geometry2 = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const material2 = new THREE.MeshLambertMaterial({
color: 0x00ff00,
});
const cube2 = new THREE.Mesh(geometry2, material2);
cube2.position.set(0.5, 0.5, 0.5);
scene.add(cube2);
const geometry3 = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const material3 = new THREE.MeshLambertMaterial({
color: 0xff0000,
});
const cube3 = new THREE.Mesh(geometry3, material3);
cube3.position.set(0.5, -0.5, -0.5);
scene.add(cube3);
//环境光
const AmbientLight = new THREE.AmbientLight(0xffffff, 0.4) // 创建环境光
scene.add(AmbientLight) // 将环境光添加到场景
//Directional Light
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
// 设置光源的方向:通过光源position属性和目标指向对象的position属性计算
directionalLight.position.set(100, 75, 30);
// 方向光指向对象网格模型mesh,可以不设置,默认的位置是0,0,0
scene.add(directionalLight);
//相机在Three.js三维坐标系中的位置
camera.position.set(0, 0, 2);
camera.lookAt(0, 0, 2);
const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);
function animate() {
requestAnimationFrame(animate);
//.x是围绕X转
cube.rotation.x += 0.01;
//.y是围绕X转
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
addEventListener("click", (e) => {
// 获取鼠标位置
const px = e.offsetX
const py = e.offsetY
// 转换坐标 归一化设备坐标NDC
const x= (px / window.innerWidth) * 2 - 1
const y= -(py / window.innerHeight) * 2 + 1
// 创建射线投射器 Raycaster
const raycaster = new THREE.Raycaster()
// 设置射线投射位置和方向
raycaster.setFromCamera(new THREE.Vector2(x,y), camera)
// 获取射线选中的物体
const intersects = raycaster.intersectObjects([cube, cube2, cube3])
if(intersects.length > 0) {
// 改变颜色
intersects[0].object.material.color.set(Math.random() * 0xffffff)
}
console.log(intersects)
});
// 挂载完毕 获取dom
onMounted(() => {
// 相机控间
const contros = new OrbitControls(camera, containerRef.value);
contros.enableDamping = true;
containerRef.value.appendChild(renderer.domElement);
});
</script>