Three.js投影仪和Ray对象

时间:2012-06-14 15:10:52

标签: javascript 3d three.js

我一直在尝试使用Projector和Ray类来进行一些碰撞检测演示。我已经开始尝试使用鼠标选择对象或拖动它们。我已经查看了使用这些对象的示例,但是它们似乎没有任何评论可以解释Projector和Ray正在做什么的一些方法。我有几个问题,我希望有人能回答这些问题。

究竟发生了什么以及Projector.projectVector()和Projector.unprojectVector()之间的区别是什么?我注意到在使用投影仪和光线对象的所有示例中似乎都是unproject方法在创建光线之前调用。 您何时使用projectVector?

我在 demo 中使用以下代码,以便在使用鼠标拖动时旋转多维数据集。 有人可以用简单的术语解释当我使用mouse3D和相机进行取消投影然后创建Ray时究竟发生了什么。光线是否依赖于对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);
}

4 个答案:

答案 0 :(得分:71)

我发现我需要在表面下更深入地在示例代码范围之外工作(例如使用不填充屏幕或具有其他效果的画布)。我写了一篇关于它的博客文章 here 。这是一个缩短版本,但应该涵盖我发现的几乎所有内容。

怎么做

以下代码(类似于@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 );
    }

让我们坚持使用旧方法,因为它可以更深入地了解幕后发生的事情。您可以看到此工作 here ,只需点击多维数据集即可更改其颜色。

发生了什么?

    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得出点击的位置与整个窗口宽度成比例。基本上,这是从从左上角的(0,0)开始到右下角的(window.innerWidthwindow.innerHeight)的屏幕坐标转换为中心的笛卡尔坐标(0,0) ),范围从(-1,-1)到(1,1),如下所示:

translation from web page coordinates

请注意,z的值为0.5。我不会在这一点上详细了解z值,只是说这是我们沿着z轴投影到3D空间的远离相机的点的深度。稍后会详细介绍。

下一步:

    projector.unprojectVector( mouse3D, camera );

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

请注意,mouse3D现在将包含此未投影的值。这是我们感兴趣的沿着光线/轨迹的3D空间中的点的位置。精确点取决于z值(稍后我们将看到)。

此时,查看以下图像可能很有用:

Camera, unprojected value and ray

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

现在,我们不想在3D空间中使用单个点,而是想要一个光线/轨迹(由黑点表示),以便我们可以确定物体是否沿着此光线/轨迹定位。请注意,沿光线显示的点只是任意点,光线是来自相机的方向,而不是一组点

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

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

我们现在在3D空间中有一个从相机到此点的方向(mouse3D现在包含此方向)。然后通过标准化将其转换为单位矢量。

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

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

其余代码确定3D空间中的对象是否与光线相交。令人高兴的是,使用intersectsObjects在幕后处理我们。

演示

好的,让我们看看我的网站 here 中的一个演示,该演示显示这些光线在3D空间中投射。单击任意位置时,相机会围绕对象旋转,以显示光线的投射方式。请注意,当相机返回其原始位置时,您只能看到一个点。这是因为所有其他点都沿着投影线,因此被前点阻挡。这类似于当你向下看直接指向你的箭头线时 - 你看到的只是基础。当然,同样适用于向下看直接朝向你的箭头线(你只看到头部),这通常是一个糟糕的情况。

z坐标

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

好的,让我们再看一下这个函数:

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

我们选择0.5作为值。我之前提到过z坐标决定投影到3D的深度。所以,让我们看看z的不同值,看看它有什么效果。为此,我在相机所在的位置放置了一个蓝点,并在相机和未投影位置放置了一条绿点。然后,在计算出交叉点后,我将相机向后移动到侧面以显示光线。最好看一些例子。

首先,z值为0.5:

z value of 0.5

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

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

z value of 0.9

如您所见,绿线现在已进一步扩展到3D空间。 0.99进一步延伸。

我不知道z的价值有多大是否重要。似乎更大的值会更精确(如更长的枪管),但由于我们正在计算方向,即使是短距离也应该非常准确。我见过的例子使用0.5,所以除非另有说明,否则我将坚持使用。

