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

背景

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

射线Ray

javascript 复制代码
const ray = new THREE.ray()  // 创建射线实例ray

Perporties

  • origin
  • direction

Methods

  • intersectTriangle

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

    demo

    javascript 复制代码
    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

javascript 复制代码
const raycaster = new THREE.Raycaster();

Methods

  • intersectObject

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

    demo

    javascript 复制代码
    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. 设备坐标是以中心为原点

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

javascript 复制代码
// 坐标转化公式
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;
})
javascript 复制代码
// 屏幕坐标转标准设备坐标
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属性,判断是属于哪个层级,效率也是很高的
javascript 复制代码
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事件

我们先清楚我们的目的

stateDiagram-v2 鼠标放模型上 --> 射线判断 射线判断 --> 做一些处理

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

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

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

javascript 复制代码
this.groups = this.scene.children.filter(child => child instanceof THREE.Group);

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

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

javascript 复制代码
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)加一个监听事件

javascript 复制代码
this.debounceFn = (()=>debounce(xxx, 20))()
this.xxx.addEventListener('mousemove', this.debounceFn)

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

接下来就是计算函数debounceFn

javascript 复制代码
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事件)

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

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

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

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

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

javascript 复制代码
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的异步任务,开启监听

    javascript 复制代码
    xxx.addEventListener('click', ()=>{
     this.xxxxxx.removeEventListener('click', this.onXXXXXXClick)
     setTimeout(()=>{
       this.xxxxxx.addEventListener('click', this.onXXXXXXClick)
     }, 0)
    })

参考链接

Three.js中文网

相关推荐
吃杠碰小鸡10 分钟前
commitlint校验git提交信息
前端
虾球xz41 分钟前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇1 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒1 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员1 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐1 小时前
前端图像处理(一)
前端
程序猿阿伟1 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒1 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪1 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背1 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript