Three.js投影仪和Ray对象

我一直在尝试使用Projector和Ray类来做一些碰撞检测演示。 我已经开始尝试使用鼠标来select对象或拖动它们。 我已经看过使用这些对象的例子,但是他们似乎都没有解释投影机和Ray的某些方法在做什么。 我有一些问题,我希望有人会很容易回答。

究竟发生了什么,Projector.projectVector()和Projector.unprojectVector()之间有什么区别? 我注意到在所有使用投影仪和光线对象的例子中,在创build光线之前调用了未投影的方法。 你什么时候使用projectVector?

在这个演示中 ,我使用下面的代码在用鼠标拖动时旋转多维数据集。 有人可以用简单的术语解释,当我用mouse3D和相机取消投影,然后创buildRay时究竟发生了什么。 光线是否取决于对unprojectVector()的调用

/** Event fired when the mouse button is pressed down */ function onDocumentMouseDown(event) { event.preventDefault(); mouseDown = true; mouse3D.x = mouse2D.x = mouseDown2D.x = (event.clientX / window.innerWidth) * 2 - 1; mouse3D.y = mouse2D.y = mouseDown2D.y = -(event.clientY / window.innerHeight) * 2 + 1; mouse3D.z = 0.5; /** Project from camera through the mouse and create a ray */ projector.unprojectVector(mouse3D, camera); var ray = new THREE.Ray(camera.position, mouse3D.subSelf(camera.position).normalize()); var intersects = ray.intersectObject(crateMesh); // store intersecting objects if (intersects.length > 0) { SELECTED = intersects[0].object; var intersects = ray.intersectObject(plane); } } /** This event handler is only fired after the mouse down event and before the mouse up event and only when the mouse moves */ function onDocumentMouseMove(event) { event.preventDefault(); mouse3D.x = mouse2D.x = (event.clientX / window.innerWidth) * 2 - 1; mouse3D.y = mouse2D.y = -(event.clientY / window.innerHeight) * 2 + 1; mouse3D.z = 0.5; projector.unprojectVector(mouse3D, camera); var ray = new THREE.Ray(camera.position, mouse3D.subSelf(camera.position).normalize()); if (SELECTED) { var intersects = ray.intersectObject(plane); dragVector.sub(mouse2D, mouseDown2D); return; } var intersects = ray.intersectObject(crateMesh); if (intersects.length > 0) { if (INTERSECTED != intersects[0].object) { INTERSECTED = intersects[0].object; } } else { INTERSECTED = null; } } /** Removes event listeners when the mouse button is let go */ function onDocumentMouseUp(event) { event.preventDefault(); /** Update mouse position */ mouse3D.x = mouse2D.x = (event.clientX / window.innerWidth) * 2 - 1; mouse3D.y = mouse2D.y = -(event.clientY / window.innerHeight) * 2 + 1; mouse3D.z = 0.5; if (INTERSECTED) { SELECTED = null; } mouseDown = false; dragVector.set(0, 0); } /** Removes event listeners if the mouse runs off the renderer */ function onDocumentMouseOut(event) { event.preventDefault(); if (INTERSECTED) { plane.position.copy(INTERSECTED.position); SELECTED = null; } mouseDown = false; dragVector.set(0, 0); } 

基本上,您需要从3D世界空间和2D屏幕空间进行投影。

渲染器使用projectVector将3D点转换为2D屏幕。 unprojectVector基本上是做相反的,把2D点投射到3D世界里。 对于这两种方法,您都可以通过相机来查看场景。

所以,在这个代码中,你正在2D空间中创build一个归一化的vector。 说实话,我从来没有太确定z = 0.5逻辑。

 mouse3D.x = (event.clientX / window.innerWidth) * 2 - 1; mouse3D.y = -(event.clientY / window.innerHeight) * 2 + 1; mouse3D.z = 0.5; 

然后,这个代码使用相机投影matrix将其转换到我们的3D世界空间。

 projector.unprojectVector(mouse3D, camera); 

将鼠标三维点转换为三维空间,我们现在可以使用它来获取方向,然后使用相机位置投射光线。

 var ray = new THREE.Ray(camera.position, mouse3D.subSelf(camera.position).normalize()); var intersects = ray.intersectObject(plane); 

我发现我需要在表面之下进行更深入的工作,才能在示例代码的范围之外工作(例如使canvas不能填充屏幕或产生额外的效果)。 我在这里写了一篇关于它的博客文章。 这是一个缩短的版本,但应涵盖几乎所有我find的。

怎么做

以下代码(类似于@mrdoob提供的代码)将在点击时更改多维数据集的颜色:

  var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1, //x -( event.clientY / window.innerHeight ) * 2 + 1, //y 0.5 ); //z projector.unprojectVector( mouse3D, camera ); mouse3D.sub( camera.position ); mouse3D.normalize(); var raycaster = new THREE.Raycaster( camera.position, mouse3D ); var intersects = raycaster.intersectObjects( objects ); // Change color if hit block if ( intersects.length > 0 ) { intersects[ 0 ].object.material.color.setHex( Math.random() * 0xffffff ); } 

随着更新的three.js版本(r55和更高版本),你可以使用pickingRay来进一步简化事物,

  var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1, //x -( event.clientY / window.innerHeight ) * 2 + 1, //y 0.5 ); //z var raycaster = projector.pickingRay( mouse3D.clone(), camera ); var intersects = raycaster.intersectObjects( objects ); // Change color if hit block if ( intersects.length > 0 ) { intersects[ 0 ].object.material.color.setHex( Math.random() * 0xffffff ); } 

让我们坚持旧的方法,因为它可以更深入地了解发生了什么。 你可以在这里看到这个工作,只需点击立方体来改变它的颜色。

发生了什么?

  var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1, //x -( event.clientY / window.innerHeight ) * 2 + 1, //y 0.5 ); //z 

event.clientX是点击位置的x坐标。 按window.innerWidth划分给出点击的位置与整个窗口宽度的比例。 基本上,这是从左上angular的(0,0)到右下angular的( window.innerWidthwindow.innerHeight )开始的屏幕坐标转换到中心(0,0)的笛卡尔坐标,范围从(-1,-1)到(1,1),如下所示:

从网页坐标翻译

请注意,z的值为0.5。 这里我不会详细讨论z值,只是说这是远离摄像机的点的深度,我们沿着z轴投影到三维空间中。 稍后再说。

下一个:

  projector.unprojectVector( mouse3D, camera ); 

如果你看看three.js代码,你会发现这实际上是从3D世界到相机的投影matrix的倒置。 请记住,为了从3D世界坐标到屏幕上的投影,3D世界需要投影到相机的2D表面(这是您在屏幕上看到的)。 我们基本上是做相反的事情。

请注意,mouse3D现在将包含这个未投影的值。 这是沿着我们感兴趣的射线/轨迹的三维空间中的一个点的位置。确切点取决于z值(我们将在后面看到这一点)。

在这一点上,看看下面的图像可能是有用的:

相机,未投影的价值和射线

我们刚刚计算的点(mouse3D)由绿点表示。 请注意,点的大小纯粹是说明性的,它们与相机或鼠标三维点的大小无关。 我们对点的中心坐标更感兴趣。

现在,我们不仅仅需要三维空间中的一个点,而是需要一个光线/轨迹(用黑点表示),以便我们可以确定一个物体是否沿着这个光线/轨迹定位。 请注意,沿着光线显示的点只是任意点, 光线是从相机的方向,而不是一组点

幸运的是,因为我们沿着光线有一个点,我们知道轨迹必须从相机传递到这个点,我们可以确定光线的方向。 因此,下一步是从鼠标三维位置减去相机的位置,这将给出一个方向vector,而不是一个单一的点:

  mouse3D.sub( camera.position ); mouse3D.normalize(); 

我们现在有一个方向从相机到3D空间中的这一点(mouse3D现在包含这个方向)。 然后通过规范化它变成一个单位向量。

下一步是从相机位置开始创build一个光线(Raycaster),并使用方向(mouse3D)投射光线:

  var raycaster = new THREE.Raycaster( camera.position, mouse3D ); 

其余的代码决定3D空间中的物体是否与光线相交。 令人高兴的是,它使用intersectsObjects在幕后照顾我们。

演示

好的,让我们来看看我的网站上的演示,这些演示显示了这些射线在3D空间中的投射。 当你点击任何地方,相机围绕物体旋转,向你展示如何投射光线。 请注意,当相机返回到原来的位置时,您只能看到一个点。 这是因为所有其他的点都沿着投影线,因此被前点所阻挡。 这与当您向下看直接指向您的箭头线时相似 – 您所看到的只是基础。 当然,从直接向着你的方向看的箭头(你只能看到头部)也是一样,这通常是一个糟糕的情况。

z坐标

我们再看看那个z坐标。 在阅读本节时请参考本演示 ,并尝试使用不同的z值。

好的,再看看这个function:

  var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1, //x -( event.clientY / window.innerHeight ) * 2 + 1, //y 0.5 ); //z 

我们select0.5作为价值。 我之前提到,z坐标决定投影到3D的深度。 所以,让我们来看看z的不同值,看看它有什么影响。 为此,我在相机所在的位置放置了一个蓝点,从相机到未投影的位置放置了一排绿点。 然后,在计算完十字路口之后,我将摄像机移回到侧面来显示光线。 最好看到几个例子。

首先,az值为0.5:

z值为0.5

请注意从相机(蓝点)到未投影值(3D空间中的坐标)的绿色点线。 这就像枪pipe一样,指向射线应该投射的方向。 绿线基本上表示在归一化之前计算的方向。

好的,让我们试试0.9的值:

z值为0.9

正如你所看到的,绿线现在已经进一步扩展到三维空间。 0.99进一步扩大。

我不知道是否有什么重要的z值有多大。 似乎更大的价值会更精确(就像一个更长的枪pipe),但是由于我们正在计算方向,即使是短距离也应该是相当准确的。 我看到的例子使用0.5,所以这是我将坚持,除非另有说明。

当canvas不是全屏幕时投影

现在我们知道了更多关于正在发生的事情,我们可以找出当canvas没有填充窗口并且位于页面上时值应该是什么。 举个例子,说:

  • 包含three.jscanvas的div从左侧偏移X,从屏幕顶部偏移Y.
  • canvas的宽度等于viewWidth,高度等于viewHeight。

代码将是:

  var mouse3D = new THREE.Vector3( ( event.clientX - offsetX ) / viewWidth * 2 - 1, -( event.clientY - offsetY ) / viewHeight * 2 + 1, 0.5 ); 

基本上,我们正在做的是计算相对于canvas的鼠标点击的位置(对于x: event.clientX - offsetX )。 然后我们按比例确定发生点击的位置(对于x: /viewWidth ),类似于canvas填充窗口的时间。

就是这样,希望它有帮助。

从版本r70开始, Projector.unprojectVectorProjector.pickingRay已被弃用。 相反,我们有raycaster.setFromCamera这使得生活更容易find鼠标指针下的对象。

 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); 

