8.8. Picking

SoRayPickAction( C++ | Java | .NET ) finds objects along a ray from the camera through a point on the near plane of the view volume. This ray is typically specified by giving the coordinates of a window-space pixel through which it passes. SoRayPickAction( C++ | Java | .NET ) traverses the scene graph you apply the action to and then returns the paths to all shapes along the picking ray, sorted from nearest to farthest. The picking action is primarily interested in geometry, transformation, and shape nodes.

[Tip]

Tip: The SoSelection( C++ | Java | .NET ) node picks objects automatically. You don't need to explicitly use the pick action to select objects. The SoHandleEvent action also performs picking automatically. In addition, the SoEventCallback( C++ | Java | .NET ) node allows you to register a callback function that is invoked whenever a certain event (such as a mouse press) occurs over a specified object. See Chapter 10, Handling Events and Selection for more information on SoSelection( C++ | Java | .NET ), SoHandleEvent, and SoEventCallback( C++ | Java | .NET ).

By default, all objects in the scene graph are pickable (even invisible and transparent objects). To make an object or group of objects invisible to the pick action, insert an SoPickStyle( C++ | Java | .NET ) node in the scene graph and set its style field to UNPICKABLE. Anything that follows in the scene graph cannot be picked until the SoPickStyle( C++ | Java | .NET ) node is reset to SHAPE (to pick points on the shape objects in the scene) or BOUNDING_BOX (to pick points on the bounding boxes for the objects in the scene). BOUNDING_BOX pick style is most often used for SoText3( C++ | Java | .NET ) nodes. The pick style, like all other properties, is saved and restored by SoSeparator( C++ | Java | .NET ) groups.

The constructor for SoRayPickAction( C++ | Java | .NET ) has one parameter, the viewport region (a required parameter).

An example of creating an instance of SoRayPickAction( C++ | Java | .NET ) is


C++
SbViewportRegion myViewport;
SoRayPickAction myPickAction(myViewport);
  

.NET
SbViewportRegion myViewport = new SbViewportRegion();
SoRayPickAction myPickAction = new SoRayPickAction(myViewport);
    

Java
SbViewportRegion myViewport = new SbViewportRegion();
SoRayPickAction myPickAction = new SoRayPickAction(myViewport);
    

The viewport region is used to compute the bounding boxes for screen-aligned objects such as SoText2( C++ | Java | .NET ).

Before you apply the picking action, you can set the following parameters:

The picking ray can be specified in one of two ways: either specify a window point and a radius, or specify a point and a direction in world space. The first method is the more typical for interactive programs, since you are generally most interested in the area underneath the cursor.

The picking action can be applied to either a node, a path, or a path list. To apply the picking action to the root node of a scene graph:


C++
pickAction->apply(rootNode);
                                  

.NET
pickAction.Apply(rootnode);
                                

Java
pickAction.apply(rootnode);
                                

The results of the pick are stored in an SoPickedPoint( C++ | Java | .NET ) (for the first hit) or an SoPickedPointList( C++ | .NET ) (for information on all hit objects). Use the methods on SoPickedPoint( C++ | Java | .NET ) to obtain this information.

An SoPickedPoint( C++ | Java | .NET ) represents a point on the surface of an object that was picked. The picked point contains the point of intersection, the surface normal and texture coordinates at that point, the index into the current set of materials, and the path to the object that was intersected. Use the following methods on SoPickedPoint( C++ | Java | .NET ) to obtain this information:

For example:


C++
SoPath *pathToPickedObject;

const SoPickedPoint *myPickedPoint = myPickAction.getPickedPoint();
if (myPickedPoint != NULL)
  pathToPickedObject = myPickedPoint->getPath();
        

.NET
SoPath pathToPickedObject;

SoPickedPoint myPickedPoint = myPickAction.GetPickedPoint();
if (myPickedPoint != null)
  pathToPickedObject = myPickedPoint.GetPath();
        

Java
SoPath pathToPickedObject;

SoPickedPoint myPickedPoint = myPickAction.getPickedPoint();
if (myPickedPoint != null)
  pathToPickedObject = myPickedPoint.getPath();
        

