three.js Raycaster简介,不基于body加载,Raycaster射线拾取对象出现误差问题,基于全屏和非全屏DOM射线拾取方法封装

浏览器中浏览3D图形的时候,想要与3D图形之间做一些点击事件和交互操作,比较常用的一个解决方案就是使用Raycaster对象来实现(射线拾取)。

光线投射Raycaster API简述

光线投射用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)。
Raycaster( origin : Vector3, direction : Vector3, near : Float, far : Float )

  • origin:光线投射的原点向量。
  • direction:向射线提供方向的方向向量,应当被标准化。
  • near:返回的所有结果比near远。near不能为负值,其默认值为0。
  • far:返回的所有结果都比far近。far不能小于near,其默认值为Infinity(正无穷。)

属性

  • .far:远距离因数(投射远点)。这个值表明哪些对象可以基于该距离而被raycaster所丢弃。 这个值不应当为负,并且应当比near属性大。
  • .near:近距离因数(投射近点)。这个值表明哪些对象可以基于该距离而被raycaster所丢弃。 这个值不应当为负,并且应当比far属性小。
  • .camera : 对依赖于视图的对象进行光线投射时使用的相机。默认为空。
  • .layers:Raycaster 在执行相交测试时使用它来选择性地忽略 3D 对象。
  • .ray:用于进行光线投射的射线。

方法

  • .set ( origin : Vector3, direction : Vector3 ) : undefined:设置射线的起点和方向。
    • origin一个Vector3对象,光线投射的原点向量(射线起点)。
    • direction 一个Vector3对象,为光线提供方向的标准化方向向量(射线方向)。
  • .setFromCamera ( coords : Vector2, camera : Camera ) : undefined:通过相机设置射线。
    • coords 在标准化设备坐标中鼠标的二维坐标,X分量与Y分量应当在-1到1之间。
    • camera 射线所来源的摄像机。
  • .intersectObject ( object : Object3D, recursive : Boolean, optionalTarget : Array ) : Array:检测所有在射线与物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个。
    • object 检查与射线相交的物体。
    • recursive 若为true,则同时也会检查所有的后代。否则将只会检查对象本身。默认值为true。
    • optionalTarget (可选)设置结果的目标数组。如果不设置这个值,则一个新的Array会被实例化;如果设置了这个值,则在每次调用之前必须清空这个数组(例如:array.length = 0;)。
  • .intersectObjects ( objects : Array, recursive : Boolean, optionalTarget : Array ) : Array:检测所有在射线与物体之间,包括或不包括后代的相交部分。返回结果时,相交部分将按距离进行排序,最近的位于第一个
    • objects检测和射线相交的一组物体。
    • recursive若为true,则同时也会检测所有物体的后代。否则将只会检测对象本身的相交部分。默认值为true。
    • optionalTarget(可选)设置结果的目标数组。如果不设置这个值,则一个新的Array会被实例化;如果设置了这个值,则在每次调用之前必须清空这个数组(例如:array.length = 0;)。

注意点

  1. intersectObjectintersectObjects方法返回一个包含有交叉部分的数组,凡是选中的都会以数组的形式返回,返回数据结构是:[ { distance, point, face, faceIndex, object }, ... ]
    • distance射线投射原点和相交部分之间的距离。
    • point 射线与物体相交的第一个顶点(世界坐标)
    • face与射线相交的面
    • faceIndex与射线相交的平面的索引
    • object相交的物体,一般是Mesh,point,Line等。
  2. 如果两个网格模型屏幕坐标位置是重合的,那么都会被选中,因此可以通过数组下标的形式访问第几个对象, 被选中的网格模型对象以object属性的形式存在,代码intersects[0].object就表示被选中所有的网格模型中的第一个网格模型对象。 例如通过语句intersects[0].object.material.opacity = 0.6;可以更改材质对象的透明度。
  3. 当计算这条射线是否和物体相交的时候,Raycaster将传入的对象委托给raycast方法。这将可以让mesh对于光线投射的响应不同于linespointclouds
  4. 对于网格来说,面必须朝向射线的原点,以便其能够被检测到。 用于交互的射线穿过面的背侧时,将不会被检测到。如果需要对物体中面的两侧进行光线投射,你需要将material中的side属性设置为THREE.DoubleSide