intersects[0].object在鼠标指针下给出对象并intersects[0].point给出点击鼠标指针的对象上的点。

Projector.unprojectVector()将vec3视为一个位置。 在这个过程中,vector被翻译,所以我们使用.sub(camera.position) 。 另外我们需要在这个操作之后对它进行标准化。

我会添加一些graphics到这个职位,但现在我可以描述操作的几何形状。

我们可以将相机视为几何形状的金字塔。 实际上,我们用6个窗格(左,右,上,下,近和远)(接近尖端的平面)来定义它。

如果我们站在三维空间观察这些行动,我们就会看到这个金字塔处于一个任意的位置,并在空间中任意旋转。 可以说,这个金字塔的起源是在它的尖端,它的负Z轴跑向底部。

如果我们应用正确的matrix变换序列,那么最终被包含在这6个平面内的结果将最终呈现在屏幕上。 我的opengl是这样的:

 NDC_or_homogenous_coordinates = projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

这需要我们的网格从对象空间到世界空间,进入相机空间,最后它将投影做透视投影matrix,它基本上把所有东西都放到一个小的立方体(NDC,范围从-1到1)。

对象空间可以是一组整齐的xyz坐标,在这些坐标中,您可以在程序上生成一些东西,或者说,一个3d模型,一个艺术家使用对称性进行build模,从而与坐标空间整齐地坐在一起,而不是从一个像REVIT或AutoCAD。

