Local Info

Topics

j3d.org

Basic Picking Example

Picking is the process of seeing whether two objects intersect and reporting the results. Along with blitting triangles to screen, it is one of the most fundamental 3D graphics concepts. Picking is what allows you to select an object in the virtual world, perform terrain following, or collide with a tree.

If you would like to see the complete version of this code, it can be found in the examples/picking directory and is named BasicPickDemo.java.

 

Setting up the application

In order to illustrate picking, we need to show the effects of the picker both finding an object and not finding it. So for this example, we're going to start with familiar basic application framework and then add some animation so that you can see the result of picking. What we're going to do is have a piece of geometry that will be picked against that will move about, and the picking object remaining stationary. To indicate when a pick intersection has been found, we'll change the colour of the box.

After setting up the basic scene graph structure as in previous examples, we need to create the pieces of geometry needed - a box with a transform over it (so we can animate it), and a piece of geometry that represents the location of the picker. By now, this should be fairly straight-forward, so here's the code:

    BoxGenerator generator = new BoxGenerator(0.15f, 0.15f, 0.15f);
    generator.generate(data);

    TriangleArray geom = new TriangleArray();
    geom.setVertices(TriangleArray.COORDINATE_3,
                     data.coordinates,
                     data.vertexCount);
    geom.setNormals(data.normals);

    Material material = new Material();
    material.setEmissiveColor(new float[] { 0, 0, 1 });

    Appearance app = new Appearance();
    app.setMaterial(material);

    Shape3D shape = new Shape3D();
    shape.setGeometry(geom);
    shape.setAppearance(app);

    trans.set(0.5f, 0, 0);
    Matrix4f mat2 = new Matrix4f();
    mat2.setIdentity();
    mat2.setTranslation(trans);

    TransformGroup shape_transform = new TransformGroup();
    shape_transform.addChild(shape);
    shape_transform.setTransform(mat2);

    scene_root.addChild(shape_transform);

    // Place a point object where the picker is
    float[] coords = new float[3];
    PointArray point_geom = new PointArray();
    point_geom.setVertices(PointArray.COORDINATE_3,
                           coords,
                           1);

    material = new Material();
    material.setEmissiveColor(new float[] { 1, 0, 0 });

    PointAttributes p_attr = new PointAttributes();
    p_attr.setPointSize(4);

    app = new Appearance();
    app.setMaterial(material);
    app.setPointAttributes(p_attr);

    shape = new Shape3D();
    shape.setGeometry(point_geom);
    shape.setAppearance(app);

    scene_root.addChild(shape);

In addition, we're going to need a class that will handling the animation and interaction with the picking code. That will be the job of the class named BasicPickHandler. The outline code is fairly simple, as you would imaging - just something that ticks every frame and moves the box back and forth, like so:

public class BasicPickHandler
    implements ApplicationUpdateObserver, NodeUpdateListener {

    private Vector3f translation;
    private Matrix4f matrix;
    private TransformGroup transform;
    private float angle;
    private Group pickRoot;

    public BasicPickHandler(Group root, TransformGroup tx, Material mat) {
        pickRoot = root;
        material = mat;
        translation = new Vector3f();
        matrix = new Matrix4f();
        matrix.setIdentity();
        transform = tx;
    }

    public void updateSceneGraph() {
        transform.boundsChanged(this);
    }

    public void updateNodeBoundsChanges(Object src) {
        angle += Math.PI / 100;
        float x = 0.4f * (float)Math.sin(angle);
        translation.x = x;
        matrix.setTranslation(translation);
        transform.setTransform(matrix);
    }

    public void updateNodeDataChanges(Object src) {
    }
}