射线拾取使用

在 three.js 中利用射线Raycaster进行碰撞检测获取射线穿透对象。射线最主要的用途就是拾取物体,简单说,我们在屏幕上添加一个点击事件,然后以屏幕的位置发射一条射线,执行拾取所有场景的物体,并拿到拾取到的物体,做对应的交互事件,这样我们一套点击事件就完成了。主要步骤:

  1. 绑定点击事件。
  2. 获取点击时的位置并创建标准化设备坐标。
  3. 根据点击时的标准化设备坐标创建射线.
  4. 根据逻辑,让整个数组的物体发生交互事件,或者指定拾取到的第一个,让其发生交互事件。

创建射线

// 创建射线对象
let rayCaster = new THREE.Raycaster();
// 创建一个用来存储标准化设备坐标的二维向量
let mouse = new THREE.Vector2();

绑定点击事件

window.addEventListener( 'click', event => {
    // 获取鼠标位置
    let x = event.clientX;
    let y = event.clientY;
});

获取点击时的位置并创建映射顶点

我们最终点击的位置,要用映射的方式传给射线,射线根据计算的比例,计算出实际发射射线的方向,再发出射线。
官方使用的方式是映射,而我们鼠标点击的位置一般不是空间中的实际位置。我们要将屏幕的坐标的坐标系转换一下,并计算当前点中的顶点在坐标系中表示的二维顶点,并保存在mouse上。

// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;

如果你的 canvas 不是全屏,就会发现射线拾取的计算偏了,所以官方提供的方法是建立在你的 canvas 覆盖全屏的情况下。
我们的 canvas 不是全屏时,我们需要使用另外的公式计算,我们依然可以用event.clientXevent.clientY拿到鼠标的点击位置。不一样的是,我们要根据dom.getBoundingClientRect().leftdom.getBoundingClientRect().top获取到 canvas 距离屏幕左边和上边的位置,通过event.clientX减掉dom.getBoundingClientRect().left在除以当前canvas的宽度,就可得到比例值。

let getBoundingClientRect = canvas.getBoundingClientRect();
// 屏幕坐标转标准设备坐标
let x = ((event.clientX - getBoundingClientRect.left) / canvas.offsetWidth) * 2 - 1; // 标准设备横坐标
let y = -((event.clientY - getBoundingClientRect.top) / canvas.offsetHeight) * 2 + 1; // 标准设备纵坐标

根据点击时的位置创建射线

创建射线有两种方法实现:

  1. 通过raycaster的.setFromCamera()计算射线射线位置。把鼠标单击位置坐标和相机作为.setFromCamera()方法的参数,.setFromCamera()就会计算射线投射器Raycaster的射线属性.ray,形象点说就是在点击位置创建一条射线,用来选中拾取模型对象。
// 通过鼠标点击的位置(二维坐标)和当前相机的矩阵计算出射线位置
raycaster.setFromCamera(mouse,camera);
  1. 坐标转换 + 计算射线方向。通过三维向量.unproject()将设备坐标转世界坐标,计算相机和设备坐标计算射线投射方向,及射线投射器Raycaster的射线属性.ray
// 标准设备坐标
let vector = new THREE.Vector3(x, y, 0); 
// 标准设备坐标转世界坐标
let worldVector = vector.unproject(camera);
// 射线投射方向单位向量(worldVector坐标减相机位置坐标)
let ray = worldVector.sub(camera.position).normalize();
// 创建射线投射器对象
let rayCaster = new THREE.Raycaster(camera.position, ray);

注意THREE.Raycaster()的第二个参数direction需要标准化,执行.normalize()归一化或者说标准化。

获取拾取的物体并做出指定操作

// 返回射线选中的对象
let intersects = rayCaster.intersectObjects(scene.children);
// 返回选中的对象数组
if(intersects.length > 0){
	console.log(intersects[0].object);
}

