背景
我们要是想要与场景中做交互,大概率都是需要射线去做,点击事件、模拟鼠标hover效果等。 因为鼠标属于二维,咱们的场景是属于3D, 计算这些东西还是比较麻烦的,不过幸好three有对应的拓展
射线Ray
javascript
const ray = new THREE.ray() // 创建射线实例ray
Perporties
origin
direction
Methods
-
intersectTriangle
计算射线和一个三角形在3D空间中是否交叉
demo
javascriptconst 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
javascriptconst 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,0), 横向为x, 纵向为y
- 设备坐标是以中心为原点
转换(这个我们在后面会经常遇到)
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)
对复杂的层级模型进行射线拾取计算,会得到他们的某个子类
其实这种有很多解决办法
- 给给需要射线拾取父对象的所有子对象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];
}
- 分组,对层级模型,进行分组,就是我我下面几个场景用的方式,比较暴力,遍历所有组的children进行与射线计算,比较耗时和耗性能
拾取Sprite控制场景
射线投射器Raycaster
通过.intersectObjects()
方法可以拾取精灵模型Sprite
在这里就不做多余赘述
场景一:用鼠标模拟hover事件
我们先清楚我们的目的
我们文章上面有这个判断,一般我们模型都是比较复杂的,我们还要清楚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事件)
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的异步任务,开启监听
javascriptxxx.addEventListener('click', ()=>{ this.xxxxxx.removeEventListener('click', this.onXXXXXXClick) setTimeout(()=>{ this.xxxxxx.addEventListener('click', this.onXXXXXXClick) }, 0) })