The arguments to the constructor are the transform for our box, the Material node used on the box (we want to change it's colour, so we need the material), and a group node that represents the root of the place that we're going to pick from. More on this shortly...

Picking and the Scene Graph

Picking in Aviatrix3D is provided through a combination of a class and an interface. The interface, named PickableObject marks a scene graph node as being capable of handling picking. Several of the most common nodes in the scene graph implement this interface - Group, Shape and VertexGeometry, as well as other possible custom node implementations. Any of the scene graph nodes that implement this interface are marking themselves as being capable of processing picking requests. Picking requests can be made at any point in the scene graph - there is no requirement to only pick from the root node.

The other half of the pair is the class PickRequest, which encapsulates the user's request to the picking sub system as well as the returned results. Requests are formed by setting a number of the variables to indicate the object type to do the picking (eg line, point, sphere, box etc), the way the return geometry is to be provided (only find the first, find all, sort or not sorted) and, of course, the geometric information needed to define the picked shape.

Picking within Aviatrix3D is very flexible and can be customised to your application's needs. One particular feature that we include is the ability to label pieces of your scene graph as containing a certain feature and then let the picking subsystem intersect with only those features. This comes in the form of a bitmask that can be set using the setPickMask() method of PickableObject. This bit mask allows you to flag that the node and it's children represent any given feature you need to isolate as a separate pickable entity. For example, you want to have water and standard terrain. Boats should float on the top of the water, vehicles follow the terrain (tank on the bottom of the lake). By using the appropriate mask set (it's customisable, you can use whatever values for masks you want) for the grouping nodes and the picking object, you can trivially filter the results from the pick to be exactly as you need them without needing to write lots of code.

If you know you need to perform a number of different pick requests, there is the option of putting them all into a single set of requests to be evaluated as a single pass through the scene graph. For situations where you have a lot of different objects needing to pick (eg a set of lines or points for some form of modelling) this can be far more efficient than make multiple sets of single calls as the scene graph only needs to be traversed once, saving a lot of calculations like matrix transformations etc.

Note: There are several other picking interfaces derived from the interface PickTarget. These are internal marker interfaces that the picking subsystem uses to do the rendering of the scene graph rather than use the real scene graph structure. Unless you are implementing your own nodes, you should not call the methods from those interfaces. In a future revision of Aviatrix3D, they are likely to be hidden within the node's implementation.

Creating the Picking Object

Setting up for picking requires the creation of an instance of PickRequest. This is the class that we'll be using to set up the point location for picking and then passing to the scene graph to get the results. For this demo, the picking is going to be a simple point in space, and a to make life even simpler, it will be at the local origin. The code starts like this:

    pointPick1 = new PickRequest();
    pointPick1.pickGeometryType = PickRequest.PICK_POINT;

pickGeometryType is where you indicate the sort of geometry thatyou want to use for testing the intersection. Normally you would also provide the location of the geometry in one or both of the variables origin, destination and additionalData. The details and use of each of these variables is explained in the class documentation for each of the geometry types, so we won't go over it here as the point is located at the origin, so no need to set anything.

Next we need to tell the picking system about what sort of return results are expected. For example, if you want to know if you've hit anything at all, but don't care about what was actually hit, you can tell it to terminate further searching after it has found the first intersection. On the other end of the scale, you may want to have the system find every single intersection and have them sort the results so that the closest intersection is first and so on. The sorting is defined by the SORT_* constants, and are set using the pickSortType variable. Whether to find only a single object, or multiples is defined by the FIND_* constants and set using pickType. For this demo, we only have a single piece of geometry that we want to pick, so let's be lazy and just tell it to find everything, unsorted:

    pointPick1.pickSortType = PickRequest.SORT_ALL;
    pointPick1.pickType = PickRequest.FIND_ALL;

That's the minimum that needs to be set up for a pick. There is, however, one more variable that can be set: generateVWorldMatrix. This flag controls whether the picking should also go to the trouble of calculating the matrix that indicates where the object is in the virtual world space (relative to the root object that you picked from). If all you need to know is the object that was intersected, and not where it was located in space, this can save quite a lot of internal computation. Turn it off if you don't need it.

One last thing to point out is the placement of this creation in the code. Every frame we will be picking using exactly the same details, so there's no need to create a new object every frame and populate the setup values. The object can be reused every frame (even if you are changing the setup values every time), so the above code snippets will be placed at the end of the constructor of BasicPickHandler

Testing for Intersections

After creating the picking request, the next step is to make the request. In the constructor, one of the arguments was the root object that we were going to pick against. To make a pick, we're going to call the pickSingle method of this object to see what was found. That is a relatively straightforward task - just call the method and wait for it to return the results. The only part that you need to pay particular attention to is the timing of when you make the calls to the pick method.

Following the rest of the design of Aviatrix3D, there are only certain points in time where you are allowed to make changes to the scene graph. There is also a limited window in which you can make picks of the scene graph. Mostly this is for your own sanity. It would be bad, for example, to make a pick while the internals of the engine were in the middle of the state sorting causing temporary internal variables to have strange values. Picking, therefore, is restricted to being made only during the callback from the ApplicationUpdateObserver interface - the one that you use to clock your scene animation timing with. This allows you to perform picks to check for intersections, head off into application code to decide what needs to be done, then come back and notify AV3D of which objects you want to modify.

Assuming that we've made pickPoint1 from the previous section, a class global variable, the code to request the pick is very simple; just place a single line of code into the updateSceneGraph() callback method that looks like this:

public void updateSceneGraph() {

    ...
    transform.boundsChanged(this);
    pickRoot.pickSingle(pointPick1);
}

where the transform line is the one used to animate the box back and forth.

Visualising the Output

For most applications, the output of the picking won't lead to a visually identifiable change on screen. It may be something subtle, such as not letting the arms of two avatars pass through each other. For this example, we'll make if quite obvious when a pick has returned a positive result. As that box is moving back and forth past the picking point, it will change colour when an intersection is detected (pickCount is non-zero), as the following series of 3 screen captures show:

To do this, we need to process the results of the pick request to see how many objects were found. If none were found, set the box to blue, otherwise, set it to green. A naive implementation would look something like this:

public void updateSceneGraph() {
    transform.boundsChanged(this);
    pickRoot.pickSingle(pointPick1);

    material.dataChanged(this);
}

public void updateNodeDataChanges(Object src) {
    if(pointPick1.pickCount != 0)
        material.setEmissiveColor(GREEN);
    else
        material.setEmissiveColor(BLUE);
}

However, as you can see, this isn't exactly the most efficient code in the world - every frame it is changing the object colour, even when it doesn't need to. Optimising the above code requires us to look at the number of items picked this frame, compare it to the number picked last frame and only change the colour if they're different. That, is just a matter of being a little more careful about our calls when doing the picking - just save the old count and compare to the new one. No need to change the updateNodeDataChanges method

public void updateSceneGraph() {
    int old_count = pointPick1.pickCount;

    transform.boundsChanged(this);
    pickRoot.pickSingle(pointPick1);

    if(old_count != pointPick1.pickCount)
        material.dataChanged(this);
}

That is the complete code for this example. Basic picking is not all that difficult to implement. Careful selection of the code structures can result in a good high-performance application. To see more of what can be done with the AV3D picking APIs, see the next tutorial.