Skip to content

射线拾取模型

🕒 Published at:

射线Ray

射线Ray和三维向量Vector3一样属于数学几何计算相关的API,可以进行射线交叉计算。

学习Three.js中的射线Ray概念,你可以类比数学几何中提到的射线,在三维空间中,一条线把一个点作为起点,然后沿着某个方向无限延伸。

js
// 创建射线对象Ray
const ray = new THREE.Ray()

射线起点.origin

射线Ray的起点.origin在3D空间中的坐标,可以用一个三维向量Vector3的x、y、z分量表示。

js
// 设置射线起点
ray.origin = new THREE.Vector3(1,0,3);

起点.origin属性值是三维向量Vector3,也可以用.set()方法设置。

js
// 设置射线起点
ray.origin.set(1, 0, 3);

射线方向.direction

射线Ray的方向.direction通用用一个三维向量Vector3表示,向量长度保证为1,也就是单位向量。

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

注意.direction的值需要是单位向量,不是的话可以执行.normalize()归一化或者说标准化。

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

// 表示射线沿着xy坐标轴的中间线
ray.direction = new THREE.Vector3(1,1,0).normalize();

.intersectTriangle()方法

射线Ray有很多关于数学计算的方法,下面就先介绍一个与三角形交叉计算相关的方法.intersectTriangle(),简单说,就是计算一个射线和一个三角形在3D空间中是否交叉。

执行.intersectTriangle()方法,如果相交返回交点坐标,不相交返回空值null。

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();//用来记录射线和三角形的交叉点
// `.intersectTriangle()`计算射线和三角形是否相交叉,相交返回交点,不相交返回null
const result = ray.intersectTriangle(p1,p2,p3,false,point);
console.log('交叉点坐标', point);
console.log('查看是否相交', result);

.intersectTriangle()参数4表示是否进行背面剔除,p1,p2,p3可以理解为一个三角形,有正反两面,一面是正面,一面是反面

在一面观察p1,p2,p3,如果沿着三个点的顺序转圈是逆时针方向,表示正面,另一面观察,p1,p2,p3就是顺时针方向,表示背面。

.intersectTriangle()参数4设为true,表示进行背面剔除,虽然从几何空间上讲,该案例源码射线和三角形虽然交叉,但在threejs中,三角形背面对着射线,视为交叉无效,进行背面剔除,返回值r是null。

js
const r = ray.intersectTriangle(p1,p2,p3,true,point);
console.log('查看是否相交', r);

测试案例

准备三个mesh用于射线拾取

js
const geometry = new THREE.SphereGeometry(25, 50, 50);
const material = new THREE.MeshLambertMaterial({
    color: 0x009999,
});
const mesh1 = new THREE.Mesh(geometry, material);
const mesh2 = mesh1.clone();
mesh2.position.y = 100;
const mesh3 = mesh1.clone();
mesh3.position.x = 100;
const model = new THREE.Group();
// 三个网格模型mesh1,mesh2,mesh3用于射线拾取测试
model.add(mesh1, mesh2, mesh3);
model.updateMatrixWorld(true);

射线投射器Raycaster

射线投射器Raycaster具有一个射线属性.ray,该属性的值就是射线对象Ray。

js
const raycaster = new THREE.Raycaster();
console.log('射线属性',raycaster.ray);

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

射线交叉计算(.intersectObjects()方法)

射线投射器Raycaster通过.intersectObjects()方法可以计算出来与自身射线.ray相交的网格模型。

.intersectObjects([mesh1, mesh2, mesh3])对参数中的网格模型对象进行射线交叉计算,未选中对象返回空数组[],选中一个对象,数组1个元素,选中多个对象,数组多个元素,如果选中多个对象,对象在数组中按照先后排序。

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

.intersectObjects()射线拾取返回信息

射线拾取返回的intersects里面的元素包含多种信息,你可以通过threejs文档查看,或者在浏览器控制台打印查看。

.intersectObjects()和.intersectObject()功能相同,只是具体语法不同,.intersectObjects()返回数组元素包含的信息,可以参考文档关于.intersectObject()的介绍。

js
console.log("射线器返回的对象", intersects);
// intersects.length大于0说明,说明选中了模型
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);
}

注意事项

注意射线拾取的时候,mesh1, mesh2, mesh3位置要确保更新的情况下,执行射线计算,threejs一般是渲染器执行一次.render()之后,你设置的mesh.position或者mesh父对象的position才会真实生效。

js
...
// 注意更新下模型的世界矩阵,你设置的mesh.position生效,再进行射线拾取计算
model.updateMatrixWorld(true);
...
const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);

mesh2.position.y = 100;
mesh3.position.x = 100;
const model = new THREE.Group();
// 三个网格模型mesh1,mesh2,mesh3用于射线拾取测试
model.add(mesh1, mesh2, mesh3);

//注意更新下模型的世界矩阵,你设置的position生效,再进行射线拾取计算
model.updateMatrixWorld(true);

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]);

