本章主要学习知识点
- 了解射线概念
- 实现如何通过点击选中场景中的模型
- 掌握如何通过点击拾取层级中的模型
- 学会如何拾取精灵模型并控制场景中
射线Ray
射线(Ray) 就像一根虚拟的“激光笔”,用于检测三维空间中点、线和物体之间的相交关系。它是实现交互(如点击选中物体)、碰撞检测等功能的数学工具
射线通过起点(origin
)和方向(direction
)两个向量定义,其类似于激光笔发射的光线,有起点(如相机位置)和无限延伸的方向
Three.js 中封装了射线操作的Raycaster
类,使得操作射线简洁方便,下面让我们创建一个简单的示例
创建射线拾取器
const raycaster = new THREE.Raycaster();
raycaster.ray.origin.set(0, 0, 0); // 射线起点
raycaster.ray.direction.set(30, 30, 30); // 射线方向
为了可视化看见射线,这里我们创建一条蓝色的线条,用来表示射线,同时这里创建了3个小球,通过gui实时改变射线位置观察小球变化
const lineGeometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
0, 0, 0,
30, 30, 30
])
lineGeometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
const lineMaterial = new THREE.LineBasicMaterial({ color: 'deepskyblue' });
const line = new THREE.Line(lineGeometry, lineMaterial);
scene.add(line);
const geometry = new THREE.SphereGeometry(1, 32, 32);
const material = new THREE.MeshLambertMaterial({ color: 0x00ff00 });
const mesh1 = new THREE.Mesh(geometry, material);
const mesh2 = mesh1.clone();
mesh2.position.y = 5;
const mesh3 = mesh1.clone();
mesh3.position.x = 5;
const model = new THREE.Group();
model.add(mesh1, mesh2, mesh3);
model.updateMatrixWorld(true);
scene.add(model);
射线应用场景
场景 | 用途 |
---|---|
鼠标拾取 | 点击选中模型、按钮交互 |
碰撞检测 | 判断角色是否碰到障碍物 |
路径规划 | 检测导航路径上的障碍物 |
激光效果 | 模拟子弹轨迹或激光武器 |
点击选中模型
思路原理
- 屏幕坐标转3D坐标:浏览器中的鼠标点击位置是二维坐标,需转换为Three.js 的三维空间坐标
- 发射探测射线:根据点击位置和相机参数,生成一条虚拟射线
- 碰撞检测:检测射线与场景中模型的交叉点,找出被点击的物体
创建10个立方体,随机放置在场景中的不同位置上
for (let i = 0; i < 10; i++) {
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff });
const cube = new THREE.Mesh(geometry, material);
cube.position.x = Math.random() * 10 - 5;
cube.position.y = Math.random() * 10 - 5;
cube.position.z = Math.random() * 10 - 5;
meshArr.push(cube)
scene.add(cube);
}
监听鼠标点击事件,并获取点击位置(基于canvas画布坐标)
window.addEventListener('click', (event) => {
const px = event.offsetX;
const py = event.offsetY;
})
接下来就是很关键的坐标转换了
// 通过鼠标点击位置计算出raycaster所需要的点的位置,以屏幕中心为原点,值的范围为-1到1.
const x = ( px / window.innerWidth ) * 2 - 1;
const y = - ( py / window.innerHeight ) * 2 + 1;
做好前面的工作后,就可以创建射线进行碰撞检测
window.addEventListener('click', (event) => {
const px = event.offsetX;
const py = event.offsetY;
// 通过鼠标点击位置计算出raycaster所需要的点的位置,以屏幕中心为原点,值的范围为-1到1.
const x = ( px / window.innerWidth ) * 2 - 1;
const y = - ( py / window.innerHeight ) * 2 + 1;
// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
const raycaster = new THREE.Raycaster();
// 设置射线从相机发出
raycaster.setFromCamera( {x, y}, camera );
// 获取射线与物体数组相交的物体
const intersects = raycaster.intersectObjects( meshArr );
// 如果有相交的物体
if(intersects.length > 0) {
// 将相交的物体的颜色设置为deeppink
intersects[0].object.material.color.set('deeppink')
}
})
最终效果如下
值得注意的点
- 选中不准确:检查坐标转换是否基于canvas的实际分辨率
- 模型层级问题:若模型由多部分组成,需递归检测所有子对象
- 性能优化:复杂场景可将静态模型排除在检测列表外
拾取层级模型
层级模型拾取 可以理解为用虚拟“激光笔”穿透父级模型,精准识别最里层的子物体。
这里导入一个嵌套的城市模型,并将其置于场景中心位置
loader.load('model/low_poly_city_pack/scene.glb', function( gltf ) {
// 递归遍历设置每个模型的材质和边线
gltf.scene.traverse( function ( child ) {
if( child.isMesh ){
models.push( child );
}
})
parentModel = gltf.scene;
scene.add( gltf.scene );
// 设置模型处于中心位置
// 计算模型的边界盒
const box = new THREE.Box3().setFromObject( gltf.scene );
// 计算边界盒的中心点
const center = new THREE.Vector3();
box.getCenter( center );
gltf.scene.position.x += (gltf.scene.position.x - center.x);
gltf.scene.position.z += (gltf.scene.position.z - center.z);
});
监听鼠标点击,并注意计算鼠标点击位置在画布上的位置,通过intersectObjects
设置true参数让射线递归检测所有子对象
window.addEventListener('click', function (event) {
// 计算鼠标点击位置在画布上的位置
const x = (event.clientX / window.innerWidth) * 2 - 1;
const y = -(event.clientY / window.innerHeight) * 2 + 1;
// 创建一个射线
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
const intersects = raycaster.intersectObject(parentModel,true);
if (intersects.length > 0) {
const selectedObject = intersects[0].object;
// 设置模型发光
outlinePass.selectedObjects = [selectedObject];
// 设置模型边线
selectedObject.material.color.set('deeppink');
}
})
很简单,不是吗,下面是效果
拾取精灵控制场景
精灵(Sprite)的拾取与控制场景,可以理解为通过鼠标点击2D精灵对象(如文字标签、图标),触发3D场景的交互控制(如开关灯光、切换视角)。
回忆下如何创建精灵模型,在场景中创建一个精灵模型
创建射线,检查点击时是否与模型相交,如果相交,则改变精灵模型对象的材质颜色和场景背景色
const intersects = raycaster.intersectObjects([sprite]);
if(intersects.length > 0) {
intersects[0].object.material.color.set('deepskyblue')
// 改变场景背景色
scene.background = new THREE.Color('deepskyblue')
}
效果如下