这时就有人问为啥做射线拾取的时候,总是被其他物体挡住,而不到我想要的物体。
根据上述API讲述intersectObjects方法的参数我们就可以知道,如果需要你需要拾取指定的物体mesh1mesh2,你就将指定物体放入一个数组中[mesh1,mesh2],作为intersectObjects的第一个参数参入,这个时候射线就只会针对于mesh1mesh2做射线拾取算法,而得到的结果不会是这两个元素以外的元素。
如果你不仅想获取当前拾取元素,还想获取拾取的子元素,就设置intersectObjects第二个参数为true。

拾取方法封装和使用

基础版本

基于全屏,只针对于3D模型加载canvas填充整个屏幕时有效。采用Raycaster自带的setFromCamera方法。

/**
 * @param { 事件对象 } event
 * @param { 场景对象 } scene
 * @param { 镜头对象 } camera
 */
function getCanvasIntersects(event, scene, camera) {
    // 声明 raycaster 和 mouse 变量
    let rayCaster = new THREE.Raycaster();
    let mouse = new THREE.Vector2();
    event.preventDefault();
    if (event.touches) {
        mouse.x = (event.touches[0].pageX / window.innerWidth) * 2 - 1;
        mouse.y = -(event.touches[0].pageY / window.innerHeight) * 2 + 1;
    } else {
        // 通过鼠标点击位置,计算出 raycaster 所需点的位置,以屏幕为中心点,范围 -1 到 1
        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    }
    // 通过鼠标点击的位置(二维坐标)和当前相机的矩阵计算出射线位置
    rayCaster.setFromCamera(mouse, camera);
    // 返回射线选中的对象 第二个参数如果不填 默认是false
    let intersects = rayCaster.intersectObjects(scene.children, true);
    // 返回选中的对象数组
    return intersects;
}

使用方法

// 点击事件 获取某一个盒子canvas中模型对象
function getBoxClickObjFn(event, scene, camera, canvas) {
    let intsersects = getCanvasIntersects(event, scene, camera, canvas);
    if (intsersects.length > 0) {
    	// 改变材质透明度
    	intersects[0].object.material.opacity = 0.6;
    }
}

基于某个DOM节点射线穿透获取对象

基于某一个不充满全屏的DOM时获取对象。采用坐标转换计算射线方向方法实现。

/**
 * @param { 事件对象 } event
 * @param { 场景对象 } scene
 * @param { 镜头对象 } camera
 * @param canvas 绘制盒子
 * 当canvas不占满整屏时射线拾取存在偏差,获取点击对象
 */
function getCanvasIntersects(event, scene, camera, canvas) {
    event.preventDefault();
    // 获取元素的大小及其相对于视口的位置
    let getBoundingClientRect = canvas.getBoundingClientRect();
    // 屏幕坐标转标准设备坐标
    let x = ((event.clientX - getBoundingClientRect.left) / canvas.offsetWidth) * 2 - 1; // 标准设备横坐标
    let y = -((event.clientY - getBoundingClientRect.top) / canvas.offsetHeight) * 2 + 1; // 标准设备纵坐标
    let vector = new THREE.Vector3(x, y, 0); // 标准设备坐标
    // 标准设备坐标转世界坐标
    let worldVector = vector.unproject(camera);
    // 射线投射方向单位向量(worldVector坐标减相机位置坐标)
    let ray = worldVector.sub(camera.position).normalize();
    // 创建射线投射器对象
    let rayCaster = new THREE.Raycaster(camera.position, ray);
    // 返回射线选中的对象 第二个参数如果不填 默认是false
    let intersects = rayCaster.intersectObjects(scene.children, true);
    //返回选中的对象数组
    return intersects;
}

使用方法:

// 点击事件 获取某一个盒子canvas中模型对象
function getBoxClickObjFn(event, scene, camera, canvas) {
    let intsersects = getCanvasIntersects(event, scene, camera, canvas);
    if (intsersects.length > 0) {
    	// 改变材质透明度
    	intersects[0].object.material.opacity = 0.6;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值