Three.js 投影仪和射线对象

2023-12-24

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

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

我在此使用以下代码demo http://cmg0030.zxq.net/threejs/acclDemo7/用鼠标拖动时旋转立方体。有人可以用简单的术语解释一下当我使用 mouse3D 和相机取消投影然后创建光线时到底发生了什么。射线是否取决于对 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);
}

我发现我需要更深入地了解示例代码范围之外的工作(例如画布不填满屏幕或具有其他效果)。我写了一篇关于它的博客文章here http://barkofthebyte.azurewebsites.net/post/2014/05/05/three-js-projecting-mouse-clicks-to-a-3d-scene-how-to-do-it-and-how-it-works。这是一个简短的版本,但应该涵盖了我发现的几乎所有内容。

怎么做

以下代码(类似于 @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 http://barkofthebyte.azurewebsites.net/demos/projspikesfssimple.htm,只需单击立方体即可更改其颜色。

发生了什么?

    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 空间中的远离相机的点的深度。稍后会详细介绍这一点。

Next:

    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.

The Demo

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

z 坐标

让我们再看一下 z 坐标。参考这个演示 http://barkofthebyte.azurewebsites.net/demos/projspikesfs.htm当您阅读本节并尝试不同的 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:

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

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

正如您所看到的,绿线现在已进一步延伸到 3D 空间。 0.99 甚至更进一步。

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

画布非全屏时的投影

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

  • 包含 Three.js 画布的 div 距离屏幕左侧为 offsetX,距离屏幕顶部为 offsetY。
  • 画布的宽度等于视图宽度,高度等于视图高度。

代码将是:

    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)类似于画布填充窗口时的情况。

就是这样,希望有帮助。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Three.js 投影仪和射线对象 的相关文章

随机推荐