鼠标/画布 X、Y 坐标转换为 Three.js 世界坐标 X、Y、Z

85

我查找了很多例子,但都不能满足我的需求。我正在尝试将屏幕鼠标坐标转换为三维世界坐标,并考虑相机的影响。

我发现的解决方案都是通过光线交叉实现对象拾取。

我的目标是将Three.js对象的中心定位到当前鼠标“悬停”位置的坐标上。

我的相机位于x:0、y:0、z:500(尽管在模拟过程中它会移动),而我的所有对象都在z = 0处,具有不同的x和y值,因此我需要知道基于假定跟随鼠标位置的对象z = 0时的世界X、Y坐标。

这个问题看起来像是一个类似的问题,但没有解决方案:Getting coordinates of the mouse in relation to 3D space in THREE.js

给定屏幕上的鼠标位置,范围为“左上=0,0 | 右下=window.innerWidth,window.innerHeight”,是否有人可以提供一种将Three.js对象沿z = 0移动到鼠标坐标的解决方案?


1
嘿,罗布,没想到在这里遇见你 :) - acheo
你好,能否为这个案例提供一个小的jsfiddle? - utdev
11个回答

159

你不需要在场景中放置任何对象来完成这个操作。

你已经知道相机的位置。

使用vector.unproject( camera )函数,你可以获得一个指向所需方向的射线。

你只需要从相机位置开始延伸这条射线,一直延伸到射线末端的z坐标为零为止。

可以按照以下方式实现:

var vec = new THREE.Vector3(); // create once and reuse
var pos = new THREE.Vector3(); // create once and reuse

vec.set(
    ( event.clientX / window.innerWidth ) * 2 - 1,
    - ( event.clientY / window.innerHeight ) * 2 + 1,
    0.5 );

vec.unproject( camera );

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

var distance = - camera.position.z / vec.z;

pos.copy( camera.position ).add( vec.multiplyScalar( distance ) );

变量pos是指在三维空间中,鼠标下面并且在平面z=0上的点的位置。


编辑:如果您需要得到在平面z = targetZ下的鼠标下方点的位置,请用以下代码替换距离计算:

var distance = ( targetZ - camera.position.z ) / vec.z;

three.js r.98


4
对我有相同问题的完美回答。可以提到投影仪只需要实例化,不需要进行任何设置 - projector = new THREE.Projector(); - tonycoupland
9
我想我弄清楚了。如果你用 var distance = (targetZ - camera.position.z) / dir.z; 替换 var distance = -camera.position.z / dir.z;,你就可以指定 z 值(作为 targetZ)。请注意,这里的翻译尽可能保留原文意思和语法结构,并使其更加通俗易懂。 - Scott Thiessen
3
@WestLangley - 感谢您的详细解释,我想分享这个链接,它也帮助我理清了一些事情。其核心思想是,threejs空间中的所有内容都可以用坐标xyz在-1和1之间(NDC)来描述。对于x和y,通过重新规范化event.client变量来完成,对于z,则通过选择-1和1之间的任意值(即0.5)来完成。所选择的值对代码的其余部分没有影响,因为我们使用它来定义沿射线的方向。 - Matteo
2
@WestLangley - 最后一件事。说“unproject”函数返回在threejs空间中映射到向量中给定的x,y坐标的2D点是正确的吗?有一系列点映射到该2D点,因此我们要求指定z值的那个点。 - Matteo
2
@nnyby 这个答案中的 vec.xvec.y 在画布边缘必须返回 -1 和 +1;在中心返回 0。请参考此答案以确保画布在 div 中时的正确性。 - WestLangley
显示剩余23条评论

12

当我使用一个正交相机时,这对我起作用了。

let vector = new THREE.Vector3();
vector.set(
    (event.clientX / window.innerWidth) * 2 - 1,
    - (event.clientY / window.innerHeight) * 2 + 1,
    0
);
vector.unproject(camera);

WebGL three.js r.89