画布不是全屏时的投影

现在我们对正在发生的事情了解得更多,我们可以弄清楚当画布没有填满窗口并位于页面上时应该是什么值。例如,说:

  • 包含three.js画布的div是左边的offsetX和屏幕顶部的offsetY。
  • 画布的宽度等于viewWidth,高度等于viewHeight。

代码将是:

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

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

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

答案 1 :(得分:50)

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

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

因此,在此代码中,您将在2D空间中创建规范化矢量。说实话,我对z = 0.5逻辑一直不太确定。

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

然后,此代码使用相机投影矩阵将其转换为我们的3D世界空间。

projector.unprojectVector(mouse3D, camera);

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

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

答案 2 :(得分:18)

自版本r70起,Projector.unprojectVectorProjector.pickingRay已弃用。相反,我们有raycaster.setFromCamera,这使得在鼠标指针下查找对象的生活更加轻松。

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给出点击鼠标指针的对象上的点。

答案 3 :(得分:1)

Projector.unprojectVector()将vec3视为一个位置。在此过程中,矢量被翻译,因此我们在其上使用 .sub(camera.position) 。此外,我们需要在此操作之后将其标准化。

我将在这篇文章中添加一些图形但是现在我可以描述操作的几何形状。

我们可以将相机视为几何学上的金字塔。事实上,我们用6个窗格定义它 - 左,右,顶部,底部,近处和远处(靠近最靠近尖端的平面)。

如果我们站在一些3d并观察这些操作,我们会看到这个金字塔处于任意位置,在空间中任意旋转。让我们说这个金字塔的起源在它的尖端,它的负z轴向底部延伸。

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

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

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

对象空间可以是一组整齐的xyz坐标,您可以在其中生成一些程序性的或者说三维模型,艺术家使用对称建模并因此整齐地坐在与坐标空间对齐,而不是从说REVIT或AutoCAD之类的东西。

objectMatrix可能发生在模型矩阵和视图矩阵之间,但这通常需要提前处理。比如说,翻转y和z,或者将远离原点的模型带入边界,转换单位等。

如果我们认为我们的平面2d屏幕好像它有深度,它可以用与NDC立方体相同的方式描述,虽然稍微扭曲。这就是我们为相机提供宽高比的原因。如果我们想象一个屏幕高度大小的正方形,剩下的就是我们需要缩放x坐标所需的宽高比。

现在回到3d空间。

我们站在一个三维场景中,我们看到了金字塔。如果我们切割金字塔周围的一切,然后将金字塔与其中包含的场景的一部分一起取出并将其尖端放在0,0,0,并将底部指向-z轴,我们将结束在这里:

viewMatrix * modelMatrix * position.xyzw

将它乘以投影矩阵将与我们采用尖端相同,并开始在x和y轴上拉动它,从而创建该点的一个正方形,并将金字塔变成一个框。

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

projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

在这个空间中,我们可以控制二维鼠标事件。由于它在我们的屏幕上,我们知道它是二维的,并且它在NDC多维数据集中的某个地方。如果它是二维的,我们可以说我们知道X和Y但不知道Z,因此需要光线投射。

因此,当我们投射光线时,我们基本上是通过立方体发出一条线,垂直于它的一侧。

现在我们需要弄清楚那条光线是否会碰到场景中的某些东西,为了做到这一点,我们需要将光线从这个立方体转换成适合计算的空间。我们希望光线在世界空间。

雷是太空中的无限线。它与矢量不同,因为它有一个方向,它必须通过一个空间点。事实上,这就是雷卡斯特的观点。

因此,如果我们将线条顶部与线条一起挤回金字塔,线条将从尖端开始向下延伸并与金字塔底部相交 - mouse.x * farRange和 - mouse.y * farRange。

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

由于这是相机的默认位置(即它的物体空间),如果我们将它自己的世界矩阵应用于光线,我们将把它与相机一起变换。

由于光线通过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,介于-camera aspect和+ cameraAspect之间的X和-1以及y上的1。

因为它是一个点,应用相机世界矩阵不仅会旋转它,还会翻译它。因此需要通过减去摄像机位置将其恢复到原点。