鼠标/画布X,Y到Three.js世界X,Y,Z


69

我到处搜索了一个与我的用例匹配但找不到的示例。我正在尝试将摄像头的屏幕鼠标坐标转换为3D世界坐标。

解决方案我发现所有方法都可以进行射线相交以实现对象拾取。

我想做的是将Three.js对象的中心定位在鼠标当前“在”上方的坐标上。

我的相机位于x:0,y:0,z:500(尽管它会在模拟过程中移动),并且我的所有对象都位于z = 0,且x和y值不同,所以我需要了解基于X,Y的世界假设将跟随鼠标位置的对象的az = 0。

这个问题看起来像一个类似的问题,但没有解决方案:在THREE.js中获取鼠标相对于3D空间的坐标

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


1
嗨,罗布(Hey Rob)花式在这里
遇到

您好,您可以在这种情况下发布一些jsfiddle吗?
utdev

Answers:


130

您无需在场景中放置任何对象即可执行此操作。

您已经知道相机的位置。

使用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是点在3D空间中,“鼠标下方”和平面中的位置z=0


编辑:如果您需要点“在鼠标下”和在平面中z = targetZ,将距离计算替换为:

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

three.js r.98


4
对我所遇到的同一问题的完美答案。可以提及的是,可以仅实例化放映机,而无需进行任何设置-放映机= new THREE.Projector();。
tonycoupland 2012年

8
我想我知道了。如果您替换var distance = -camera.position.z / dir.z;var distance = (targetZ - camera.position.z) / dir.z;,你可以指定z值(targetZ)。
Scott Thiessen 2015年

1
@WestLangley-感谢您的出色回答。我只有一个疑问,请问为什么在第6行中向量的z坐标设置为0.5?当我们要指定z的值不同于0时(例如在SCCOTTT的情况下),该行也会有所不同吗?
马泰奥

1
@Matteo代码正在“取消保护”从标准化设备坐标(NDC)空间到世界空间的点。0.5的值是任意的。如果您不了解Google NDC空间的概念。
WestLangley

2
@WestLangley-最后一件事。声明unproject函数正在返回Threejs空间中的点,该点投影到向量中给定的坐标x,y的2d点是否正确?一束点映射到该2d点,因此我们要求具有指定z值的点。
Matteo,

5

在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);

3
为什么是0.5?看起来0.5可以是任何值,因为它是在法线方向上。我已经尝试过使用其他数字,但似乎没有任何区别。
2014年

对我来说,这种解决方案是最干净的。@ChrisSeddon:方法中的z-coordinate立即被覆盖pickingRay
PontusGranström2014年

pickingRay已被删除,因此不适用于最新版本(截至2014
Pete Kozak 2014年

它说被raycaster.setFromCamera代替,但不是从投影机使用新的THREE.Raycaster();。
MistereeDevlord 2015年

这行得通,但是我在这里找到了一个更简单的解决方案(尽管可能仅适用于自上而下的相机):stackoverflow.com/a/48068550/2441655
Venryx

3

以下是我根据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平面上的投影鼠标位置。


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 );

有一个很好的例子在这里。但是,要使用项目向量,用户单击时必须有一个对象。相交将是鼠标位置处所有对象的数组,无论其深度如何。


很酷,因此我将对象的位置分配给x:vector.x,y:vector.y,z:0?
罗布·埃文斯

我不确定,您是要尝试将对象移动到鼠标位置还是要找到对象的鼠标位置?您是要从鼠标坐标转换成three.js坐标,还是反过来?
BishopZ 2012年

实际上,这看起来不正确……object.matrixWorld.getPosition()。clone()来自哪里?没有对象开始,我想创建一个新对象并将其放置在发生鼠标事件的位置。
罗布·埃文斯

刚刚看到了您的最后一条消息,是的,将对象移动到鼠标位置:)
Rob Evans

感谢那。差不多了,但是我已经找到了一些帖子来查找现有对象的交集。我需要的是如果相机之外的世界都是空的,我将如何在单击鼠标的位置创建一个新对象,然后在移动该对象时继续将其移动到鼠标位置。
Rob Evans

2

当使用 orthographic camera

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


为我工作,为正交摄影机工作。谢谢!(这里的另一台相机也可以使用,但是它不像您的解决方案那么简单:stackoverflow.com/a/17423976/2441655。但是,该相机应该适用于非自上而下的相机,而我不确定是否可以使用它会。)
Venryx

2

我的画布小于我的整个窗口,需要确定单击的世界坐标:

// 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

ThreeJS慢慢地从Projector移开了。(Un)ProjectVector以及使用projection.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;
}

0

这是我从中创建es6类的方法。使用Three.js r83。使用rayCaster的方法来自mrdoob:Three.js投影仪和Ray对象

    export default class RaycasterHelper
    {
      constructor (camera, scene) {
        this.camera = camera
        this.scene = scene
        this.rayCaster = new THREE.Raycaster()
        this.tapPos3D = new THREE.Vector3()
        this.getIntersectsFromTap = this.getIntersectsFromTap.bind(this)
      }
      // objects arg below needs to be an array of Three objects in the scene 
      getIntersectsFromTap (tapX, tapY, objects) {
        this.tapPos3D.set((tapX / window.innerWidth) * 2 - 1, -(tapY / 
        window.innerHeight) * 2 + 1, 0.5) // z = 0.5 important!
        this.tapPos3D.unproject(this.camera)
        this.rayCaster.set(this.camera.position, 
        this.tapPos3D.sub(this.camera.position).normalize())
        return this.rayCaster.intersectObjects(objects, false)
      }
    }

如果要检查场景中的所有对象是否有命中,可以使用此方法。我在上面将递归标志设置为false,因为对于我的用途,我不需要使用它。

var helper = new RaycasterHelper(camera, scene)
var intersects = helper.getIntersectsFromTap(tapX, tapY, 
this.scene.children)
...

0

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

例如,如果您按中心(模型空间中的已知点)和半径绘制球体-您需要将半径作为未投影的鼠标坐标的增量-但是您不能!出于所有应有的尊重,使用TargetZ的@WestLangley方法不起作用,它给出了错误的结果(如果需要,我可以提供jsfiddle)。另一个示例-您需要通过双击鼠标来设置轨道控制目标,但不能对场景对象进行“真实的”光线投射(当您没有其他选择时)。

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

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; 
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.