89

I've searched around for an example that matches my use case but cannot find one. I'm trying to convert screen mouse co-ordinates into 3D world co-ordinates taking into account the camera.

Solutions I've found all do ray intersection to achieve object picking.

What I am trying to do is position the center of a Three.js object at the co-ordinates that the mouse is currently "over".

My camera is at x:0, y:0, z:500 (although it will move during the simulation) and all my objects are at z = 0 with varying x and y values so I need to know the world X, Y based on assuming a z = 0 for the object that will follow the mouse position.

This question looks like a similar issue but doesn't have a solution: Getting coordinates of the mouse in relation to 3D space in THREE.js

Given the mouse position on screen with a range of "top-left = 0, 0 | bottom-right = window.innerWidth, window.innerHeight", can anyone provide a solution to move a Three.js object to the mouse co-ordinates along z = 0?

2
  • 1
    Hey Rob fancy running into you here :)
    – acheo
    Commented Oct 6, 2013 at 23:52
  • 1
    Hi could you post a little jsfiddle for this case?
    – utdev
    Commented Jun 6, 2017 at 14:24

11 Answers 11

164

You do not need to have any objects in your scene to do this.

You already know the camera position.

Using vector.unproject( camera ) you can get a ray pointing in the direction you want.

You just need to extend that ray, from the camera position, until the z-coordinate of the tip of the ray is zero.

You can do that like so:

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

The variable pos is the position of the point in 3D space, "under the mouse", and in the plane z=0.


EDIT: If you need the point "under the mouse" and in the plane z = targetZ, replace the distance computation with:

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

three.js r.98

28
  • 4
    Perfect answer to the same question I had. Could mention that projector can just be instantiated and doesnt need to be set up in anyway - projector = new THREE.Projector(); Commented Nov 9, 2012 at 21:11
  • 9
    I think I figured it out. If you replace var distance = -camera.position.z / dir.z; with var distance = (targetZ - camera.position.z) / dir.z;, you can specify the z value (as targetZ). Commented Jun 3, 2015 at 1:14
  • 2
    @WestLangley - Thanks for the great answer. I have only one doubt, could you please explain why is the z coordinate of vector set to 0.5 in line 6? Would that line also be different when we want to specify a value of z different from 0 (as in SCCOTTT's case)?
    – Matteo
    Commented Aug 23, 2016 at 0:02
  • 2
    @Matteo The code is "unprotecting" a point from Normalized Device Coordinate (NDC) space to world space. The 0.5 value is arbitrary. Google NDC space if you do not understand the concept. Commented Aug 23, 2016 at 3:13
  • 3
    @WestLangley - Thanks for elaborating, I want to share this link that also helped me figure things out. The idea is that everything in the threejs space can be described with coordinates xyz between -1 and 1 (NDC). For the x and y that is done by renormalizing the event.client variables, for z it is done by selecting an arbitrary value between -1 and 1 (i.e. 0.5). The value chosen has no effect on the rest of the code since we are using it to define a direction along a ray.
    – Matteo
    Commented Aug 23, 2016 at 15:48
16

This worked for me when using an 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

3
  • Worked for me, for an orthographic camera. Thanks! (This other one here works too, but it's not as simple as your solution: www.greatytc.com/a/17423976/2441655. But that one should work for non-top-down cameras, whereas this one I'm not sure if it would.)
    – Venryx
    Commented Feb 23, 2018 at 7:02
  • 5
    If using React Three Fiber, you can shorten this even more. The formulas above convert the cursor position to NDC space before unprojecting (threejs.org/docs/#api/en/math/Vector3.unproject), but RTF already calculates this for you in useThree().mouse (i.e. const { mouse } = useThree(); vector.set(mouse.x, mouse.y, 0); vector.unproject(camera);)
    – Brooke
    Commented Oct 10, 2021 at 12:09
  • 1
    I found the calculations to work a little better when I used the canvas' width/height instead of the windows width/height. Commented Sep 8, 2022 at 22:56
9

In r.58 this code works for me:

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
  • 6
    Why 0.5? Looks as though the 0.5 can be anything because it is in the direction of the normal. I've tried it with other numbers and it doesn't seem to make any difference.
    – resigned
    Commented Feb 20, 2014 at 20:37
  • To me, this solution is the cleanest. @ChrisSeddon: The z-coordinate is immediately overwritten in the pickingRay method. Commented May 17, 2014 at 17:38
  • pickingRay has been removed so this doesn't work with the most recent version (as of 29/10/2014)
    – Pete Kozak
    Commented Oct 29, 2014 at 15:25
  • 1
    It says replaced with raycaster.setFromCamera but that's not from a projector use new THREE.Raycaster(); Commented Apr 18, 2015 at 10:00
  • This works, but I found an even simpler solution here (might only work for top-down camera, though): www.greatytc.com/a/48068550/2441655
    – Venryx
    Commented Feb 23, 2018 at 7:04
6

Below is an ES6 class I wrote based on WestLangley's reply, which works perfectly for me in THREE.js r77.

Note that it assumes your render viewport takes up your entire browser viewport.

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

You can use the class like this:

// 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 now contains the projected mouse position on the z=0 plane.

4

I had a canvas that was smaller than my full window, and needed to determine the world coordinates of a click:

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

Here's an example. Click the same region of the donut before and after sliding and you'll find the coords remain constant (check the browser console):

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

2
  • 1
    For me, this snippet displays a black screen that doesn't change. Commented Feb 14, 2022 at 22:37
  • Probably because the three.js api has changed. The code above would only work with three.js version 97
    – duhaime
    Commented Feb 15, 2022 at 15:34
3

to get the mouse coordinates of a 3d object use 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;

to get the three.js 3D coordinates that relate to specific mouse coordinates, use the opposite, 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 );

There is a great example here. However, to use project vector, there must be an object where the user clicked. intersects will be an array of all objects at the location of the mouse, regardless of their depth.

7
  • Cool, so then I assign the object's position to x: vector.x, y: vector.y, z:0?
    – Rob Evans
    Commented Oct 24, 2012 at 18:29
  • not sure I understand, are you trying to move the object to a mouse position, or find the mouse position of an object? Are you going from mouse coords to three.js coords, or the other way around?
    – BishopZ
    Commented Oct 24, 2012 at 18:31
  • Actually, that doesn't look right... where is object.matrixWorld.getPosition().clone() coming from? There is no object to start with, I want to create a new one and position it where the mouse event occurred.
    – Rob Evans
    Commented Oct 24, 2012 at 18:32
  • Just saw your last message, yes move an object to the mouse position :)
    – Rob Evans
    Commented Oct 24, 2012 at 18:33
  • Thanks for that. It's almost there but I already found posts to find the intersection of existing objects. What I need is if the world is empty apart from the camera, how would I create a new object where the mouse was clicked, and then continue to move that object to the mouse position as it is moved.
    – Rob Evans
    Commented Oct 24, 2012 at 18:38
