Computer Graphics Workshop '96 Lecture Notes


Today's topics
Interactivity via selection

One very powerful form of interactivity in a 3D program is the ability to select objects on the screen: that is, being able to click on a rendered image of a 3D object and to have the application know that that object has been selected. If you think about it, this is actually a hard problem, or at least one very inconvenient to solve; in order to implement this using the X window system, you would have to trap the mouse button event, find the window coordinates where the click occurred, backproject those coordinates into the 3D scene, find the intersection with the closest object, etc. What seems to be a difficult problem, however, becomes trivial with Open Inventor.

An SoSelection node is a subclass of SoSeparator; that is, it acts as a group node with Separator's additions of the push/pop of the rendering state. However, it implements several very powerful functions, primary that it automatically implements mouse-based selection of objects below it in the scene graph. Several callbacks can be set up along the selection process to give the user full control over which objects get selected and what happens when certain objets are selected.

The format for a callback activated upon selection/deselection of an object is as follows:

typedef void SoSelectionPathCB(void *userData, SoPath *path);

When an object is selected or deselected, the appropriate callback will be called with the user's specified data as the first argument, and the path to the appropriate node as the second argument. A path is an object which represents a path from the root node of the scene graph to any other node in the graph; essentially, it is a sequence of nodes, and it can be operated upon as a stack using pushes and pops. We can use the getTail method of SoPath to find the selected or deselected object.

Let's create a very simple example to try this out:

