Three.js:“选择工具”如何检测2D正方形和3D对象的交集

时间:2022-01-05 04:45:52

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.

我有一个带有对象的3D地图,我想选择我的屏幕上的2D框x1,y1到x2,y2中的所有对象。

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:

prevX和prevY是鼠标按下的坐标:

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

3 个解决方案

#1


3  

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.

在屏幕空间中相交一个框相当于交叉三维空间的金字塔(透视图)或立方体(正交视图)。我认为你应该根据你的2D盒子定义一个THREE.Frustum。

For perspective camera:

对于透视相机:

  1. convert the screen-space box corner coordinates to 3D vectors (a vector from the camera position to the given point)
  2. 将屏幕空间框角坐标转换为3D矢量(从摄像机位置到给定点的矢量)

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

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.
  2. 基于这些点构造4个平面(一个盒子侧的一个平面)。飞机的第三个点是摄像机位置。

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

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.
  2. 构建一个基于这些飞机的Frustum,将摄像机附近和远处的平面添加到它们。

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

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

  1. Intersect the frustum with the objects
  2. 将视锥体与物体相交

frustum.intersectsBox(object.geometry.boundingBox)

frustum.intersectsBox(object.geometry.boundingBox)

or

要么

frustum.intersectsSphere(object.geometry.boundingSphere)

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:

使用Frustum对象本身的替代方法:您可以跳过近处和远处的平面,您只需要检查对象是否位于与4个平面接壤的空间中。您可以编写像Frustum.intersectSphere()方法这样的函数,只有4个平面:

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

#2


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.

您可以使用对象的边界框。 THREE.Geometry包含boundingBox属性。默认情况下为null,您应该通过直接调用computeBoundingBox()来计算它。

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

一旦有了边界框,边界框的二维坐标就是(如果'y'是向上轴):

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

请参见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)

(但是,如果您需要具有非矩形形状的网格的精确结果,则需要进一步计算)

#3


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

对于比subfrustum选择更准确的方法,请参阅具有三个js的选框选择。它的工作原理是将边界框的8个角投影到屏幕空间,然后与屏幕矩形相交。另请参见此处,了解第三种更快的方法(用ClojureScript编写),它将3D边界球投影到屏幕空间中的边界圆上,以及前两种方法的实现。另请参阅相关的堆栈溢出问题,尝试使用GPU执行屏幕投影和使用readpixels进行拾取(我没有考虑过这种方法,因为我假设readpixels会使渲染管道停滞太多,但请告诉我,如果我错了)。

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

我在http://jsbin.com/tamoce/3/上做了一个关于subfrustum选择的工作示例,但它太不准确了。重要的是:

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

#1


3  

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.

在屏幕空间中相交一个框相当于交叉三维空间的金字塔(透视图)或立方体(正交视图)。我认为你应该根据你的2D盒子定义一个THREE.Frustum。

For perspective camera:

对于透视相机:

  1. convert the screen-space box corner coordinates to 3D vectors (a vector from the camera position to the given point)
  2. 将屏幕空间框角坐标转换为3D矢量(从摄像机位置到给定点的矢量)

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

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.
  2. 基于这些点构造4个平面(一个盒子侧的一个平面)。飞机的第三个点是摄像机位置。

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

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.
  2. 构建一个基于这些飞机的Frustum,将摄像机附近和远处的平面添加到它们。

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

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

  1. Intersect the frustum with the objects
  2. 将视锥体与物体相交

frustum.intersectsBox(object.geometry.boundingBox)

frustum.intersectsBox(object.geometry.boundingBox)

or

要么

frustum.intersectsSphere(object.geometry.boundingSphere)

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:

使用Frustum对象本身的替代方法:您可以跳过近处和远处的平面,您只需要检查对象是否位于与4个平面接壤的空间中。您可以编写像Frustum.intersectSphere()方法这样的函数,只有4个平面:

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

#2


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.

您可以使用对象的边界框。 THREE.Geometry包含boundingBox属性。默认情况下为null,您应该通过直接调用computeBoundingBox()来计算它。

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

一旦有了边界框,边界框的二维坐标就是(如果'y'是向上轴):

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

请参见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)

(但是,如果您需要具有非矩形形状的网格的精确结果,则需要进一步计算)

#3


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

对于比subfrustum选择更准确的方法,请参阅具有三个js的选框选择。它的工作原理是将边界框的8个角投影到屏幕空间,然后与屏幕矩形相交。另请参见此处,了解第三种更快的方法(用ClojureScript编写),它将3D边界球投影到屏幕空间中的边界圆上,以及前两种方法的实现。另请参阅相关的堆栈溢出问题,尝试使用GPU执行屏幕投影和使用readpixels进行拾取(我没有考虑过这种方法,因为我假设readpixels会使渲染管道停滞太多,但请告诉我,如果我错了)。

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

我在http://jsbin.com/tamoce/3/上做了一个关于subfrustum选择的工作示例,但它太不准确了。重要的是:

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