Three.js投影仪和Ray对象

Cor*_*oss 38 javascript 3d three.js

我一直在尝试使用Projector和Ray类来进行一些碰撞检测演示.我已经开始尝试使用鼠标选择对象或拖动它们.我已经查看了使用这些对象的示例,但是它们似乎没有任何评论可以解释投影仪和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);
}
Run Code Online (Sandbox Code Playgroud)

aca*_*lon 72

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

怎么做

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

使用更新的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 );
    }
Run Code Online (Sandbox Code Playgroud)

让我们坚持使用旧方法,因为它可以更深入地了解幕后发生的事情.您可以在此处看到此工作,只需单击立方体即可更改其颜色.

发生了什么?

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

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 );
Run Code Online (Sandbox Code Playgroud)

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

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

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

相机,未投影的值和光线

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

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

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

    mouse3D.sub( camera.position );                
    mouse3D.normalize();
Run Code Online (Sandbox Code Playgroud)

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

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

    var raycaster = new THREE.Raycaster( camera.position, mouse3D );
Run Code Online (Sandbox Code Playgroud)

其余代码确定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  
Run Code Online (Sandbox Code Playgroud)

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

首先,az值为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 );
Run Code Online (Sandbox Code Playgroud)

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

就是这样,希望它有所帮助.

  • 非常棒的答案,如果可以的话,+ 10. (3认同)
  • 赞成。图形对于我们视觉人士来说是很棒的。我认为这个答案要彻底得多。 (2认同)
  • 还有很棒的演示.这让我理解得比接受的答案好得多 (2认同)

mrd*_*oob 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;
Run Code Online (Sandbox Code Playgroud)

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

projector.unprojectVector(mouse3D, camera);
Run Code Online (Sandbox Code Playgroud)

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

var ray = new THREE.Ray(camera.position, mouse3D.subSelf(camera.position).normalize());
var intersects = ray.intersectObject(plane);
Run Code Online (Sandbox Code Playgroud)

  • 需要0.5.有时它也需要1. (5认同)
  • @mrdoob谢谢.您是否能够解释为什么z需要为0.5,何时需要为1?ps我猜p95上的"WebGL:Up and Running"这本书是错误的,当它说视口坐标范围从-0.5到+0.5时... (4认同)
  • API发生了很大的变化.使用您在此示例中找到的代码:http://threejs.org/examples/canvas_interactive_voxelpainter.html (3认同)
  • 不确定碰撞代码.它被删除了,因为它重复了做事的方式而且没有维护.你认为Ray&co不能做什么? (2认同)

Pra*_*gam 19

从版本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);
Run Code Online (Sandbox Code Playgroud)

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