3

Here's a current answer (THREE.REVISION==157) that works with rotated cameras and smaller-than-window renderers, and finds a point on the ground plane. (Substitute any other plane you like.)

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)
  }
})
2

ThreeJS is slowly mowing away from Projector.(Un)ProjectVector and the solution with projector.pickingRay() doesn't work anymore, just finished updating my own code.. so the most recent working version should be as follow:

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

For those using @react-three/fiber (aka r3f and react-three-fiber), I found this discussion and it's associated code samples by Matt Rossman helpful. In particular, many examples using the methods above are for simple orthographic views, not for when OrbitControls are in play.

Discussion: https://github.com/pmndrs/react-three-fiber/discussions/857

Simple example using Matt's technique: https://codesandbox.io/s/r3f-mouse-to-world-elh73?file=/src/index.js

More generalizable example: https://codesandbox.io/s/react-three-draggable-cxu37?file=/src/App.js

2
  • How could I slow the following a bit down? so that the object is not immediately on the same position as the mouse, rather than following the mouse?
    – Suisse
    Commented Jan 13, 2023 at 18:48
  • btw. this should be the correct answer, since it is almost a one liner.
    – Suisse
    Commented Jan 13, 2023 at 18:49
0

Here is my take at creating an es6 class out of it. Working with Three.js r83. The method of using rayCaster comes from mrdoob here: 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)
      }
    }

You would use it like this if you wanted to check against all your objects in the scene for hits. I made the recursive flag false above because for my uses I did not need it to be.

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

Although the provided answers can be useful in some scenarios, I hardly can imagine those scenarios (maybe games or animations) because they are not precise at all (guessing around target's NDC z?). You can't use those methods to unproject screen coordinates to the world ones if you know target z-plane. But for the most scenarios, you should know this plane.

For example, if you draw sphere by center (known point in model space) and radius - you need to get radius as delta of unprojected mouse coordinates - but you can't! With all due respect @WestLangley's method with targetZ doesn't work, it gives incorrect results (I can provide jsfiddle if needed). Another example - you need to set orbit controls target by mouse double click, but without "real" raycasting with scene objects (when you have nothing to pick).

The solution for me is to just create the virtual plane in target point along z-axis and use raycasting with this plane afterward. Target point can be current orbit controls target or vertex of object you need to draw step by step in existing model space etc. This works perfectly and it is simple (example in 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; 
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.