Figure 8.5, “ Path to Picked Point and Detail List shows the path returned by an SoRayPickAction( C++ | Java | .NET ) (which can be obtained with the getPath() method on SoPickedPoint( C++ | Java | .NET )). This path contains a pointer to each node in the path to the picked object. Use the following methods on SoPickedPoint( C++ | Java | .NET ) to obtain information about the pick in the object space of a particular node in the path chain. You pass in a pointer to the node you are interested in, or use the default (NULL) to obtain information about the tail of the path:

Each node in the picked path may have an associated SoDetail( C++ | Java | .NET ) in which it can store additional information about the pick. For some classes, this associated SoDetail( C++ | Java | .NET ) is NULL. Table 8.2, “Classes That Store an SoDetail” shows the classes that store information in a subclass of SoDetail( C++ | Java | .NET ).

Figure 8.6, “ Detail Classes ” shows the class tree for SoDetail( C++ | Java | .NET ).


Table 8.2. Classes That Store an SoDetail

Class Name

Type of Detail Added

Information Provided

SoCone

SoConeDetail

Contains information about which part of the cone was hit

SoCube

SoCubeDetail

Contains information about which face (part) of the cube was hit

SoCylinder

SoCylinderDetail

Contains information about which part of the cylinder was hit

SoText2, SoText3

SoTextDetail

Specifies the index of the string that was hit; the index of the character within the string that was hit; which part of the text was hit; the object-space bounding box of the character that was intersected

SoFaceSet; all vertex-based shapes except lines, points, and NURBS

SoFaceDetail

Specifies which face in the shape was hit

SoLineSet, SoIndexedLineSet

SoLineDetail

Specifies which line in the line set was hit

SoPointSet

SoPointDetail

Specifies which point in the point set was hit

Use the getDetail() method onSoPickedPoint( C++ | Java | .NET ) to return the detail for a given node in the picked path. This method takes a pointer to a node in the picked path. It returns information for the tail of the path if NULL or no node is specified. For example, to determine whether a cylinder was hit and, if so, whether it was the top part of the cylinder, the code would be as follows:


C++
const SoDetail *pickDetail = myPickedPoint->getDetail();
if (pickDetail != NULL && pickDetail->getTypeId() ==
                        SoCylinderDetail::getClassTypeId())
{
   // Picked object is a cylinder
   SoCylinderDetail *cylDetail = (SoCylinderDetail *) pickDetail;

   // See if top of the cylinder was hit
   if (cylDetail->getPart() == SoCylinder::TOP)
   {
      printf("Top of cylinder was hit\n");
   }
}
                                   

.NET
SoDetail pickDetail = myPickedPoint.GetDetail();
          
if (pickDetail != null && pickDetail is SoCylinderDetail)
{
   // Picked object is a cylinder
   SoCylinderDetail cylDetail = (SoCylinderDetail) pickDetail;

   // See if top of the cylinder was hit
   if (cylDetail.GetPart() == (int)SoCylinder.PartType.TOP)
   {
      Console.WriteLine("Top of cylinder was hit");
   }
}
                                   

Java
SoDetail pickDetail = myPickedPoint.getDetail();

if (pickDetail != null && pickDetail instanceof SoCylinderDetail)
{
   // Picked object is a cylinder
   SoCylinderDetail cylDetail = (SoCylinderDetail) pickDetail;

   // See if top of the cylinder was hit
   if (cylDetail.getPart() == SoCylinder.PartType.TOP.getValue())
   {
      System.out.println("Top of cylinder was hit");
   }
}
                                   

The following fragment shows how you could find the closest vertex to the hit point of a face-based shape using an SoFaceDetail( C++ | Java | .NET ). An SoFaceDetail( C++ | Java | .NET ) contains an array of SoPointDetails. You can examine these details to find the coordinates of the point closest to the hit point by using the getCoordinateIndex() method on SoPointDetail( C++ | Java | .NET ). Finding the node that contains the coordinates is left to the application. (You can create a search action, apply it to the picked path, and ask for the last SoCoordinate3( C++ | Java | .NET ) node in the path. But you also need to know something about the structure of your graph—for example, whether it contains Override flags or Ignore flags that may affect the search.)