;; create viewer
(define viewer (new-SoXtExaminerViewer))
(-> viewer 'show)

;; set up scene graph with selection
(define root (new-SoSelection))
(-> root 'ref)
(-> root 'addChild (new-SoSphere))
(define transform (new-SoTransform))
(-> root 'addChild transform)
(-> (-> transform 'translation) 'setValue 3 0 0)
(-> root 'addChild (new-SoCone))

;; set up selection callback
(define callback-info (new-SchemeSoCBInfo))
(-> callback-info 'ref)
(-> (-> callback-info 'callbackName) 'setValue "selection-callback")
(-> root 'addSelectionCallback (get-scheme-selection-path-cb) 
    (void-cast callback-info))

;; selection callback function
(define (selection-callback data path)
  (let ((selected-object (-> path 'getTail)))
    (display "Object ")
    (display selected-object)
    (display " was selected.")

(-> viewer 'setSceneGraph root)
(-> viewer 'setViewing 0)
Now we can display the "identity" of the object which was selected; but note that we still have no programmatic knowledge about which object was picked. The data type returned by SoPath's getTail method is an SoNode; both SoSphere and SoCone are subclasses of SoNode, and it seems as though we can not tell the difference between them.

There are two solutions to this problem. One is to assign each object a name using SoBase's setName method. This name is stored inside the object, and can be retrieved later using the getName method. By checking the name of the object at the tail of the path, we could tell the difference between the cone and the sphere.

However, because we are not trying to distinguish between two objects of the same type (i.e. two cones), there is a better way to achieve this functionality. All Inventor nodes store information internally about their data type; that is, a cone "knows" that it is a cone, even if it is represented as one of its parent classes (i.e. SoNode). In addition, all Inventor classes have a unique type identifier, which can be accessed by the static getClassTypeId function. For example:

returns the type identifier for the SoSphere class. The isOfType method of SoBase takes as argument a type specifier and returns 1 if the object is of this data type, and 0 otherwise. So we could rewrite the above callback function as follows:
(define (selection-callback data path)
  (let ((selected-object (-> path 'getTail)))
    (if (= 1 (-> selected-object 'isOfType (SoSphere::getClassTypeId)))
	(let ((the-sphere (SoSphere-cast selected-object)))
	  (display "Picked a sphere.")
	  ;; additional operations with sphere
	(let ((the-cone (SoCone-cast selected-object)))
	  (display "Picked a cone.")
	  ;; additional operations with cone
Several possibilities come to mind for how to use this: for example, you could have some text on the screen saying "new game", and each time that text is clicked it could reset the state of the game.

There are three policies for selection of objects; the chosen one is indicated in SoSelection's policy field. The default, SHIFT, policy selects the object under the mouse when the left button is clicked (deselecting all previously selected objects), deselects all objects when the mouse is clicked when over the background, and adds the object under the mouse to the selection list when an object is selected with the shift button depressed. The TOGGLE policy is equivalent to the SHIFT policy with the shift key depressed; the SINGLE policy is equivalent to the SHIFT policy without the shift key depressed. The selection node's policy can be changed in the following way:

(-> (-> my-selection 'policy) 'setValue SoSelection::SINGLE)
NOTE that the definition of the TOGGLE policy in Scheme is SoSelection::ENUM_TOGGLE, because of a conflict with the toggle method of the SoSelection class. C++ does not have this conflict.

Interaction via events

We have seen how to create high-level interaction with the scene using the SoSelection node. If lower-level interaction is desired, such as monitoring of the mouse's position and pressing of keys, callbacks for individual events can be set up using the SoEventCallback node.

An SoEventCallback node is added to the scene graph like any other node, but has a method which allows callbacks for specific event types to be set up; when one of these events occurs in the viewer where this scene graph is being displayed, it is handled by the EventCallback node.

SoEventCallback - node which invokes callbacks for events
SoBase > SoFieldContainer > SoNode > SoEventCallback
typedef void SoEventCallbackCB(void *userData, SoEventCallback *node)

Methods from class SoEventCallback:

void addEventCallback(SoType eventType, SoEventCallbackCB *f, void *userData = NULL)
void setHandled()

From looking at the SEE ALSO section at the bottom of the manual page for SoEvent, we can see the different types of events for which we can set up callbacks:

We will ignore the Motion3Event and SpaceballButtonEvent types because we do not have 3-D input devices available.

To set up a callback for a key press, for example, we would do the following. Note the very similar structure for setting up a callback as in the sensor and selection examples.

(define viewer (new-SoXtExaminerViewer))
(-> viewer 'show)

(define root (new-SoSeparator))
(-> root 'ref)

(define event-callback (new-SoEventCallback))
(-> root 'addChild event-callback)

(define mat (new-SoMaterial))
(-> root 'addChild mat)

(-> root 'addChild (new-SoSphere))
(-> viewer 'setSceneGraph root)
(-> viewer 'setViewing 0) ;; select the arrow icon in the viewer

;; add a callback (defined below) for key presses.
;; Note that the callback (of type SoEventCallbackCB *) is acquired by
;; the call to (get-scheme-event-callback-cb), and the data for the
;; callback is, as usual, a SchemeSoCBInfo (cast to a void *).
;; We use the material node as the user data field in this example.

(define callback-info (new-SchemeSoCBInfo))
(-> callback-info 'ref)
(-> (-> callback-info 'callbackName) 'setValue "key-event-cb")
(-> (-> callback-info 'affectsNode) 'setValue mat)
(-> event-callback 'addEventCallback (SoKeyboardEvent::getClassTypeId)
                                     (void-cast callback-info))

(define (key-event-cb user-data event-node)
  (let ((material-node (SoMaterial-cast user-data))
	(event (-> event-node 'getEvent)))
    (if (= 1 (SO_KEY_PRESS_EVENT event ANY))
	  ;; turn the material node red.
	  (-> (-> material-node 'diffuseColor) 'setValue 0.8 0.2 0.2))
	  ;; return the material node to white on key release events.
	  (-> (-> material-node 'diffuseColor) 'setValue 0.8 0.8 0.8)))
    ;; tell the callback node that we have handled the event.
    (-> event-node 'setHandled)))
Now whenever we press a key, the sphere turns red. When we release the key, the sphere returns to white. We could have tested the event for specific key presses and performed different functions for different keys.

One thing about the above code bears explaining: the use of the SO_KEY_PRESS_EVENT macro. From the manual page for SoKeyboardEvent:

SoKeyboardEvent - keyboard key press and release events
SoEvent > SoButtonEvent > SoKeyboardEvent
#define SO_KEY_PRESS_EVENT(EVENT,KEY) (SoKeyboardEvent::isKeyPressEvent(EVENT,SoKeyboardEvent::KEY))
#define SO_KEY_RELEASE_EVENT(EVENT,KEY) (SoKeyboardEvent::isKeyReleaseEvent(EVENT,SoKeyboardEvent::KEY))

Methods from class SoKeyboardEvent:

static SbBool isKeyPressEvent(const SoEvent *e, SoKeyboardEvent::Key whichKey)
static SbBool isKeyReleaseEvent(const SoEvent *e, SoKeyboardEvent::Key whichKey)

There are similar macros for SoMouseButtonEvent:

SoMouseButtonEvent - mouse button press and release events
SoEvent > SoButtonEvent > SoMouseButtonEvent
#define SO_MOUSE_PRESS_EVENT(EVENT,BUTTON) (SoMouseButtonEvent::isButtonPressEvent(EVENT,SoMouseButtonEvent::BUTTON))
#define SO_MOUSE_RELEASE_EVENT(EVENT,BUTTON) (SoMouseButtonEvent::isButtonReleaseEvent(EVENT,SoMouseButtonEvent::BUTTON))

Methods from class SoMouseButtonEvent:

static SbBool isButtonPressEvent(const SoEvent *e, SoMouseButtonEvent::Button whichButton)
static SbBool isButtonReleaseEvent(const SoEvent *e, SoMouseButtonEvent::Button whichButton)

These macros are usually defined by the C preprocessor and save keystrokes when testing whether an event is a key or mouse button press or release event. They have been redefined in Scheme to have similar syntax to the C form; an example of the syntax is in the code above. There are more examples in the Scheme versions of the Inventor Mentor examples. Note that these functions return an SbBool, so in Scheme you must test the result of the macro to see whether it is 0 or 1.

Keyboard events are of obvious utility; if you want to have keyboard-driven interactivity with your program, it is straightforward to implement it. Mouse button events are probably trickier to implement usefully. Ideally, you would probably like the kind of high-level functionality (i.e. 3-D picking) which SoSelection provides. However, you can get the mouse's 2D position within the window by using the getPosition or getNormalizedPosition methods of SoEvent; you might then calculate a trajectory based on that point and send an object along it (as in the firing of shells in Missile Command).

Location2Events are generated whenever the mouse reaches a new position in the window; they are not generated continuously with the mouse's current position. You might use a Location2Event to update the mouse's current position in the window, and an idle callback to perform some manipulation based on that position (like moving the camera).

Next lecture

Next time we will examine the implementation of a complete game in Scheme - Tetris.

Back to the CGW '96 home page

$Id: index.html,v 1.2 1996/01/19 18:45:12 kbrussel Exp $