13 Three.js场景交互

由于浏览器是一个2d视口,而在里面显示three.js的内容是3d场景,场景交互就是在二维平面控制三维场景的模型,问题就是如何将2d视口的xy坐标转换成three.js场景中的3d坐标。好在three.js已经有了解决相关问题的方案,那就是THREE.Raycaster射线,用于鼠标拾取(计算出鼠标移过的三维空间中的对象)等等。我们看一张图片:
raycaster
我们一般都会设置三维场景的显示区域,如果,指明当前显示的2d坐标给THREE.Raycaster的话,它将生成一条从显示的起点到终点的一条射线。也就是说,我们再屏幕上点击了一个点,在three.js里面获取的则是一条直线。

THREE.Raycaster构造函数和对象方法

实例化

new Raycaster( origin, direction, near, far );

origin - 光线投射的原点矢量。
direction - 光线投射的方向矢量,应该是被归一化的。
near - 投射近点,用来限定返回比near要远的结果。near不能为负数。缺省为0。
far - 投射远点,用来限定返回比far要近的结果。far不能比near要小。缺省为无穷大。

属性

far

当前射线的最远距离,射线的终点。此值不应该为负值,而且比near值要大。

near

当前射线的最近距离,射线的起点。此值不应为负值,比far值要小。

linePrecision

射线和线相交的精度,浮点数类型的值。

方法

.set()

此方法可以重新设置射线的原点和方向

.set(origin,direction)

origin - 射线的新的原点矢量位置
direction - 基于原点位置的射线的方向矢量

.setFromCamera ()

使用当前相机和界面的2d坐标设置射线的位置和方向。

.setFromCamera ( coords, camera )

coords - 鼠标的二维坐标,在归一化的设备坐标(NDC)中,也就是X 和 Y 分量应该介于 -1 和 1 之间。
camera - 射线起点处的相机,即把射线起点设置在该相机位置处。

.intersectObject ()和 .intersectObjects ()

检查射线和物体之间的所有交叉点(包含或不包含后代)。交叉点返回按距离排序,最接近的为第一个。 返回一个交叉点对象数组。

.intersectObject ( object, recursive, optionalTarget)

object - 用来检测和射线相交的物体。
recursive - 如果为true,它还检查所有后代。否则只检查该对象本身。缺省值为false。
optionalTarget - 可选参数,用于设置放置结果的数组。如果没有则将实例化一个新数组并将获取到的数据放入,。

.intersectObjects ( array, recursive, optionalTarget)

intersectObjectintersectObjects的区别就是,intersectObject第一个参数需要传入一个3d对象,而intersectObjects需要传入一个3d对象组成的数组。

返回数组每一个对象的内容

如果射线与场景内的模型没有相交,将返回一个空数组,否则,将返回从近到远的顺序生成的一个对象数组。

[ { distance, point, face, faceIndex, indices, object }, ... ]

distance – 射线的起点到相交点的距离
point – 在世界坐标中的交叉点
face – 相交的面
faceIndex – 相交的面的索引
indices – 组成相交面的顶点索引
object – 相交的对象

当一个网孔(Mesh)对象和一个缓存几何模型(BufferGeometry)相交时,faceIndex 将是 undefined,并且 indices 将被设置; 而当一个网孔(Mesh)对象和一个几何模型(Geometry)相交时,indices 将是 undefined。

当计算这个对象是否和射线相交时,Raycaster 把传递的对象委托给 raycast 方法。 这允许 mesh 对于光线投射的反应可以不同于 lines 和 pointclouds(mesh,lines,pointclouds都是不同的计算相交的方法)。

注意,对于网格,面(faces)必须朝向射线原点,这样才能被检测到;通过背面的射线的交叉点将不被检测到。 为了光线投射一个对象的正反两面,你得设置 material 的 side 属性为 THREE.DoubleSide。

实现一个模型的点击事件

上面讲解了射线的相关内容,接下来,我们来看一下,如何使用射线实现一个普通的点击事件

  • 首先,我们通过点击事件回调的event获取到点击的位置:
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;

默认没有经过矩阵转换过的显示区域的宽和高分别是2,即中心点也是webgl场景的坐标原点,左上角的坐标是(-1.0, 1.0, 0.0), 右下角的坐标轴是(1.0, -1.0, 0.0)。我们可以通过点击点的位置计算出当前点击的点在场景中,没有矩阵转换过的平面坐标。如果webgl的渲染区域不是占满窗口状态,我们还需要获取到显示区域距离窗口左上角的偏移量,再计算位置:

