开启Three之旅(二)射线、拾取模型、解决鼠标点击、Hover以及CSS3Renderer点击穿透问题

avatar
作者
猴君
阅读量:2

文章目录


背景

我们要是想要与场景中做交互,大概率都是需要射线去做,点击事件、模拟鼠标hover效果等。
因为鼠标属于二维,咱们的场景是属于3D, 计算这些东西还是比较麻烦的,不过幸好three有对应的拓展

射线Ray

const ray = new THREE.ray()  // 创建射线实例ray 

Perporties

  • origin
  • direction

Methods

  • intersectTriangle

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

    demo

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

    参数4 – 是否进行背面剔除

    如何判断正反面?

Raycaster

这个构造函数是我们本文的重点,这个对标的就是Mesh

const raycaster = new THREE.Raycaster(); 

Methods

  • intersectObject

    计算自身射线.ray相交的网格模型, 接收一组网格模型,返回值也是一个数组(对象在数组中按照先后顺序)。

    demo

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

屏幕坐标 --> 设备坐标

  1. 屏幕坐标我们是熟悉的,左上角为(0,0), 横向为x, 纵向为y
  2. 设备坐标是以中心为原点

image.png

转换(这个我们在后面会经常遇到)

// 坐标转化公式 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; }) 
// 屏幕坐标转标准设备坐标 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; }) 

射线坐标计算(Canvas尺寸变化)

画布尺寸变化,我们的射线拾取也需要变化(要不然会拾取不到模拟对象)

射线拾取层级模型

我们上面已经知道aycaster.intersectObjects()方法可以拾取Mesh模型对象,但是真实开发中,一个层级模型可能里面有很多网格模型。

执行.intersectObjects(cunchu.children)对复杂的层级模型进行射线拾取计算,会得到他们的某个子类

其实这种有很多解决办法

  1. 给给需要射线拾取父对象的所有子对象Mesh自定义一个属性.ancestors,然后让该属性指向需要射线拾取父对象
    下面是官方文档中的例子
    最终是根据子模型的ancestors属性,判断是属于哪个层级,效率也是很高的
const cunchu = model.getObjectByName('存储罐'); // 射线拾取模型对象(包含多个Mesh) // 可以给待选对象的所有子孙后代Mesh,设置一个祖先属性ancestors,值指向祖先(待选对象)     for (let i = 0; i < cunchu.children.length; i++) {     const group = cunchu.children[i];     //递归遍历chooseObj,并给chooseObj的所有子孙后代设置一个ancestors属性指向自己     group.traverse(function (obj) {         if (obj.isMesh) {             obj.ancestors = group;         }     }) } // 射线交叉计算拾取模型 const intersects = raycaster.intersectObjects(cunchu.children); console.log('intersects', intersects); if (intersects.length > 0) {     // 通过.ancestors属性判断那个模型对象被选中了     outlinePass.selectedObjects = [intersects[0].object.ancestors]; }  
  1. 分组,对层级模型,进行分组,就是我我下面几个场景用的方式,比较暴力,遍历所有组的children进行与射线计算,比较耗时和耗性能

拾取Sprite控制场景

射线投射器Raycaster通过.intersectObjects()方法可以拾取精灵模型Sprite

在这里就不做多余赘述

场景一:用鼠标模拟hover事件

我们先清楚我们的目的

鼠标放模型上射线判断做一些处理

我们文章上面有这个判断,一般我们模型都是比较复杂的,我们还要清楚Group的概念

就是我们一般需要判断是哪一个组被鼠标放上去了(也方便点击事件)

我们可以把所有组都筛选出来,下面的思路是以Vue为例

this.groups = this.scene.children.filter(child => child instanceof THREE.Group); 

这一步最好是在,确保模型加载完之后(这是我写的一个办法,当然也可以有其它办法)
因为后面会频繁计算(这也算一个小小的优化项)

其实,也可以你自己新new Group()一个实例也可以(需要手动添加)

async Model(url) {   const loader = new GLTFLoader();   const promises = url.map((element, index) => {     return new Promise((resolve, reject) => {       loader.load(element, (gltf) => {         const model = gltf.scene;         // 在这里做一些操作         this.scene.add(model);         resolve();       });     });   });   await Promise.all(promises);   this.groups = this.scene.children.filter(child => child instanceof THREE.Group); } 

然后就是在画布之上(包裹画布的父元素就行xxx)加一个监听事件

this.debounceFn = (()=>debounce(xxx, 20))() this.xxx.addEventListener('mousemove', this.debounceFn) 

this.debounce是把该计算加了一个防抖处理,需要挂到data里面的属性值里面去(方便准确取消监听)

接下来就是计算函数debounceFn

this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; this.raycaster.setFromCamera(this.mouse, this.camera); const intersects = this.raycaster.intersectObjects(this.groups ? this.groups : []); if (intersects.length > 0) {     // 处理 xxx } else {     // 处理 xxx } 

场景二: 选中模型(click事件)

坐标转换计算射线做出处理
//创建一个射线投射器`Raycaster` const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(new THREE.Vector2(x, y), camera); 

我们参考场景一:用鼠标模拟hover事件的导入分组Group

导入的时候(还是参考场景一的,我导入的时候就是一组模型,如果你的不是,可以手动分组,再添加到场景中),给group绑定上事件在属性userData对象上,就是挂载在上面

loader.load(element, (gltf) => {     const model = gltf.scene;     // 在这里做一些操作     model.userData.onClick = Fn     this.scene.add(model);     resolve(); }); 

射线交叉计算,项目中一般是比较复杂的,我们就得去处理Group模型(就是一组模型,可以看看Group的概念)

this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; this.raycaster.setFromCamera(this.mouse, this.camera); for (const group of this.groups) {     const intersects = this.raycaster.intersectObject(group);     if (intersects.length > 0) {       group.userData.onClick()       break;     } } 

注意

射线用一个就好,最好还是复用,

场景三:处理射线穿透问题

  • 模型穿透,射线计算返回值intersects,是按照顺序返回的,直接判断第一个就行,再加一些操作

  • CSS3Renderer, html插入到场景中,很容易穿透(click事件为例)

    pointerEvents = 'none'以免模型标签HTML元素遮挡鼠标选择场景模型

    其实有一个简单的办法,先取消模型的点击事件,再加个0ms的异步任务,开启监听

    xxx.addEventListener('click', ()=>{  this.xxxxxx.removeEventListener('click', this.onXXXXXXClick)  setTimeout(()=>{    this.xxxxxx.addEventListener('click', this.onXXXXXXClick)  }, 0) }) 

参考链接

Three.js中文网

小广告~

vx关注:A返x小助手 (购物返现、外卖大额领券、高额打车券,优惠点餐,低价电影票等)

在这里插入图片描述

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!