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


背景

我们要是想要与场景中做交互,大概率都是需要射线去做,点击事件、模拟鼠标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小助手 (购物返现、外卖大额领券、高额打车券,优惠点餐,低价电影票等)

在这里插入图片描述

  • 15
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
使用Three.js可以轻松实现鼠标点击位置移动的效果。首先,在场景中创建一个3D模型,比如一个立方体或者球体。然后,监听鼠标点击事件,当鼠标模型上被点击时,获取点击位置的坐标。接着,可以通过修改模型的位置,将模型移动到鼠标点击的位置。 具体的代码可以如下: ```javascript // 创建场景 var scene = new THREE.Scene(); // 创建一个立方体 var geometry = new THREE.BoxGeometry(1, 1, 1); var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); var cube = new THREE.Mesh(geometry, material); scene.add(cube); // 创建摄像机 var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z = 5; // 创建渲染器 var renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 监听鼠标点击事件 document.addEventListener('click', onDocumentMouseClick, false); function onDocumentMouseClick(event) { // 获取鼠标点击位置的坐标 var mouse = new THREE.Vector2(); mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; // 通过鼠标点击位置的坐标获取模型的位置 var raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); var intersects = raycaster.intersectObjects(scene.children); if (intersects.length > 0) { // 将模型移动到鼠标点击位置 var targetPosition = intersects[0].point; cube.position.copy(targetPosition); } } // 渲染场景 function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); ``` 以上代码通过监听鼠标点击事件,获取鼠标点击位置的坐标,并将模型移动到该位置。这样,就实现了鼠标点击位置移动的效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李和贵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值