Three.js-硬要自学系列25 (射线Ray、点击选中模型、拾取层级模型、拾取精灵控制场景)

0 阅读4分钟

本章主要学习知识点

  • 了解射线概念
  • 实现如何通过点击选中场景中的模型
  • 掌握如何通过点击拾取层级中的模型
  • 学会如何拾取精灵模型并控制场景中

射线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);

3523.gif

射线应用场景

场景用途
鼠标拾取点击选中模型、按钮交互
碰撞检测判断角色是否碰到障碍物
路径规划检测导航路径上的障碍物
激光效果模拟子弹轨迹或激光武器

点击选中模型

思路原理

  • 屏幕坐标转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')
    }
})

最终效果如下

56.gif

值得注意的点

  • 选中不准确:检查坐标转换是否基于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');       
        }
})

很简单,不是吗,下面是效果

33.gif

拾取精灵控制场景

精灵(Sprite)的拾取与控制场景,可以理解为通过鼠标点击2D精灵对象(如文字标签、图标),触发3D场景的交互控制(如开关灯光、切换视角)。

回忆下如何创建精灵模型,在场景中创建一个精灵模型

image.png

创建射线,检查点击时是否与模型相交,如果相交,则改变精灵模型对象的材质颜色和场景背景色

const intersects = raycaster.intersectObjects([sprite]);

if(intersects.length > 0) { 
    intersects[0].object.material.color.set('deepskyblue')
    // 改变场景背景色
    scene.background = new THREE.Color('deepskyblue')
}

效果如下

53.gif

以上案例均可在案例中心查看体验

THREE 案例中心

image.png