【问题标题】:Mouse / Canvas X, Y to Three.js World X, Y, ZMouse / Canvas X, Y to Three.js World X, Y, Z
【发布时间】:2012-10-14 20:19:59
【问题描述】:

我搜索了一个与我的用例匹配但找不到的示例。考虑到相机,我正在尝试将屏幕鼠标坐标转换为 3D 世界坐标。

我发现的所有解决方案都通过光线相交来实现对象拾取。

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

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

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

鉴于鼠标在屏幕上的位置范围为“top-left = 0, 0 | bottom-right = window.innerWidth, window.innerHeight”,任何人都可以提供将 Three.js 对象移动到鼠标的解决方案沿 z = 0 的坐标?

【问题讨论】:

  • 嘿,Rob 喜欢在这里碰到你 :)
  • 嗨,你能为这个案例发布一个小 jsfiddle 吗?

标签: three.js point projection mouse-coordinates


【解决方案1】:

您不需要在场景中拥有任何对象即可执行此操作。

您已经知道相机位置。

使用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

【讨论】:

  • 完美回答我的同一个问题。可以提一下,投影仪可以仅被实例化,并且无论如何都不需要设置 - 投影仪 = new THREE.Projector();
  • 我想我明白了。如果将var distance = -camera.position.z / dir.z; 替换为var distance = (targetZ - camera.position.z) / dir.z;,则可以指定z 值(如targetZ)。
  • @WestLangley - 感谢您的出色回答。我只有一个疑问,请您解释一下为什么第 6 行中向量的 z 坐标设置为 0.5?当我们想要指定一个不同于 0 的 z 值时(如 SCCOTTT 的情况),该行是否也会有所不同?
  • @Matteo 该代码正在“取消保护”从标准化设备坐标 (NDC) 空间到世界空间的点。 0.5 值是任意的。如果您不了解这个概念,请使用 Google NDC 空间。
  • @WestLangley - 感谢您的详细说明,我想分享这个link,这也帮助我解决了问题。这个想法是,threejs 空间中的所有内容都可以用 -1 和 1 (NDC) 之间的坐标 xyz 来描述。对于通过重新规范化 event.client 变量来完成的 x 和 y,对于 z 是通过选择 -1 和 1 之间的任意值(即 0.5)来完成的。选择的值对其余代码没有影响,因为我们使用它来定义沿射线的方向。
【解决方案2】:

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

【讨论】:

  • 为什么是 0.5?看起来好像 0.5 可以是任何东西,因为它在法线的方向上。我用其他数字试过了,似乎没有任何区别。
  • 对我来说,这个解决方案是最干净的。 @ChrisSeddon:z 坐标会立即在 pickingRay 方法中被覆盖。
  • pickingRay 已被删除,因此这不适用于最新版本(截至 2014 年 10 月 29 日)
  • 它说替换为 raycaster.setFromCamera 但不是来自投影仪使用 new THREE.Raycaster();
  • 这可行,但我在这里找到了一个更简单的解决方案(但可能只适用于自上而下的相机):stackoverflow.com/a/48068550/2441655
【解决方案3】:

这对我有用 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。但是那个应该适用于非自上而下的相机,而这个我不确定它是否可以。)
  • 如果使用 React Three Fiber,你可以进一步缩短它。上面的公式在取消投影之前将光标位置转换为 NDC 空间 (threejs.org/docs/#api/en/math/Vector3.unproject),但 RTF 已经在 useThree().mouse 中为您计算了这一点(即const { mouse } = useThree(); vector.set(mouse.x, mouse.y, 0); vector.unproject(camera);
【解决方案4】:

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

【讨论】:

    【解决方案5】:

    要获取 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 );
    

    有一个很好的例子here。但是,要使用投影矢量,必须有用户单击的对象。 intersects 将是鼠标位置处所有对象的数组,无论其深度如何。

    【讨论】:

    • 酷,然后我将对象的位置分配给x:vector.x,y:vector.y,z:0?
    • 不确定我是否理解,您是想将对象移动到鼠标位置,还是要找到对象的鼠标位置?你是从鼠标坐标到 three.js 坐标,还是相反?
    • 其实这看起来不太对... object.matrixWorld.getPosition().clone() 来自哪里?没有对象可开始,我想创建一个新对象并将其放置在鼠标事件发生的位置。
    • 刚刚看到你的最后一条消息,是的,将一个对象移动到鼠标位置:)
    • 谢谢。它几乎就在那里,但我已经找到了找到现有对象交集的帖子。我需要的是,如果除了相机之外世界是空的,我将如何创建一个单击鼠标的新对象,然后在移动时继续将该对象移动到鼠标位置。
    【解决方案6】:

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

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

    【讨论】:

      【解决方案7】:

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

      【讨论】:

        【解决方案8】:

        以下是我对创建 es6 类的看法。使用 Three.js r83。 rayCaster的使用方法来自这里的mrdoob:Three.js Projector and Ray objects

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

        【讨论】:

          【解决方案9】:

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

          例如,如果您按中心(模型空间中的已知点)和半径绘制球体 - 您需要将半径作为未投影鼠标坐标的增量来获取 - 但您不能!恕我直言@WestLangley 的 targetZ 方法不起作用,它给出了不正确的结果(如果需要,我可以提供 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; 
          }
          

          【讨论】:

            【解决方案10】:

            对于那些使用 @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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2013-02-26
              • 1970-01-01
              • 2022-01-12
              • 2012-01-07
              • 1970-01-01
              • 1970-01-01
              • 2012-10-07
              • 1970-01-01
              相关资源
              最近更新 更多