C++
//   This function finds the closest vertex to an intersection
//   point on a shape made of faces, passed in the
//   "pickedPoint" argument. It returns the SoCoordinate3 node
//   containing the vertex's coordinates in the "coordNode"
//   argument and the index of the vertex in that node in the
//   "closestIndex" argument. If the shape is not made of faces
//   or there were any other problems, this returns FALSE.

static SbBool
findClosestVertex(const SoPickedPoint *pickedPoint,
            SoCoordinate3 *&coordNode, int &closestIndex)
{
   const SoDetail *pickDetail = pickedPoint->getDetail();

   if (pickDetail != NULL && pickDetail->getTypeId() ==
                             SoFaceDetail::getClassTypeId()) {
      // Picked object is made of faces
      SoFaceDetail *faceDetail = (SoFaceDetail *) pickDetail;

      // Find the coordinate node that is used for the faces.
      // Assume that it's the last SoCoordinate3 node traversed
      // before the picked shape.
      SoSearchAction  mySearchAction;
      mySearchAction.setType(SoCoordinate3::getClassTypeId());
      mySearchAction.setInterest(SoSearchAction::LAST);
      mySearchAction.apply(pickedPoint->getPath());

      if (mySearchAction.getPath() != NULL) {  // We found one
         coordNode = (SoCoordinate3 *)
                     mySearchAction.getPath()->getTail();

         // Get the intersection point in the object space
         // of the picked shape
         SbVec3f objIntersect = pickedPoint->getObjectPoint();

         // See which of the points of the face is the closest
         // to the intersection point
         float minDistance = 1e12;
         closestIndex = -1;
         for (int i = 0; i < faceDetail->getNumPoints(); i++) {
            int pointIndex =
                  faceDetail->getPoint(i)->getCoordinateIndex();
            float curDistance = (coordNode->point[pointIndex] -
                  objIntersect).length();
            if (curDistance < minDistance) {
               closestIndex = pointIndex;
               minDistance = curDistance;
            }
         }

         if (closestIndex >= 0)
            return TRUE;
      }
   }

   return FALSE;
}
                                   

.NET
static bool
findClosestVertex(SoPickedPoint pickedPoint,
            SoCoordinate3 coordNode, int closestIndex)
{
   SoDetail pickDetail = pickedPoint.GetDetail();

   if (pickDetail != null && pickDetail.GetType() == typeof(SoFaceDetail))
   {
      // Picked object is made of faces
      SoFaceDetail faceDetail = (SoFaceDetail) pickDetail;

      // Find the coordinate node that is used for the faces.
      // Assume that it's the last SoCoordinate3 node traversed
      // before the picked shape.
      SoSearchAction  mySearchAction = new SoSearchAction();
      mySearchAction.SetType(typeof(SoCoordinate3));
      mySearchAction.SetInterest(SoSearchAction.Interests.LAST);
      mySearchAction.Apply(pickedPoint.GetPath());

      if (mySearchAction.GetPath() != null)  // We found one
      { 
         coordNode = (SoCoordinate3)
                     mySearchAction.GetPath().GetTail();

         // Get the intersection point in the object space
         // of the picked shape
         SbVec3f objIntersect = pickedPoint.GetObjectPoint();

         // See which of the points of the face is the closest
         // to the intersection point
         float minDistance = 1e12f;
         closestIndex = -1;
         for (int i = 0; i < faceDetail.GetNumPoints(); i++)
         {
            int pointIndex =
                  faceDetail.GetPoint(i).GetCoordinateIndex();
            float curDistance = (coordNode.point[pointIndex] -
                  objIntersect).Length();
            if (curDistance < minDistance)
            {
               closestIndex = pointIndex;
               minDistance = curDistance;
            }
         }

         if (closestIndex >= 0)
            return true;
      }
   }

   return false;
}
                                 

