Skip to content

射线拾取模型

射线 Ray

以一个点作为起点, 沿着某个方向无限延伸

1.创建射线对象 Ray

js
const ray = new THREE.Ray();

2.设置射线起点.origin

js
ray.origin = new THREE.Vector3(1, 0, 3);
// set方法
ray.origin.set(1, 0, 3);

3.射线方向.direction

为三维单位向量, 向量长度保证为 1

js
// 表示射线沿着x轴正方向
ray.direction = new THREE.Vector3(1, 0, 0);
// 表示射线沿着y方向
ray.direction = new THREE.Vector3(0, 1, 0);

不是单位向量要执行.normalize()归一化

js
ray.direction = new THREE.Vector3(5, 0, 0).normalize();

4.intersectTriangle()方法

计算一个射线和一个三角形在 3D 空间中是否交叉

js
// 三角形三个点坐标
const p1 = new THREE.Vector3(100, 25, 0);
const p2 = new THREE.Vector3(100, -25, 25);
const p3 = new THREE.Vector3(100, -25, -25);
const point = new THREE.Vector3(); //用来记录射线和三角形的交叉点
// 相交返回交点,不相交返回null
const result = ray.intersectTriangle(p1, p2, p3, false, point);
console.log("交叉点坐标", point);
console.log("查看是否相交", result);
  • 三角形有正反两面, 按顶点顺序顺时针表示背面, 逆时针表示正面
  • 参数 4 设为 true 时执行背面剔除, 即射线从图形背面穿过来, 视为交叉无效
js
const r = ray.intersectTriangle(p1, p2, p3, true, point);

射线拾取模型(Raycaster)

1.射线投射器

js
const raycaster = new THREE.Raycaster();
// 设置射线起点
raycaster.ray.origin = new THREE.Vector3(-100, 0, 0);
// 设置射线方向射线方向沿着x轴
raycaster.ray.direction = new THREE.Vector3(1, 0, 0);

2.射线交叉计算

js
const raycaster = new THREE.Raycaster();
raycaster.ray.origin = new THREE.Vector3(-100, 0, 0);
raycaster.ray.direction = new THREE.Vector3(1, 0, 0);
// 射线发射拾取模型对象
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
console.log("射线器返回的对象", intersects);

射线返回对象是一个包含多个信息的数组, 当数组长度>0 时说明选中了模型

js
if (intersects.length > 0) {
  console.log("交叉点坐标", intersects[0].point);
  console.log("交叉对象", intersects[0].object);
  console.log("射线原点和交叉点距离", intersects[0].distance);
}

选中的对象模型可以被获取并修改

js
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
if (intersects.length > 0) {
  // 选中模型的第一个模型,设置为红色
  intersects[0].object.material.color.set(0xff0000);
}

屏幕坐标转化为设备坐标

屏幕坐标

通过 js 原生事件获取鼠标点击屏幕的坐标位置,clientoffset 区别在于坐标原点位置不同 屏幕坐标

js
addEventListener("click", function (event) {
  const px = event.offsetX;
  const py = event.offsetY;
  // client
  const cx = event.clientX;
  const cy = event.clientY;
});

设备坐标

以 Canvas 画布中心为坐标原点,坐标值为相对坐标,范围在[-1,1]之间

标准设备坐标

坐标转换

  • offsetXoffsetY
js
addEventListener("click", function (event) {
  const px = event.offsetX;
  const py = event.offsetY;
  //屏幕坐标px、py转标准设备坐标x、y
  //width、height表示canvas画布宽高度
  const x = (px / width) * 2 - 1;
  const y = -(py / height) * 2 + 1;
});
  • clientXclientY
js
addEventListener("click", function (event) {
  // left、top表示canvas画布布局,距离顶部和左侧的距离(px)
  const px = event.clientX - left;
  const py = event.clientY - top;
  //屏幕坐标px、py转标准设备坐标x、y
  //width、height表示canvas画布宽高度
  const x = (px / width) * 2 - 1;
  const y = -(py / height) * 2 + 1;
});

鼠标点击选中模型

1.坐标系转化

js
renderer.domElement.addEventListener("click", function (event) {
  // .offsetY、.offsetX以canvas画布左上角为坐标原点,单位px
  const px = event.offsetX;
  const py = event.offsetY;
  //屏幕坐标px、py转WebGL标准设备坐标x、y
  //width、height表示canvas画布宽高度
  const x = (px / width) * 2 - 1;
  const y = -(py / height) * 2 + 1;
});

2.射线计算

.setFromCamera()在点击位置创建一条射线,用来选中拾取模型对象

js
//...
//创建一个射线投射器`Raycaster`
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
//...

3.射线交叉计算

js
//...
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
if (intersects.length > 0) {
  // 选中模型的第一个模型,设置为红色
  intersects[0].object.material.color.set(0xff0000);
}
//...

Canvas 尺寸变化调整射线计算

  • 画布全屏
js
renderer.domElement.addEventListener("click", function (event) {
  const px = event.offsetX;
  const py = event.offsetY;
  //屏幕坐标转WebGL标准设备坐标
  const x = (px / window.innerWidth) * 2 - 1;
  const y = -(py / window.innerHeight) * 2 + 1;
});
  • 局部布局
js
renderer.domElement.addEventListener("click", function (event) {
  const width = window.innerWidth - 200; //canvas画布宽度
  const height = window.innerHeight - 60; //canvas画布高度
  //屏幕坐标转WebGL标准设备坐标
  const x = (px / width) * 2 - 1;
  const y = -(py / height) * 2 + 1;
});

射线拾取层级模型

问题:当要选取的模型是由多个子模型组成的时候,选取该模型会返回某个子对象模型

解决方法:给要拾取的父对象的所有子孙模型添加一个.ancestors属性,该属性指向父对象

js
addEventListener("click", function (event) {
  //...
  const storeJar = model.getObjectByName("存储罐");
  for (let i = 0; i < storeJar.children.length; i++) {
    const group = storeJar.children[i];
    //递归遍历chooseObj,并给其所有子孙后代设置一个ancestors属性指向自己
    group.traverse(function (obj) {
      if (obj.isMesh) {
        obj.ancestors = group;
      }
    });
  }
  //...
});

射线拾取 sprite 控制场景

1. 给精灵模型绑定函数事件

js
sprite.change = function () {
  mesh.material.color.set(0xffffff);
};
sprite2.change = function () {
  mesh.material.color.set(0xffff00);
};

2. 选中精灵模型执行事件

js
addEventListener("click", function (event) {
  // ...
  // 射线交叉计算拾取精灵模型
  const intersects = raycaster.intersectObjects([sprite, sprite2]);
  if (intersects.length > 0) {
    intersects[0].object.change(); //执行选中sprite绑定的change函数
  }
});