Threejs - How to pick all objects in area?

2020-05-24 16:57发布

I'm using Three.js and I wonder how to get all objects in a given area?

For example, get all objects that found in the green-square: Mass object selection

Solution:

        getEntitiesInSelection: function(x, z, width, height, inGroup) {
            var self = this,
                entitiesMap = [],
                color = 0,
                colors = [],
                ids = [],
                pickingGeometry = new THREE.Geometry(),
                pickingMaterial = new THREE.MeshBasicMaterial( { vertexColors: THREE.VertexColors } ),
                pickingScene = new THREE.Scene(),
                pickingTexture = new THREE.WebGLRenderTarget( this._renderer.domElement.width, this._renderer.domElement.height),
                cloneMesh,
                entities = inGroup ?
                    engine.getObjectsByGroup(inGroup) : engine.getRegisteredEntities();

            pickingTexture.generateMipmaps = false;

            //Go over each entity, change its color into its ID
            _.forEach(entities, function(entity) {
                if(undefined == entity.threeRenderable) {
                    return ;
                }

                //Clone entity
                cloneMesh = entity.threeRenderable.mesh().clone();
                cloneMesh.material = entity.threeRenderable.mesh().material.clone();
                cloneMesh.material.map = null;
                cloneMesh.material.vertexColors = THREE.VertexColors;
                cloneMesh.geometry = entity.threeRenderable.mesh().geometry.clone();
                cloneMesh.position.copy( entity.threeRenderable.mesh().position );
                cloneMesh.rotation.copy( entity.threeRenderable.mesh().rotation );
                cloneMesh.scale.copy( entity.threeRenderable.mesh().scale );

                //Cancel shadow
                cloneMesh.castShadow = false;
                cloneMesh.receiveShadow  = false;

                //Set color as entity ID
                entitiesMap[color] = entity.id();
                self._applyVertexColors(cloneMesh.geometry, new THREE.Color( color ) );
                color++;

                THREE.GeometryUtils.merge( pickingGeometry,  cloneMesh);
            });

            pickingScene.add( new THREE.Mesh( pickingGeometry, pickingMaterial ) );

            //render the picking scene off-screen
            this._renderer.render(pickingScene, this._objs[this._mainCamera], pickingTexture );
            var gl = this._renderer.getContext();

            //read the pixel under the mouse from the texture
            var pixelBuffer = new Uint8Array( 4 * width * height );
            gl.readPixels( x, this._renderer.domElement.height - z, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelBuffer );

            //Convert RGB in the selected area back to color
            for(var i=0; i<pixelBuffer.length; i+=4) {
                if( 0 == pixelBuffer[i] && 0 == pixelBuffer[i+1] && 0 == pixelBuffer[i+2] && 0 == pixelBuffer[i+3] ) {
                    continue;
                }

                color = ( pixelBuffer[i] << 16 ) | ( pixelBuffer[i+1] << 8 ) | ( pixelBuffer[i+2] );
                colors.push(color);
            }
            colors = _.unique(colors);

            //Convert colors to ids
            _.forEach(colors, function(color) {
                ids.push(entitiesMap[color]);
            });

            return ids;
        }

The line engine.getObjectsByGroup(inGroup) : engine.getRegisteredEntities(); just return an array of entities, which in turn, I iterate over the entities:

 _.forEach(entities, function(entity) { ...

Only entities that have the 'threeRenderable' property (object) are visible, therefore, I ignore those that doesn't have it:

if(undefined == entity.threeRenderable) {
      return ;
}

then I merge the entity's cloned mesh with with the pickingGeometry:

THREE.GeometryUtils.merge( pickingGeometry, cloneMesh);

eventually, I add the pickingGeometry to the pickingScene:

 pickingScene.add( new THREE.Mesh( pickingGeometry, pickingMaterial ) );

Then I read the colors of the selected area, and return an array of IDs.

You can checkout the Node.js game engine I wrote back then.

标签: three.js
1条回答
【Aperson】
2楼-- · 2020-05-24 17:37

I've wanted to implement something like this and I choose a very different method - maybe much worse, I don't really know - but much easier to do IMO, so I put it here in case someone wants it.

Basically, I used only 2 raycasts to know the first and last points of the selection rectangle, projected on my ground plane, and iterate over my objects to know which ones are in.

Some very basic code:

   function onDocumentMouseDown(event) {
      // usual Raycaster stuff ...

      // get the ground intersection
      var intersects = raycaster.intersectObject(ground);

      GlobalGroundSelection = {
        screen: { x: event.clientX, y: event.clientY },
        ground: intersects[0].point
      };
    }

   function onDocumentMouseUp(event) {
      // ends a ground selection
      if (GlobalGroundSelection) {
        // usual Raycaster stuff ...

        // get the ground intersection
        var intersects = raycaster.intersectObjects(ground);

        var selection = {
          begins: GlobalGroundSelection.ground,
          ends: intersects[0].point
        };

        GlobalGroundSelection = null;
        selectCharactersInZone(selection.begins, selection.ends);
      }
    }

    function onDocumentMouseMove(event) {

      if (GlobalGroundSelection) {
        // in a selection, draw a rectangle
        var p1 = GlobalGroundSelection.screen,
            p2 = { x: event.clientX, y: event.clientY };

        /* with these coordinates
          left: p1.x > p2.x ? p2.x : p1.x,
          top: p1.y > p2.y ? p2.y : p1.y,
          width: Math.abs(p1.x - p2.x),
          height: Math.abs(p1.y - p2.y)
        */
      }
    }

Here is my select function:

function selectCharactersInZone (start, end) {

  var selected = _.filter( SELECTABLE_OBJECTS , function(object) {
    // warning: this ignore the Y elevation value
    var itsin = object.position.x > start.x
            && object.position.z > start.z 
            && object.position.x < end.x
            && object.position.z < end.z;

    return itsin;
  });

  return selected;
}

Some warnings: as far as I know, this technique is only usable when you don't care about Y positions AND your selection is a basic rectangle.

My 2c

查看更多
登录 后发表回答