Java
static boolean findClosestVertex(SoPickedPoint pickedPoint,
	             SoCoordinate3 coordNode, int closestIndex)
{
   SoDetail pickDetail = pickedPoint.getDetail();

   if (pickDetail != null && pickDetail instanceof SoFaceDetail)
   {
      // Picked object is made of faces
      SoFaceDetail faceDetail = (SoFaceDetail) pickDetail;

      // Find the coordinate node that is used for the faces.
      // Assume that it's the last SoCoordinate3 node traversed
      // before the picked shape.
      SoSearchAction  mySearchAction = new SoSearchAction();
      mySearchAction.setNodeClass(SoCoordinate3.class);
      mySearchAction.setInterest(SoSearchAction.Interests.LAST);
      mySearchAction.apply(pickedPoint.getPath());

      if (mySearchAction.getPath() != null)  // We found one
      { 
         coordNode = (SoCoordinate3)
                     mySearchAction.getPath().regular.getTail();

         // Get the intersection point in the object space
         // of the picked shape
         SbVec3f objIntersect = pickedPoint.getObjectPoint();

         // See which of the points of the face is the closest
         // to the intersection point
         float minDistance = 1e12f;
         closestIndex = -1;
         for (int i = 0; i < faceDetail.getNumPoints(); i++)
         {
            int pointIndex =
                  faceDetail.getPoint(i).getCoordinateIndex();
            float curDistance = (coordNode.point.getValueAt(pointIndex).minus(
                objIntersect)).length();
            if (curDistance < minDistance)
            {
               closestIndex = pointIndex;
               minDistance = curDistance;
            }
         }

         if (closestIndex >= 0)
            return true;
      }
   }
   return false;
}

Example 8.2, “ Writing the Path to the Picked Object shows setting up the pick action and writing the path to the picked object to stdout.

Example 8.2.  Writing the Path to the Picked Object


C++
SbBool
writePickedPath (SoNode *root, 
   const SbViewportRegion &viewport, 
   const SbVec2s &cursorPosition)
{
   SoRayPickAction myPickAction(viewport);

   // Set an 8-pixel wide region around the pixel
   myPickAction.setPoint(cursorPosition);
   myPickAction.setRadius(8.0);
   
   // Start a pick traversal
   myPickAction.apply(root);
   const SoPickedPoint *myPickedPoint = 
            myPickAction.getPickedPoint();
   if (myPickedPoint == NULL)
      return FALSE;         // no object was picked

   // Write out the path to the picked object
   SoWriteAction myWriteAction;
   myWriteAction.apply(myPickedPoint->getPath());

   return TRUE;
}
        

.NET
bool writePickedPath(SoNode root,
                     SbViewportRegion viewport,
                     SbVec2s cursorPosition)
{
  SoRayPickAction myPickAction = new SoRayPickAction(viewport);

  // Set an 8-pixel wide region around the pixel
  myPickAction.SetPoint(cursorPosition);
  myPickAction.SetRadius(8.0f);

  // Start a pick traversal
  myPickAction.Apply(root);
  SoPickedPoint myPickedPoint = myPickAction.GetPickedPoint();
  if (myPickedPoint == null) return false;

  // Write out the path to the picked object
  SoWriteAction myWriteAction = new SoWriteAction();

  myWriteAction.Apply(myPickedPoint.GetPath());

  return true;
}
      

Java
boolean writePickedPath(SoNode root,
                        SbViewportRegion viewport,
                        SbVec2s cursorPosition)
{
  SoRayPickAction myPickAction = new SoRayPickAction(viewport);
  
  // Set an 8-pixel wide region around the pixel
  myPickAction.setPoint(cursorPosition);
  myPickAction.setRadius(8.0f);
  
  // Start a pick traversal
  myPickAction.apply(root);
  SoPickedPoint myPickedPoint = myPickAction.getPickedPoint();
  if (myPickedPoint == null) return false;
  
  // Write out the path to the picked object
  SoWriteAction myWriteAction = new SoWriteAction();
  
  myWriteAction.apply(myPickedPoint.getPath());
  
  return true;
}