39

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

到底发生了什么,Projector.projectVector() 和 Projector.unprojectVector() 之间有什么区别?我注意到在所有使用投影仪和射线对象的示例中,似乎在创建射线之前调用了 unproject 方法。什么时候使用 projectVector?

我在这个演示中使用以下代码在用鼠标拖动时旋转立方体。有人可以简单地解释一下当我用 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

4 回答 4

81

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

怎么做

以下代码(类似于@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 和更高版本),您可以使用 pickRay 进一步简化事情,这样就变成了:

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

从网页坐标翻译

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

下一个:

    projector.unprojectVector( mouse3D, camera );

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

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

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

相机、未投影值和光线

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

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

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

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

我们现在有了从相机到 3D 空间中该点的方向(mouse3D 现在包含该方向)。然后通过对其进行归一化将其转换为单位向量。

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

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

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

演示

好的,让我们看一下我网站上的一个演示,显示了这些光线在 3D 空间中投射。当您单击任意位置时,相机会围绕对象旋转,以向您展示光线是如何投射的。请注意,当相机返回其原始位置时,您只会看到一个点。这是因为所有其他点都沿着投影线,因此被前面的点挡住了视线。这类似于当您向下看直接远离您的箭头线时 - 您所看到的只是底部。当然,在向下看直接朝你行进的箭头线时也是如此(你只能看到头部),这通常是一种糟糕的情况。

z 坐标

让我们再看一下 z 坐标。当您阅读本节并尝试不同的 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 值为 0.5

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

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

z 值为 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),类似于画布填充窗口的时间。

就是这样,希望对你有帮助。

于 2014-05-06T10:58:35.540 回答
53

基本上,您需要从 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);
于 2012-06-14T17:41:49.010 回答
21

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

于 2015-03-22T10:54:30.190 回答
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 坐标,您可以在其中通过程序生成一些东西,或者说,一个 3d 模型,艺术家使用对称性建模,因此与坐标空间整齐地对齐,而不是从类似的东西获得的建筑模型REVIT 或 AutoCAD。

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

如果我们认为我们的平面 2d 屏幕好像有深度,它可以被描述为与 NDC 立方体相同的方式,尽管略有失真。这就是我们为相机提供纵横比的原因。如果我们想象一个与屏幕高度相同的正方形,则余数就是我们需要缩放 x 坐标的纵横比。

现在回到 3d 空间。

我们站在一个 3d 场景中,我们看到了金字塔。如果我们切割金字塔周围的所有东西,然后将金字塔连同其中包含的场景部分一起取出,并将其尖端放在 0,0,0 处,并将底部指向 -z 轴,我们将在此处结束:

viewMatrix * modelMatrix * position.xyzw

将其乘以投影矩阵将与我们将尖端相乘,并开始将其在 x 和 y 轴上拉开,从该点创建一个正方形,并将金字塔变成一个盒子。

在这个过程中,盒子被缩放到 -1 和 1,我们得到了透视投影,我们在这里结束了:

projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

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

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

现在我们需要确定该光线是否击中场景中的某物,为了做到这一点,我们需要将来自该立方体的光线转换为适合计算的空间。我们想要世界空间中的光线。

射线是空间中的一条无限线。它与向量不同,因为它有方向,并且必须通过空间中的一个点。事实上,这就是 Raycaster 的论证方式。

所以如果我们沿着线挤压盒子的顶部,回到金字塔中,线将从尖端开始向下延伸并与金字塔底部相交--mouse.x * farRange和-mouse.y * 远距离。

(一开始是 -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 的某个位置,在 X 上的 -camera aspect 和 + cameraAspect 以及 y 上的 -1 和 1 之间。

因为它是一个点,所以应用相机世界矩阵不仅会旋转它,还会平移它。因此,需要通过减去相机位置将其带回原点。

于 2014-09-04T08:45:24.250 回答