2

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.

eldad87
  • 195
  • 1
  • 13
  • What kind of selection are you using right now? See the GPU-Picking example. You could render a pass with color IDs and use readPixels to read all the color information in a given region, thus returning all the object color ids in that region. This would get you the information you need. Not sure about other possibilities with the raycaster... btw: looks cool what you are up to ;) – GuyGood Nov 24 '13 at 14:34
  • I'm not sure how GPU-picking will help here. in that example, each cube is rendered with a different color - that way, using readPixel (which return all pixel/colors in the given area) you can 'know' on which cube the user clicked. WHILE I need, is to return a list of objects in a given area. – eldad87 Nov 24 '13 at 22:01
  • Yep, as you said, readPixels returns all pixel/colors in the given area. The given area is your rectangular selection. The colors are your objectIDs. Using the readPixels output, you can evaluate the colors and return the list of objects in this given area, yielding exactly what you want to achieve? You only need to render all your objects IDs into a framebuffer/texture. – GuyGood Nov 24 '13 at 23:20
  • @GuyGood , 1. How can I render all my object into a frameBuffer + Set an their color as ID? 2. I tried to do it, but I got SO-MANY results: `var gl = canvas.getContext("webgl", {preserveDrawingBuffer: true}); var pixelBuffer = new Uint8Array( width * height ); // here 100 gl.readPixels( posX, posY, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelBuffer );` now this returned an array with LOTS of values (as width*height) and not all results were unique. Please advise. – eldad87 Nov 25 '13 at 23:14
  • well, see the gpu example on how to set up colorIDs. Also for better visual idea, take the gpu-example and remove the rendertarget from the first render-call and uncomment some stuff, so you can see how the picture with the colorIDs looks like. if you get an array with width*height back from readPixels, this is exactly what you want. (100 calls? 1 should be enough to get this array) of course not all values are unique. 1 objects in the region with a green colorID will have x pixels in the region. But you only need to care about the first appearance, because then you know the obj is inside. – GuyGood Nov 26 '13 at 08:49
  • @GuyGood, I tried your suggestion. now I wonder how should I implement it with MOVING entities while keeping it low on resources. any suggestions? Thanks! – eldad87 Dec 03 '13 at 22:40
  • Well, does it work? Do you already feel performance problems for one selection? Maybe you could render to the colorID rendertarget only on mousedown or between mousedown and mouseup, so you don't render into the rendertarget if you don't need the info for clicking? And wouldn't moving objects be in there already? How did you implement it at the moment? I also like the answer of vincent, using the raycaster. This could be further optimized by using an octtree or quadtree (depending on needs) if you have more than "a few" objects to select from. – GuyGood Dec 03 '13 at 22:51
  • @GuyGood, I didn't tried it yet on MOVING objects. I wonder if have any suggestions. vincent's suggestion is irrelevant because it's not usefully for isometric view. – eldad87 Dec 07 '13 at 20:54
  • what is your problem now, I don't understand it, sorry.Have you tried asking in the three.js IRC chat room? – GuyGood Dec 08 '13 at 01:32
  • The step where you merge the geometry for example could be the point where you loose transform-information, for example if you orignally had parent/child relations? You should check the mergedGeometry by having it rendered to the screen and check what it looks like. Also, i think if for example all your objects in the sceen use a phong shader, you could copy the shader code and modify it so that you can pass a uniform to switch between std material rendering and flat color shading? This way, you wouldn't have to clone your whole scene? :) Also this is just an idea, check positions first – GuyGood Dec 22 '13 at 11:23
  • I solved it, my issue was with the wrong height of gl.readPixels. see the full solution above. – eldad87 Dec 27 '13 at 21:33

1 Answers1

1

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

vincent
  • 1,525
  • 12
  • 18
  • I wonder about the implementation of `selectCharactersInZone(begins, ends)`; are you iterating on ALL your object, and returning only those that found in the selected area? – eldad87 Nov 30 '13 at 00:09
  • I use this function to find the objects in the selected area so yes, I'm iterating on all **selectectable** objects - in my case a couple of characters - so it's fast enough. In my opinion, it's up to you to iterating over a decent amount of objects, and use a simple enough "find" test. I have edited the answer with my `selectCharactersInZone(begins, ends)` function. – vincent Nov 30 '13 at 15:14