对于正交相机,这个方法对我很有效。谢谢!(这个也可以用,但它不像你的解决方案那么简单:https://dev59.com/D2cs5IYBdhLWcg3wMhGz#17423976。但是那个适用于非俯视相机,而我不确定这个是否适用。) - Venryx
3
如果使用React Three Fiber,您甚至可以缩短这个过程。上述公式将光标位置转换为NDC空间再进行反投影(https://threejs.org/docs/#api/en/math/Vector3.unproject),但是RTF已经在'useThree().mouse'中为您计算了这一点(即`const { mouse } = useThree(); vector.set(mouse.x, mouse.y, 0); vector.unproject(camera);`)。 - Sam Holmes
当我使用画布的宽度/高度而不是窗口的宽度/高度时,我发现计算结果更好。 - Get Off My Lawn

8
在r.58版本中,这段代码对我来说是有效的:
var planeZ = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
var mv = new THREE.Vector3(
    (event.clientX / window.innerWidth) * 2 - 1,
    -(event.clientY / window.innerHeight) * 2 + 1,
    0.5 );
var raycaster = projector.pickingRay(mv, camera);
var pos = raycaster.ray.intersectPlane(planeZ);
console.log("x: " + pos.x + ", y: " + pos.y);

5
为什么是0.5?似乎0.5可以是任何数,因为它在法线方向上。我尝试过用其他数字代替0.5,但看起来没有任何区别。 - resigned
对我来说,这个解决方案是最干净的。@ChrisSeddon:在pickingRay方法中,z坐标立即被覆盖。 - Pontus Granström
pickingRay已被删除,因此它无法与最新版本(截至2014年10月29日)一起使用。 - Pete Kozak
1
它说用raycaster.setFromCamera替换,但那不是来自投影仪,请使用new THREE.Raycaster(); - MistereeDevlord
这个可以用,但我在这里找到了一个更简单的解决方案(可能只适用于自上而下的相机):https://dev59.com/D2cs5IYBdhLWcg3wMhGz#48068550 - Venryx

5
以下是我基于WestLangley的回复编写的ES6类,在THREE.js r77中完美运行。
请注意,它假定您的渲染视口占用整个浏览器视口。
class CProjectMousePosToXYPlaneHelper
{
    constructor()
    {
        this.m_vPos = new THREE.Vector3();
        this.m_vDir = new THREE.Vector3();
    }

    Compute( nMouseX, nMouseY, Camera, vOutPos )
    {
        let vPos = this.m_vPos;
        let vDir = this.m_vDir;

        vPos.set(
            -1.0 + 2.0 * nMouseX / window.innerWidth,
            -1.0 + 2.0 * nMouseY / window.innerHeight,
            0.5
        ).unproject( Camera );

        // Calculate a unit vector from the camera to the projected position
        vDir.copy( vPos ).sub( Camera.position ).normalize();

        // Project onto z=0
        let flDistance = -Camera.position.z / vDir.z;
        vOutPos.copy( Camera.position ).add( vDir.multiplyScalar( flDistance ) );
    }
}

您可以像这样使用该类:
// Instantiate the helper and output pos once.
let Helper = new CProjectMousePosToXYPlaneHelper();
let vProjectedMousePos = new THREE.Vector3();

...

// In your event handler/tick function, do the projection.
Helper.Compute( e.clientX, e.clientY, Camera, vProjectedMousePos );

vProjectedMousePos现在包含在z=0平面上投影的鼠标位置。


3

我有一个画布,比我的整个窗口小,需要确定点击的世界坐标:

最初的回答:

// get the position of a canvas event in world coords
function getWorldCoords(e) {
  // get x,y coords into canvas where click occurred
  var rect = canvas.getBoundingClientRect(),
      x = e.clientX - rect.left,
      y = e.clientY - rect.top;
  // convert x,y to clip space; coords from top left, clockwise:
  // (-1,1), (1,1), (-1,-1), (1, -1)
  var mouse = new THREE.Vector3();
  mouse.x = ( (x / canvas.clientWidth ) * 2) - 1;
  mouse.y = (-(y / canvas.clientHeight) * 2) + 1;
  mouse.z = 0.5; // set to z position of mesh objects
  // reverse projection from 3D to screen
  mouse.unproject(camera);
  // convert from point to a direction
  mouse.sub(camera.position).normalize();
  // scale the projected ray
  var distance = -camera.position.z / mouse.z,
      scaled = mouse.multiplyScalar(distance),
      coords = camera.position.clone().add(scaled);
  return coords;
}

var canvas = renderer.domElement;
canvas.addEventListener('click', getWorldCoords);

这是一个例子。在滑动前后点击甜甜圈的同一区域,您会发现坐标保持不变(请检查浏览器控制台): "最初的回答"

// three.js boilerplate
var container = document.querySelector('body'),
    w = container.clientWidth,
    h = container.clientHeight,
    scene = new THREE.Scene(),
    camera = new THREE.PerspectiveCamera(75, w/h, 0.001, 100),
    controls = new THREE.MapControls(camera, container),
    renderConfig = {antialias: true, alpha: true},
    renderer = new THREE.WebGLRenderer(renderConfig);
controls.panSpeed = 0.4;
camera.position.set(0, 0, -10);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(w, h);
container.appendChild(renderer.domElement);

window.addEventListener('resize', function() {
  w = container.clientWidth;
  h = container.clientHeight;
  camera.aspect = w/h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h);
})

function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
  controls.update();
}

// draw some geometries
var geometry = new THREE.TorusGeometry( 10, 3, 16, 100, );
var material = new THREE.MeshNormalMaterial( { color: 0xffff00, } );
var torus = new THREE.Mesh( geometry, material, );
scene.add( torus );