核心代码

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;
    //创建一个射线投射器`Raycaster`
    const raycaster = new THREE.Raycaster();
    //.setFromCamera()计算射线投射器`Raycaster`的射线属性.ray
    // 形象点说就是在点击位置创建一条射线,射线穿过的模型代表选中
    raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
    //.intersectObjects([mesh1, mesh2, mesh3])对参数中的网格模型对象进行射线交叉计算
    // 未选中对象返回空数组[],选中一个对象,数组1个元素,选中两个对象,数组两个元素
    const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
    console.log("射线器返回的对象", intersects);
    // intersects.length大于0说明,说明选中了模型
    if (intersects.length > 0) {
        // 选中模型的第一个模型,设置为红色
        intersects[0].object.material.color.set(0xff0000);
    }
})

效果

完整代码

js
import * as THREE from 'three';
// 引入轨道控制器扩展库OrbitControls.js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { onMounted } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();

onMounted(() => {
  // 创建场景
  const scene = new THREE.Scene();

  const geometry = new THREE.SphereGeometry(25, 50, 50);
  const material = new THREE.MeshLambertMaterial({
    color: 0x009999
  });
  const mesh1 = new THREE.Mesh(geometry, material);
  const mesh2 = mesh1.clone();
  mesh2.geometry = mesh1.geometry.clone();
  mesh2.material = mesh1.material.clone();
  mesh2.position.y = 100;
  const mesh3 = mesh1.clone();
  mesh3.position.x = 100;
  mesh3.geometry = mesh1.geometry.clone();
  mesh3.material = mesh1.material.clone();
  const model = new THREE.Group();
  model.add(mesh1, mesh2, mesh3);
  model.updateMatrixWorld(true);

  scene.add(model); //网格模型添加到场景中

  // AxesHelper:辅助观察的坐标系
  const axesHelper = new THREE.AxesHelper(100);
  scene.add(axesHelper);

  // tip: 环境光
  //   环境光AmbientLight:没有特定的光源位置,整体改变场景的光照明暗度
  const ambient = new THREE.AmbientLight(0xffffff, 0.4);
  scene.add(ambient);

  const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
  // 设置光源的方向:通过光源position属性和目标指向对象的position属性计算
  directionalLight.position.set(100, 60, 50);
  // 方向光指向对象网格模型mesh,可以不设置,默认的位置是0,0,0
  // directionalLight.target = mesh;
  scene.add(directionalLight);

  const dirLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5, 0xff0000);
  scene.add(dirLightHelper);

  /**
   * 光源设置
   */
  //点光源
  //   const pointLight = new THREE.PointLight(0xffffff, 1.0);
  //   pointLight.decay = 0.0; //光源光照强度不随距离改变衰减
  //   //点光源位置
  //   // pointLight.position.set(400, 0, 0);//点光源放在x轴上
  //   pointLight.position.set(400, 200, 300); //偏移光源位置,观察渲染效果变化
  //   scene.add(pointLight); //点光源添加到场景中

  // width和height用来设置Three.js输出的Canvas画布尺寸(像素px)
  const width = window.innerWidth; //宽度
  const height = window.innerHeight; //高度

  /**
   * 透视投影相机设置
   */
  // 30:视场角度, width / height:Canvas画布宽高比, 1:近裁截面, 3000:远裁截面
  const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
  camera.position.set(470, 359, 297); //相机在Three.js三维坐标系中的位置

  camera.lookAt(0, 0, 0); //相机观察目标指向Three.js坐标系原点

  /**
   * 创建渲染器对象
   */
  const renderer = new THREE.WebGLRenderer();
  renderer.setSize(width, height); //设置three.js渲染区域的尺寸(像素px)
  renderer.render(scene, camera); //执行渲染操作
  document.getElementById('canvas').appendChild(renderer.domElement);

  // 设置相机控件轨道控制器OrbitControls
  const controls = new OrbitControls(camera, renderer.domElement);

  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;
    //创建一个射线投射器`Raycaster`
    const raycaster = new THREE.Raycaster();
    //.setFromCamera()计算射线投射器`Raycaster`的射线属性.ray
    // 形象点说就是在点击位置创建一条射线,射线穿过的模型代表选中
    raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
    //.intersectObjects([mesh1, mesh2, mesh3])对参数中的网格模型对象进行射线交叉计算
    // 未选中对象返回空数组[],选中一个对象,数组1个元素,选中两个对象,数组两个元素
    const intersects = raycaster.intersectObjects([mesh1, mesh2, mesh3]);
    console.log('射线器返回的对象', intersects);
    // intersects.length大于0说明,说明选中了模型
    if (intersects.length > 0) {
      // 选中模型的第一个模型,设置为红色
      intersects[0].object.material.color.set(0xff0000);
    }
  });

  function render() {
    renderer.render(scene, camera); //执行渲染操作
    // console.log(camera.position);

    requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
  }
  render();

  // 如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
  //   controls.addEventListener('change', function () {
  //     renderer.render(scene, camera); //执行渲染操作
  //     // 浏览器控制台查看相机位置变化
  //     console.log('camera.position', camera.position);
  //   }); //监听鼠标、键盘事件

  window.onresize = () => {
    if (route.query.type === '1') {
      // 重置渲染器输出画布canvas尺寸
      renderer.setSize(window.innerWidth, window.innerHeight);
      return;
    } else if (route.query.type === '2') {
      // 重置渲染器输出画布canvas尺寸
      renderer.setSize(window.innerWidth, window.innerHeight);
      camera.aspect = window.innerWidth / window.innerHeight;
      // 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
      // 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
      // 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
      camera.updateProjectionMatrix();
    }
  };
});