27

I have a THREE.js scene where a lot of elements appear, and I need to detect what object the user is clicking on.

What I have done so far is the following. The camera does not move to much - it only changes the vertical position by a limited amount, always looking towards the same point. My approximate method is the following:

  • I take the coordinates if the click relative to the canvas
  • I translate them into horizontal and vertical coordinates in the webGL scene by means of a simple rescaling, and add a Z coordinate which is sufficiently far away.
  • I take a horizontal ray starting from the point above, constructed by THREE.Ray()
  • I use ray.intersectObjects() to find the first element along the ray.

This method approximately works, but it is sometimes a few pixels away from the actual point.

Is there a more reliable technique to find out the object where a user has clicked?

gman
  • 100,619
  • 31
  • 269
  • 393
Andrea
  • 20,253
  • 23
  • 114
  • 183
  • Margin and Padding may be causing your coordinates to be a bit offset. Did you account for it? – Prusse Oct 31 '11 at 16:30
  • At the moment in the demo there are no margin nor paddings, but the technique I described is not exact anyway. – Andrea Oct 31 '11 at 16:57
  • Thank you, this is exactly what I needed. I did not know about `projector.unprojectVector()`. In a later version I was trying to unproject the vector myself, considering the angle it formed wrt to the camera, which I could find because I know the fov, but it did not work as expected. By the way, is there a reason why you normalize the vector before passing it to the Ray constructor? The direction does not change upon normalizing. – Andrea Nov 02 '11 at 08:48

4 Answers4

11

Depends on what kind of camera are you using.

1) PerspectiveCamera: is ok link that Mr.doob provides.
2) OrthographicCamera: is quite different:

var init = function() {
  camera = new THREE.OrthographicCamera( SCREEN_WIDTH / - 2, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, SCREEN_HEIGHT / - 2, NEAR, FAR);
  document.addEventListener( 'mousedown', onDocumentMouseDown, false );
}

function onDocumentMouseDown( e ) {
  e.preventDefault();
  var mouseVector = new THREE.Vector3();
  mouseVector.x = 2 * (e.clientX / SCREEN_WIDTH) - 1;
  mouseVector.y = 1 - 2 * ( e.clientY / SCREEN_HEIGHT );
  var raycaster = projector.pickingRay( mouseVector.clone(), camera );
  var intersects = raycaster.intersectObject( TARGET );
  for( var i = 0; i < intersects.length; i++ ) {
    var intersection = intersects[ i ],
    obj = intersection.object;
    console.log("Intersected object", obj);
  }
}
Luca Davanzo
  • 21,000
  • 15
  • 120
  • 146
4

Check out this one:

var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 5000);
var object; //your object

document.addEventListener('mousedown', onMouseDown, false);

function onMouseDown(e) {
    var vectorMouse = new THREE.Vector3( //vector from camera to mouse
        -(window.innerWidth/2-e.clientX)*2/window.innerWidth,
        (window.innerHeight/2-e.clientY)*2/window.innerHeight,
        -1/Math.tan(22.5*Math.PI/180)); //22.5 is half of camera frustum angle 45 degree
    vectorMouse.applyQuaternion(camera.quaternion);
    vectorMouse.normalize();        

    var vectorObject = new THREE.Vector3(); //vector from camera to object
    vectorObject.set(object.x - camera.position.x,
                     object.y - camera.position.y,
                     object.z - camera.position.z);
    vectorObject.normalize();
    if (vectorMouse.angleTo(vectorObject)*180/Math.PI < 1) {
        //mouse's position is near object's position

    }
}
Teivaz
  • 5,462
  • 4
  • 37
  • 75
  • this is a very nice solution and prevents a cpu-intensive raycast, but only sees if the mouse clicks near the pivot of the object. Avantage is that an object without mesh aso would work this way. Disadvantage is the circular "hotspot". – Hacky Mar 29 '17 at 16:12
3

Checks for intersection of the mouse and any of the Cubes in 3d space and alters it's color. Maybe this help you.

josedlujan
  • 5,357
  • 2
  • 27
  • 49
1

I ran into problems trying to implement this for a canvas which does not take up the entire width and height of the screen. Here is the solution I found works quite well.

Initialize everything on an existing canvas:

var init = function() {
  var canvas_model = document.getElementById('model')
  var viewSize = 50 // Depending on object size, canvas size etc.
  var camera = new THREE.OrthographicCamera(-canvas_model.clientWidth/viewSize, canvas_model.clientWidth/viewSize, canvas_model.clientHeight/viewSize, -canvas_model.clientHeight/viewSize, 0.01, 2000),
}

Add an event listener to the canvas:

canvas_model.addEventListener('click', function(event){
  var bounds = canvas_model.getBoundingClientRect()
  mouse.x = ( (event.clientX - bounds.left) / canvas_model.clientWidth ) * 2 - 1;
  mouse.y = - ( (event.clientY - bounds.top) / canvas_model.clientHeight ) * 2 + 1;
  raycaster.setFromCamera( mouse, camera );
  var intersects = raycaster.intersectObjects(scene.children, true);
  if (intersects.length > 0) {
     // Do stuff
  }
}, false)

Or for a 'touchstart' event, change the lines calculating the mouse.x and mouse.y into:

mouse.x = ( (event.touches[0].clientX - bounds.left) / canvas_model.clientWidth ) * 2 - 1;
mouse.y = - ( (event.touches[0].clientY - bounds.top) / canvas_model.clientHeight ) * 2 + 1;
Evertvdw
  • 827
  • 10
  • 17