// convert click coords to world space
// get the position of a canvas event in world coords
function getWorldCoords(e) {
  // get x,y coords into canvas where click occurred
  var rect = canvas.getBoundingClientRect(),
      x = e.clientX - rect.left,
      y = e.clientY - rect.top;
  // convert x,y to clip space; coords from top left, clockwise:
  // (-1,1), (1,1), (-1,-1), (1, -1)
  var mouse = new THREE.Vector3();
  mouse.x = ( (x / canvas.clientWidth ) * 2) - 1;
  mouse.y = (-(y / canvas.clientHeight) * 2) + 1;
  mouse.z = 0.0; // set to z position of mesh objects
  // reverse projection from 3D to screen
  mouse.unproject(camera);
  // convert from point to a direction
  mouse.sub(camera.position).normalize();
  // scale the projected ray
  var distance = -camera.position.z / mouse.z,
      scaled = mouse.multiplyScalar(distance),
      coords = camera.position.clone().add(scaled);
  console.log(mouse, coords.x, coords.y, coords.z);
}

var canvas = renderer.domElement;
canvas.addEventListener('click', getWorldCoords);

render();
html,
body {
  width: 100%;
  height: 100%;
  background: #000;
}
body {
  margin: 0;
  overflow: hidden;
}
canvas {
  width: 100%;
  height: 100%;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/97/three.min.js'></script>
<script src=' https://threejs.org/examples/js/controls/MapControls.js'></script>


1
对我来说,这段代码显示了一个不变的黑屏。 - Rylan Schaeffer
很可能是因为three.js API已经发生了变化。上面的代码只能在three.js版本97中运行。 - duhaime

2

要获取3D对象的鼠标坐标,请使用projectVector:

var width = 640, height = 480;
var widthHalf = width / 2, heightHalf = height / 2;

var projector = new THREE.Projector();
var vector = projector.projectVector( object.matrixWorld.getPosition().clone(), camera );

vector.x = ( vector.x * widthHalf ) + widthHalf;
vector.y = - ( vector.y * heightHalf ) + heightHalf;

要获取与特定鼠标坐标相关的three.js 3D坐标,请使用相反的unprojectVector:

var elem = renderer.domElement, 
    boundingRect = elem.getBoundingClientRect(),
    x = (event.clientX - boundingRect.left) * (elem.width / boundingRect.width),
    y = (event.clientY - boundingRect.top) * (elem.height / boundingRect.height);

var vector = new THREE.Vector3( 
    ( x / WIDTH ) * 2 - 1, 
    - ( y / HEIGHT ) * 2 + 1, 
    0.5 
);

projector.unprojectVector( vector, camera );
var ray = new THREE.Ray( camera.position, vector.subSelf( camera.position ).normalize() );
var intersects = ray.intersectObjects( scene.children );

这里有一个很好的例子(链接)。但是,要使用project vector,必须有用户点击的对象。intersects将是鼠标位置处所有对象的数组,而不考虑它们的深度。


好的,那么我将对象的位置分配给x: vector.x,y: vector.y,z: 0? - Rob Evans
我不确定我理解了,您是想将对象移动到鼠标位置,还是找到对象的鼠标位置?您是从鼠标坐标转换为three.js坐标,还是反过来? - BishopZ
谢谢你的回复。这已经接近目标了,但我已经找到了有关查找现有对象交集的帖子。我需要的是,如果世界除了相机之外是空的,我如何在鼠标单击的位置创建一个新对象,并在移动该对象时将其移动到鼠标位置。 - Rob Evans
我认为实现这个的方法是不要有一个空的世界,而是在远处放置许多透明的物体来捕捉投射的光线。 - BishopZ
嗯,我明白了。我考虑在世界中添加一个平面来进行交集检查,但这意味着该平面需要很大,或者至少足够大以填充视口(以捕获任何潜在的点击交集),然后随着相机移动。虽然要求场景中有对象似乎是一种不优雅的解决方案...但肯定有一种方法可以在没有它们的情况下完成这个任务。我想知道是否可以沿着射线的向量追踪到z=0点?不过我不确定如何做到这一点... - Rob Evans
显示剩余2条评论

1

ThreeJS正在逐渐远离Projector.(Un)ProjectVector以及使用projector.pickingRay()的解决方案不再适用,我刚刚更新了自己的代码...所以最近可用的工作版本应该如下:

var rayVector = new THREE.Vector3(0, 0, 0.5);
var camera = new THREE.PerspectiveCamera(fov,this.offsetWidth/this.offsetHeight,0.1,farFrustum);
var raycaster = new THREE.Raycaster();
var scene = new THREE.Scene();

//...

function intersectObjects(x, y, planeOnly) {
  rayVector.set(((x/this.offsetWidth)*2-1), (1-(y/this.offsetHeight)*2), 1).unproject(camera);
  raycaster.set(camera.position, rayVector.sub(camera.position ).normalize());
  var intersects = raycaster.intersectObjects(scene.children);
  return intersects;
}

1
对于使用@react-three/fiber(也称为r3f和react-three-fiber)的用户,我发现Matt Rossman的这篇讨论及其相关的代码示例非常有帮助。特别是,许多使用上述方法的示例仅适用于简单的正交视图,而不适用于OrbitControls在使用时的情况。
讨论链接:https://github.com/pmndrs/react-three-fiber/discussions/857 使用Matt技术的简单示例:https://codesandbox.io/s/r3f-mouse-to-world-elh73?file=/src/index.js 更通用的示例:https://codesandbox.io/s/react-three-draggable-cxu37?file=/src/App.js

我该如何稍微减缓以下代码的速度?这样物体就不会立即到达与鼠标相同的位置,而是跟随鼠标移动。 - Suisse
顺便说一句,这应该是正确的答案,因为它几乎只有一行代码。 - Suisse

1
这是一个当前的答案(THREE.REVISION==157),它适用于旋转的摄像机和小于窗口的渲染器,并且可以找到地面上的一个点。(您可以替换为任何其他平面。)
const raycaster = new THREE.Raycaster()
const pt = new THREE.Vector3()
renderer.domElement.addEventListener('mousemove', evt => {
  const rect = evt.target.getBoundingClientRect()

  // Update the vector to use normalized screen coordinates [-1,1]
  pt.set(
    ((evt.clientX - rect.left) / rect.width) * 2 - 1,
    ((rect.top - evt.clientY) / rect.height) * 2 + 1,
    1
  )

  raycaster.setFromCamera(pt, camera)

  // Intersecting with the ground plane, where +Y is up
  const ground = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
  const pointHitsPlane = raycaster.ray.intersectPlane(ground, pt);
  if (pointHitsPlane) {
    // pt is now a position in the 3D world
    // move a global test object to that location to verify
    testObject.position.copy(pt)
  }
})

0

虽然提供的答案在某些情况下可能有用,但我几乎无法想象这些情况(也许是游戏或动画),因为它们根本不精确(猜测目标的NDC z?)。如果您知道目标z平面,则无法使用这些方法将屏幕坐标转换为世界坐标。但对于大多数情况,您应该知道这个平面。

例如,如果您通过中心(模型空间中已知点)和半径绘制球体-您需要将半径作为未投影鼠标坐标的增量获取-但您无法!尽管尊重@WestLangley的带有targetZ的方法不起作用,它会给出错误的结果(如果需要,我可以提供jsfiddle)。另一个例子-您需要通过鼠标双击设置轨道控件目标,但没有“真正”的场景对象光线投射(当您没有任何东西可选择时)。

对我来说,解决方案就是在沿着z轴的目标点上创建虚拟平面,然后在此平面上使用光线投射。目标点可以是当前轨道控件目标或您需要逐步在现有模型空间中绘制对象的顶点等。这很完美,而且很简单(typescript示例):

screenToWorld(v2D: THREE.Vector2, camera: THREE.PerspectiveCamera = null, target: THREE.Vector3 = null): THREE.Vector3 {
    const self = this;

    const vNdc = self.toNdc(v2D);
    return self.ndcToWorld(vNdc, camera, target);
}

//get normalized device cartesian coordinates (NDC) with center (0, 0) and ranging from (-1, -1) to (1, 1)
toNdc(v: THREE.Vector2): THREE.Vector2 {
    const self = this;

    const canvasEl = self.renderers.WebGL.domElement;

    const bounds = canvasEl.getBoundingClientRect();        

    let x = v.x - bounds.left;      

    let y = v.y - bounds.top;       

    x = (x / bounds.width) * 2 - 1;     

    y = - (y / bounds.height) * 2 + 1;      

    return new THREE.Vector2(x, y);     
}

ndcToWorld(vNdc: THREE.Vector2, camera: THREE.PerspectiveCamera = null, target: THREE.Vector3 = null): THREE.Vector3 {
    const self = this;      

    if (!camera) {
        camera = self.camera;
    }

    if (!target) {
        target = self.getTarget();
    }

    const position = camera.position.clone();

    const origin = self.scene.position.clone();

    const v3D = target.clone();

    self.raycaster.setFromCamera(vNdc, camera);

    const normal = new THREE.Vector3(0, 0, 1);

    const distance = normal.dot(origin.sub(v3D));       

    const plane = new THREE.Plane(normal, distance);

    self.raycaster.ray.intersectPlane(plane, v3D);

    return v3D; 
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接