我发现我需要更深入地了解示例代码范围之外的工作(例如画布不填满屏幕或具有其他效果)。我写了一篇关于它的博客文章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
)类似于画布填充窗口时的情况。
就是这样,希望有帮助。