5

Basically what i want to create:

I have a 3D map with objects, i want to select all objects that are in the 2D box x1,y1 to x2,y2 on my screen.

Any ideas how this has to be done, because i'm clueless on how to start.

Thanks in advance!

prevX and prevY is coordinate of mouse down:

function onDocumentMouseUp(event) {
  event.preventDefault();

  var x = (event.clientX / window.innerWidth) * 2 - 1;
  var y = -(event.clientY / window.innerHeight) * 2 + 1;

  var width = (x - prevX); //* window.innerWidth;
  var height = (y - prevY); //* window.innerHeight;
  var dx = prevX; //* window.innerWidth;
  var dy = prevY; //* window.innerHeight;

  console.log(
    dx + ',' + 
    dy + "," + 
    (dx + width) + "," + 
    (dy + height) + 
    ", width=" + width + 
    ", height=" + height
  );
  var topLeftCorner3D = new THREE.Vector3(dx, dy, 1).unproject(
    camera);
  var topRightCorner3D = new THREE.Vector3(dx + width, dy, 1)
    .unproject(camera);
  var bottomLeftCorner3D = new THREE.Vector3(dx, dy + height,
    1).unproject(camera);
  var bottomRightCorner3D = new THREE.Vector3(dx + width, dy +
    height, 1).unproject(camera);

  var topPlane = new THREE.Plane();
  var rightPlane = new THREE.Plane();
  var bottomPlane = new THREE.Plane();
  var leftPlane = new THREE.Plane();

  topPlane.setFromCoplanarPoints(camera.position,
    topLeftCorner3D, topRightCorner3D);
  rightPlane.setFromCoplanarPoints(camera.position,
    topRightCorner3D, bottomRightCorner3D);
  bottomPlane.setFromCoplanarPoints(camera.position,
    bottomRightCorner3D, bottomLeftCorner3D);
  leftPlane.setFromCoplanarPoints(camera.position,
    bottomLeftCorner3D, topLeftCorner3D);

  //var frustum = new THREE.Frustum( topPlane, bottomPlane, leftPlane, rightPlane, nearPlane, farPlane);

  function isObjectInFrustum(object3D) {
    var sphere = object3D.geometry.boundingSphere;
    var center = sphere.center;
    var negRadius = -sphere.radius;

    if (topPlane.distanceToPoint(center) < negRadius) { return false; }
    if (bottomPlane.distanceToPoint(center) < negRadius) { return false; }
    if (rightPlane.distanceToPoint(center) < negRadius) { return false; }
    if (leftPlane.distanceToPoint(center) < negRadius) { return false; }

    return true;
  }
  var matches = [];
  for (var i = 0; i < window.objects.length; i++) {

    if (isObjectInFrustum(window.objects[i])) {
      window.objects[i].material = window.selectedMaterial;
    }
  }
}
a--m
  • 4,716
  • 1
  • 39
  • 59
Captain Obvious
  • 745
  • 3
  • 17
  • 39

3 Answers3

4

Intersecting a box in the screen space is equivalent to intersecting a pyramid (perspective) or a cube (orthogonal view) the 3D space. I think you should define a THREE.Frustum based on your 2D box.

For perspective camera:

  1. convert the screen-space box corner coordinates to 3D vectors (a vector from the camera position to the given point)

var topLeftCorner3D = new THREE.Vector3( topLeftCorner2D.x, topLeftCorner2D.y, 1 ).unproject( camera );

  1. construct 4 planes based on these points (one plane for one box side). The third point of the plane is the camera position.

topPlane.setFromCoplanarPoints (camera.position, topLeftCorner3D , topRightCorner3D ) rightPlane.setFromCoplanarPoints (camera.position, topRightCorner3D , bottomRightCorner3D ) bottomPlane.setFromCoplanarPoints (camera.position,bottomRightCorner3D , bottomLeftCorner3D ) leftPlane.setFromCoplanarPoints (camera.position, bottomLeftCorner3D , topLeftCorner3D )

  1. Construct a Frustum based ot these planes, adding the camera near and far planes to them.

var frustum = new THREE.Frustum( topPlane, bottomPlane, leftPlane, rightPlane, nearPlane, farPlane);

  1. Intersect the frustum with the objects

frustum.intersectsBox(object.geometry.boundingBox)

or

frustum.intersectsSphere(object.geometry.boundingSphere)

An alternative to using the Frustum object itself: you can skip the near and far planes, you only have to check whether the object is in the space bordered with your 4 planes. You can write a function like Frustum.intersectSphere() method with only 4 planes:

function isObjectInFrustum(object3D) {
    var sphere = object3D.geometry.boundingSphere;
    var center = sphere.center;
    var negRadius = - sphere.radius;

    if ( topPlane.distanceToPoint( center )< negRadius ) return false;
    if ( bottomPlane.distanceToPoint( center )< negRadius ) return false;
    if ( rightPlane.distanceToPoint( center )< negRadius ) return false;
    if ( leftPlane.distanceToPoint( center )< negRadius ) return false;

    return true;  
}
elcsiga
  • 170
  • 1
  • 8
1

You can use the bounding boxes of the objects. THREE.Geometry contains a boundingBox property. This is null by default, you should compute it by calling computeBoundingBox() directly.

scene.traverse( function ( node ) {
    if ( node.geometry )
         node.geometry.computeBoundingBox();
} );

Once you have the bounding boxes, the bounding box 2D coordinates are (if 'y' is the UP axis):

mesh.geometry.boundingBox.min.x
mesh.geometry.boundingBox.min.z
mesh.geometry.boundingBox.max.x
mesh.geometry.boundingBox.max.z

See http://threejs.org/docs/#Reference/Core/Geometry

(however, if you need the exact result for meshes that have non-rectangular shape, you need further computations)

elcsiga
  • 170
  • 1
  • 8
  • Thanks, i'll check it out tomorrow. However, i'm not sure if this is what i want? Let's say i have a fake rectangle on my window from (100,100) to (200,200), how can i check which 3D objects are visible within that rectangle at my screen? – Captain Obvious Dec 04 '14 at 23:05
  • Sorry, I ignored that you have the 2d box in screen space. In this case I think you should try 'frustum culling'. See: http://stackoverflow.com/questions/16396874/manual-frustrum-culling-with-three-js – elcsiga Dec 04 '14 at 23:17
0

For a more accurate method than subfrustum selection see marquee selection with three js. It works by projecting the 8 corners of bounding boxes onto screen space and then intersecting with screen rectangle. See also here for a 3rd faster method (written in ClojureScript) that projects 3D bounding sphere onto bounding circle in screen space, as well as implementations of the first two methods. See also related stack overflow question that tries to use GPU to perform screen projection and picking with readpixels (I haven't looked into this method since I assume readpixels would stall the rendering pipeline too much, but let me know if I am wrong).

I made a working example of subfrustum selection at http://jsbin.com/tamoce/3/ , but it is too inaccurate. The important part is:

          var rx1 = ( x1 / window.innerWidth ) * 2 - 1;
          var rx2 = ( x2 / window.innerWidth ) * 2 - 1;
          var ry1 = -( y1 / window.innerHeight ) * 2 + 1;
          var ry2 = -( y2 / window.innerHeight ) * 2 + 1;

          var projectionMatrix = new THREE.Matrix4();
          projectionMatrix.makeFrustum( rx1, rx2, ry1, ry2, camera.near, camera.far );

          camera.updateMatrixWorld();
          camera.matrixWorldInverse.getInverse( camera.matrixWorld );

          var viewProjectionMatrix = new THREE.Matrix4();
          viewProjectionMatrix.multiplyMatrices( projectionMatrix, camera.matrixWorldInverse );

          var frustum = new THREE.Frustum();
          frustum.setFromMatrix( viewProjectionMatrix );
Community
  • 1
  • 1
emh
  • 199
  • 10
  • Nice, however when I try the demo it doesn't seem to be very accurate? – Captain Obvious May 10 '16 at 16:22
  • 1
    @CaptainObvious That's true. For an accurate but completely different method that doesn't use frustum see http://blog.tempt3d.com/2013/11/marquee-selection-with-threejs.html . – emh May 11 '16 at 10:18
  • Thanks it looks indeed what I was searching for, I will check it out – Captain Obvious May 11 '16 at 10:23
  • @CaptainObvious By the way, I implemented 3 different methods at https://github.com/emnh/rts/blob/master/src.client/game/client/selection.cljs , but is ClojureScript, not JavaScript. I implemented the subfrustum method, which like you I found was too inaccurate, though fast, and I implemented the second method that I linked in previous comment, which is a bit slow since it projects all 8 corners of bounding box into screen space and then finds bounding rectangle, and a third fast method that projects 3D bounding sphere onto bounding circle in screen space. I chose the third method for my game. – emh May 11 '16 at 10:29
  • @emh I tried the subfrustum solution on an 3D sphere (https://github.com/dataarts/webgl-globe) to visualize earthquakes and its very inaccurate. Is there currently a better solution or is it still recommended to use marquee selection? Thanks for any help. – vicR Dec 29 '18 at 11:30
  • @vicR I don't remember which solution I ended up with, haven't been into this project for a while. Check https://github.com/emnh/rts/blob/master/src.client/game/client/selection.cljs. IIRC I ended up projecting each 3D object bounding sphere on screen circle and then intersecting that with selection rectangle. Also made GPU solution I think in engine2_selection.cljs. Sorry for late reply. – emh Nov 09 '19 at 11:43