一个objectMatrix可能发生在模型matrix和视图matrix之间,但这通常是事先处理好的。 说,翻转y和z,或者把远离原点的模型带入边界,转换单位等等。

如果我们认为我们的平面二维屏幕好像有深度,那么可以用与NDC立方体相同的方式来描述,尽pipe有些扭曲。 这就是为什么我们提供相机的宽高比。 如果我们想象一个正方形的屏幕高度的大小,其余的是我们需要调整我们的x坐标的纵横比。

现在回到三维空间。

我们站在3D场景中,看到金字塔。 如果我们切割金字塔周围的一切,然后把金字塔与其中包含的场景的一部分一起,把它的尖端在0,0,0,并指向底部朝着-Z轴,我们将在这里结束:

 viewMatrix * modelMatrix * position.xyzw 

把这个乘以投影matrix就好像我们把尖端一样,并开始在x轴和y轴上拉动它,从那个点上创build一个正方形,然后把金字塔变成一个盒子。

在这个过程中,框被缩放到-1和1,我们得到我们的透视投影,我们最终在这里:

 projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

在这个空间中,我们可以控制一个2维的鼠标事件。 由于它在我们的屏幕上,我们知道它是二维的,而且它在NDC立方体内的某处。 如果是二维的,我们可以说我们知道X和Y而不是Z,因此需要光线投射。

