在3D场景中常用的一个需求就是鼠标在屏幕上点击特定位置,选中一个物体模型,进行下一步的操作。比如说移动、旋转变形或者改变物体模型渲染外观等等。具体怎么实现呢?这涉及到把二维坐标转换到三维场景里,进行检测找到选种的模型。
在threejs世界里,处理这样的场景就非常简单了,今天介绍一下这个类“Raycaster”。
光线投射器(Raycaster)
该类用来处理光线投射。光线投射主要用于物体选择、碰撞检测以及图像成像等方面。
光线投射方法是基于图像序列的直接体绘制(Volume Rendering)算法。
从图像的每一个像素,沿固定方向(通常是视线方向)发射一条光线,光线穿越整个图像序列,
并在这个过程中,对图像序列进行采样获取颜色信息,同时依据光线吸收模型将颜色值进行累加,直至光线穿越整个图像序列,最后得到的颜色值就是渲染图像的颜色。
光线投射的基本步骤可以分为如下4步:
光线投射(Ray casting):对最终图像的每个像素,都有一条光线穿过体素。在这一阶段,认为体素被接触并封闭于一个包围图元中是有帮助的:一个简单的几何对象(通常是一个长方体)用来与光线和体相交。
采样(Sampling):沿着光线的射线部分位于体的内部,等距离的点采样被选择。通常体和表示光线的射线对齐,样本点通常被放于体素中间。因此,有必要对从它周围的体素的样本点的值进行插值。
着色(Shading):对每个样本点,计算出梯度。这些代表体内局部表面的方向。然后给这些样本着色,也就是根据它们的表面方向和实际的光源添加阴影和颜色。
组合(Compositing):在所有的样本点被着色后,沿着光线组合它们,得到该像素最终的颜色值。
这个过程被不断重复。计算开始于视图中最远的样本点,并且结束于最近的一个。这个工作流水线会确保被遮挡的体部分不影响上述过程得到的结果像素。
构造器(Constructor)
Raycaster( origin, direction, near, far ) {
origin — 光线投射的起点向量。
direction — 光线投射的方向向量,应该是被归一化的。
near — 投射近点,用来限定返回比near要远的结果。near不能为负数。缺省为0。
far — 投射远点,用来限定返回比far要近的结果。far不能比near要小。缺省为无穷大。
这将创建一个新的光线投射器对象。
属性(Properties)
#.ray
用于光线投射的射线。
#.near
光线投射器的近点因子,这个值指示基于这个距离哪些对象可以被舍弃。
这个值不能是负的,且应该小于far属性。
#.far
光线投射器的远点因子,这个值指示基于这个距离哪些对象可以被舍弃。
这个值不能是负的,且应该大于near属性。
.linePrecision
和 线条(Line) 对象相交时的精度因子。
方法(Methods)
#.set ( origin, direction )
origin — 光线投射的起点向量。
direction — 被归一化的光线投射的方向向量。
用一个新的起点和方向向量来更新射线(ray)。
#.setFromCamera ( coords, camera )
coords — 鼠标的二维坐标,在归一化的设备坐标(NDC)中,也就是X 和 Y 分量应该介于 -1 和 1 之间。
camera — 射线起点处的相机,即把射线起点设置在该相机位置处。
用一个新的原点和方向向量来更新射线(ray)。
#.intersectObject ( object, recursive )
object — 用来检测和射线相交的物体。
recursive — 如果为true,它还检查所有后代。否则只检查该对象本身。缺省值为false。
检查射线和物体之间的所有交叉点(包含或不包含后代)。交叉点返回按距离排序,最接近的为第一个。返回一个交叉点对象数组。
[ { distance, point, face, faceIndex, indices, object }, ... ]
distance – 射线的起点到相交点的距离
point – 在世界坐标中的交叉点
face – 相交的面
faceIndex – 相交的面的索引
indices – 组成相交面的顶点索引
object – 相交的对象
当一个网孔(Mesh)对象和一个缓存几何模型(BufferGeometry)相交时,faceIndex 将是 undefined,并且 indices 将被设置;而当一个网孔(Mesh)对象和一个几何模型(Geometry)相交时,indices 将是 undefined。
当计算这个对象是否和射线相交时,Raycaster 把传递的对象委托给 raycast 方法。这允许 meshes 对于光线投射的响应可以不同于 lines 和 pointclouds。
*注意*,对于网格,面(faces)必须朝向射线原点,这样才能被检测到;通过背面的射线的交叉点将不被检测到。为了光线投射一个对象的正反两面,你得设置 material 的 side 属性为 THREE.DoubleSide。
#.intersectObjects ( objects, recursive )
objects — 检查是否和射线相交的一组对象。
recursive — 如果为true,还同时检查所有的后代对象。否则只检查对象本身。缺省值为 false。
检查射线和对象之间的所有交叉点(包含或不包含后代)。交叉点返回按距离排序,最接近的为第一个。返回结果类似于 .intersectObject。
我们使用上次场景里(如何实现一个3d场景中的阴影效果(threejs)?)的示例,增加鼠标点击选中物体模型,改变模型渲染颜色,及让模型向上移动一部分位置的功能。
代码语言:javascript复制
添加鼠标事件:
声明raycaster和mouse变量
代码语言:javascript复制 //声明raycaster和mouse变量
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
添加鼠标点击事件
代码语言:javascript复制function onMouseClick( event ) {
//通过鼠标点击的位置计算出raycaster所需要的点的位置,以屏幕中心为原点,值的范围为-1到1.
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 1;
// 通过鼠标点的位置和当前相机的矩阵计算出raycaster
raycaster.setFromCamera( mouse, camera );
// 获取raycaster直线和所有模型相交的数组集合
var intersects = raycaster.intersectObjects( scene.children );
for ( var i = 0; i < intersects.length; i ) {
if(intersects[ i ].object.name == "plane"){
break;
}
intersects[ i ].object.material.color.set( 0xff4577 );
intersects[ i ].object.position.y = intersects[ i ].object.position.y 10;
break;
}
}
代码语言:javascript复制window.addEventListener( 'click', onMouseClick, false );
注意这句话:
var intersects = raycaster.intersectObjects( scene.children );
THREE.Raycaster对象从屏幕上的点击位置向场景中发射一束光线。
intersects 变量返回被击中对象的信息,来判断指定对象有没有被这束光线击中,相交的结果会以一个数组的形式返回,其中的元素依照距离排序,越近的排在越前。
方法名
.intersectObject ( object, recursive : Boolean, optionalTarget : Array )
参数
object - 检测与射线相交的物体
recursive- 若为 true 则检查后代对象,默认值为false
optionalTarget - (可选参数)用来设置方法返回的设置结果。若不设置则返回一个实例化的数组。如果设置,必须在每次调用之前清除这个数组(例如,array.length= 0;)
注意,对于网格,面(faces)必须朝向射线原点,这样才能被检测到;通过背面的射线的交叉点将不被检测到。为了光线投射一个对象的正反两面,你得设置 material 的 side 属性为 THREE.DoubleSide
返回值:
[ { distance, point, face, faceIndex, object }, … ]
distance - 射线的起点到相交点的距离
point - 在世界坐标中的交叉点
face -相交的面
faceIndex - 相交的面的索引
object - 相交的对象
uv - 交点的二维坐标
可以根据返回对象face属性,确定点击位置所处的模型的面。
比如在前面场景中增加一个功能,点击立方体的某个面让立方体超点击面的反方向移动。
代码语言:javascript复制 if(JSON.stringify(intersects[ i ].face.normal) == JSON.stringify(new THREE.Vector3(0,0,1)))
{
intersects[ i ].object.position.z = intersects[ i ].object.position.z - 10;
break;
}
if(JSON.stringify(intersects[ i ].face.normal) == JSON.stringify(new THREE.Vector3(0,0,-1)))
{
intersects[ i ].object.position.z = intersects[ i ].object.position.z 10;
break;
}
点击立方体的前后两个面,可以控制立方体前后移动。
用Raycaster来检测碰撞的原理很简单,我们需要以物体的中心为起点,向各个顶点(vertices)发出射线,然后检查射线是否与其它的物体相交。如果出现了相交的情况,检查最近的一个交点与射线起点间的距离,如果这个距离比射线起点至物体顶点间的距离要小,则说明发生了碰撞。