//通过dom的getBoundingClientRect方法获得当前显示区域距离左上角的偏移量
var left = renderer.domElement.getBoundingClientRect().left;
var top = renderer.domElement.getBoundingClientRect().top;

//根据浏览器的设备类型来获取到当前点击的位置
var clientX = dop.browserRedirect() === "pc" ? event.clientX - left : event.touches[0].clientX - left;
var clientY = dop.browserRedirect() === "pc" ? event.clientY - top : event.touches[0].clientY - top;

//计算出场景内的原始坐标
mouse.x = (clientX / renderer.domElement.offsetWidth) * 2 - 1;
mouse.y = -(clientY / renderer.domElement.offsetHeight) * 2 + 1;
  • 获取到坐标以后,我们需要使用射线的setFromCamera()方法配合场景坐标和相机更新射线的位置:
raycaster.setFromCamera( mouse, camera );
  • 使用intersectObjects()方法获取射线和所有模型相交的数组集合
var intersects = raycaster.intersectObjects( scene.children );

这里在提醒一句,很多小伙伴有时候发现点击了以后射线无法获取到相交的物体,那是因为为了节约性能,我们需要设置第二个参数为true,让Three.js遍历模型所有的子类去判断是否相交。

  • 最后,如果有与射线相交的模型,返回的intersects数组长度将不为零:
if(intersects.length > 0){
	alert("有相交的模型");
}

下面是一个点击案例,点中物体后,模型颜色将会变色:点击这里 案例源码地址:点击这里

实现一个简单的框选案例

最近小伙伴都想实现一个简单的框选案例,那我在这一篇文章里面添加上,本框选案例是通过模型的位置进行判断实现的框选。相对于其它实现方式,这种实现节约性能,简单易懂,能够应付大部分场景。
接下来,我讲解一下这个框选的实现思路:

  • 在鼠标按下时,记录鼠标按下时的场景坐标:
//获取到显示区域距离窗口左上角的偏移量
domClient.x = renderer.domElement.getBoundingClientRect().left;
domClient.y = renderer.domElement.getBoundingClientRect().top;
//计算出当前鼠标距离显示区域左上角的距离
down.x = e.clientX - domClient.x;
down.y = e.clientY - domClient.y;
  • 使用之前学习到的box对象方法来计算出模型的包围盒中心位置,这样对多个复杂模型比较管用,如果简单的几何体的话,可以直接使用mesh的位置来计算。通过相机将世界坐标的位置转换为平面坐标,并将模型放到一个数组内以便后期使用:
for (let i = 0; i < group.children.length; i++) {
    let box = new THREE.Box3();
    box.expandByObject(group.children[i]);

    //获取到平面的坐标
    let vec3 = new THREE.Vector3();
    box.getCenter(vec3);
    let vec = vec3.project(camera);

    modelsList.push(
        {
            component: group.children[i],
            position: {
                x: vec.x * half.width + half.width,
                y: -vec.y * half.height + half.height
            },
            normalMaterial: group.children[i].material
        }
    )
}
  • 绑定documentmousemove事件和mouseup事件,鼠标移动事件是为了判断每个模型是否处于框内,鼠标抬起事件将绑定的事件清除。
//绑定鼠标按下移动事件和抬起事件
document.addEventListener("mousemove", movefun, false);
document.addEventListener("mouseup", upfun, false);
  • 在鼠标移动事件中,我们计算出当前四个边的位置,并且循环判断哪些模型的位置处于框内,处于框内的模型的材质将被修改为框选材质:
for (let i = 0; i < modelsList.length; i++) {
    let position = modelsList[i].position;
    //判断当前位置是否处于框内
    if (position.x > min.x && position.x < max.x && position.y > min.y && position.y < max.y) {
        modelsList[i].component.material = material;
    }
    else{
        modelsList[i].component.material = modelsList[i].normalMaterial;
    }
}
  • 在最后的鼠标抬起事件内,将框选框隐藏,并将所有材质修改为默认材质:
function upfun(e) {

    //清除事件
    document.body.removeChild(div);
    document.removeEventListener("mousemove", movefun, false);
    document.removeEventListener("mouseup", upfun, false);

    //将所有的模型修改为当前默认的材质
    for (let i = 0; i < modelsList.length; i++) {
        modelsList[i].component.material = modelsList[i].normalMaterial;
    }
}

最后,附上一个可以查看的案例地址:点击这里
案例源码地址:点击这里

  • 13
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值