所以当我们投射射线时,我们基本上是通过立方体发出一条直线,垂直于它的一条边。

现在我们需要弄清楚光线是否到达场景中,为了做到这一点,我们需要将光线从这个立方体转换到适合计算的空间。 我们想要世界空间中的光芒。

雷在太空中是一条无限的线。 它与vector不同,因为它有一个方向,它必须通过一个空间点。 实际上,这就是Raycaster如何进行论证。

因此,如果我们沿着线条挤压盒子的顶部,回到金字塔中,线条将从顶端发出并向下运行,并相交于金字塔底部之间的某处 – mouse.x * farRange和-mouse.y * farRange。

(首先是-1和1,但是视图空间在世界范围内,只是旋转和移动)

由于这是摄像机的默认位置(可以说是物体空间),如果我们将它自己的世界matrix应用到光线上,我们会将其与摄像机一起转换。

由于光线通过0,0,0,我们只有它的方向和THREE.Vector3有一个方法转换方向:

 THREE.Vector3.transformDirection() 

它也标准化了这个过程中的向量。

上述方法中的Z坐标

这基本上可以与任何值一起工作,并且由于NDC立方体的工作原理而具有相同的作用。 近平面和远平面投影到-1和1上。

所以当你说的时候,

 [ mouse.x | mouse.y | someZpositive ] 

你通过一个点(mouse.x,mouse.y,1)在(0,0,someZpositive)方向发送一条线,

如果你把这个与盒子/金字塔的例子联系起来,这一点就在底部,而且由于线条源于相机,所以它也经历了这一点。

但是,在NDC空间中,这一点被拉伸到无限远,并且这条线最终与左,上,右,底平面平行。

用上面的方法不投射,基本上把它变成一个位置/点。 远平面刚好被映射到世界空间,所以我们的点位于z = -1的某个位置,在X和-1和1之间的相机方面和相机方面之间。

因为这是一个重点,应用相机的世界matrix不仅会旋转它,而且还会翻译它。 因此需要通过减去摄像机的位置来回到原点。