Computer Graphics Workshop '97 Lecture Notes

1/15/97

Today's topics
Inventor application model

So far we have been concerned only with understanding some of the facilities Inventor provides for generating computer graphics. However, there is much more involved in a real 3D application than just the graphics: there must be interaction between the program and the user, and there must be some computation going on behind the graphics.

The model for a computer application that most of you are familiar with is probably the one in whch the program starts, does some computation, perhaps interacts with the user, and then ends. At all times, the flow of control of the program is in your (or your application's) hands.

Inventor applications reverse this model. Instead, Open Inventor controls the flow of control of the program, and the programmer registers callbacks for certain events. Inventor takes care of detecting these events, such as the picking of objects in the scene using the mouse, and calls a user-specified procedure when the events occur.

Let's examine two different ways of creating a simple animation in Inventor: first the "direct control" method, and later the better, Inventor-friendly method.

Direct control of program - disadvantages

Let's write a short program to make a sphere orbit a cone.
(define pi 3.1415926585)

(define root (new-SoSeparator))
(-> root 'ref)
(define viewer (examiner root))
(-> root 'addChild (new-SoCone))

(define transform (new-SoTransform))
(-> root 'addChild transform)
(-> root 'addChild (new-SoSphere))

(define (loop radius step-number steps-per-revolution)
  (let ((x (* radius (cos (* 2 pi
			     (/ step-number
				steps-per-revolution)))))
	(z (* radius (sin (* 2 pi 
			     (/ step-number
				steps-per-revolution))))))
    (-> (-> transform 'translation) 'setValue x 0.0 z)
    (if (< (1+ step-number) steps-per-revolution)
	(loop radius (1+ step-number) steps-per-revolution)
	(loop radius 0 steps-per-revolution))))

(loop 3.0 0 30)  ; radius = 3, 30 steps per revolution
What happens? Nothing, or so it seems. The problem is that the loop is running without giving Inventor a chance to update the viewer. Let's insert the following line after the setting of the translation field in the transform node:
(-> viewer 'render)
Now we can see the sphere rotating around the cone, but note that we can not interact with the scene, and can not type at the Scheme interpreter. While we succeeded in our goal of animating the scene, we did so in a way that disabled all other functionality that Inventor provides.

Callbacks and the Inventor Xt mainloop

Why did the above method of application design not work well? Specifically, why did interaction stop working once we had the scene being updated? The reason is as follows. The examiner viewer knows how to handle mouse-based interaction; when events such as button presses and mouse movements are received, the viewer handles their processing and moves the scene around. These events are created by the X window system and are translated into Inventor events by Inventor. In this context Inventor is a program, and the flow of control must return to that program in order for the events to be handled. Since we are stuck in our "loop" procedure, Inventor does not have a chance to run, and the mouse events never get processed.

Inventor's "loop" procedure is called the Xt main loop; the actual function is called SoXt::mainLoop. This procedure is called before the Scheme interpreter starts; in fact, the interpreter is run from inside this main loop. When the user is not actively interacting with the interpreter, the Inventor main loop is still running, processing events and rendering scenes. If the interpreter goes into an infinite loop, as in the above example, the Inventor main loop stops.

The solution to our problem is to create a callback; Inventor will "call us back" when our program is allowed to run. By making this concession, that is, losing the "on-demand" feel of Inventor, we gain all of its functionality for interactivity.

Sensors

Sensors are Inventor objects which detect certain occurrences and call user-specified functions when they happen. Specifically, sensors can detect:

Timer sensors

Let's redo the above animation using a timer sensor for the animation.
(define pi 3.1415926585)

(define root (new-SoSeparator))
(-> root 'ref)
(define viewer (examiner root))
(-> root 'addChild (new-SoCone))

(define transform (new-SoTransform))
(-> root 'addChild transform)
(-> root 'addChild (new-SoSphere))

(define *steps-per-revolution* 30)
(define *radius* 3.0)

(define sensor-cb-func
  (let ((step-number 0))
    (lambda (user-data sensor)
      (let ((x (* *radius* 
		  (cos (* 2 pi
			  (/ step-number
			     *steps-per-revolution*)))))
	    (z (* *radius* 
		  (sin (* 2 pi
			  (/ step-number
			     *steps-per-revolution*))))))
	(-> (-> transform 'translation) 'setValue x 0.0 z)
	(if (< (1+ step-number) *steps-per-revolution*)
	    (set! step-number (1+ step-number))
	    (set! step-number 0))))))

;; set up callback

;; "sensor" defined in CGWInit.scm
(define timer-sensor (sensor new-SoTimerSensor sensor-cb-func))
(-> timer-sensor 'setInterval
    (new-SbTime (/ 1.0 30.0))) ; repeat 30 times/sec

;; start animating
(-> timer-sensor 'schedule)
A timer sensor is an object which calls its callback at regular intervals (specified using the setInterval command), starting at some time (by default, the time at which the sensor is scheduled). It is useful for performing tasks which have timing constraints (for example, animation). However, there is no guarantee that the sensor will actually achieve the firing rate which is specified; it is reasonable to expect 10-15 Hertz performance.

The first thing to notice in the code above is the call of the schedule method of "timer-sensor". This call puts this sensor on the timer queue. There are two sensor queues in Inventor: the timer queue and the delay queue. The timer queue contains all scheduled alarm and timer sensors, sorted by time until activation; the delay queue contains all other types of sensors. The call of

(-> timer-sensor 'schedule)
tells Inventor that this sensor should be triggered at some time in the future. Because this type of sensor is designed to be triggered multiple times, we need only schedule it once. Sensors designed to go off only once (SoOneShotSensor, SoIdleSensor, SoAlarmSensor) are not automatically rescheduled.

The second thing to notice about the above program is the callback function "sensor-cb-func". This is the function which Inventor calls when the sensor fires. It takes two arguments: the first is of type void *, or a generic C pointer; the second is a pointer to the sensor which called the function. Note how we found this function template; from the SoSensor manual page:

NAME
SoSensor - abstract base class for Inventor sensors
INHERITS FROM
SoSensor
SYNOPSIS
typedef void SoSensorCB(void *data, SoSensor *sensor)

Methods from class SoSensor:

void setFunction(SoSensorCB *callbackFunction)
void setData(void *callbackData)

This means that a valid sensor callback has no return value, and takes a void pointer and an SoSensor pointer as arguments. From Scheme's perspective, this means that the function must take two arguments, and that when it is called the arguments' values will have the above types.

The last thing to notice about the above setup procedure is the call to "sensor", which takes care of instantiating the sensor (using the passed sensor constructor function, in this case new-SoTimerSensor) and setting up the Scheme/C++ interface so the appropriate Scheme function (sensor-cb-func) gets called. We won't go into the details of how this interface is implemented (although full details are given at the end of these lecture notes), but we will discuss how to set up general Inventor callbacks below.

Idle sensors

Idle sensors (SoIdleSensor) are called by Inventor whenever the CPU is idle. This might allow a low-priority task to be called whenever there is nothing better to do. However, if an idle sensor is rescheduled from within its callback, the CPU will never become "idle". Instead, it will keep getting called as fast as possible under the constraints of the rest of the Inventor application. For example, all interaction with viewers will continue to work.

One example of an idle sensor in action is the Scheme interpreter itself. Before the Inventor main loop is entered by the Scheme application, ivyscm, an idle sensor is scheduled. The callback for this sensor first reschedules the sensor, and then checks the standard input to see if the user has typed anything. If something has been entered, it calls the Scheme interpreter to evaluate the expression; if not, it immediately returns, allowing the rest of the application to continue execution.

Node and field sensors

SoFieldSensor and SoNodeSensor are two types of data sensors; they detect when a value has changed in a field or node. More specifically, a field sensor is attached to a field and calls its callback when that value changes; a node sensor is attached to a node and is triggered when a field's value changes in that node or in any node below it, or when the layout of the scene graph changes below that node (i.e. by adding or removing nodes from the scene graph).

The Inventor Mentor's examples 12.1.FieldSensor, 12.2.NodeSensor, and 12.4.TimerSensor (see the first lecture's notes for their locations) are good references for using all of these types of sensors. (Note that the code currently doesn't use the convenience functions described above for the creation of the sensors.)

You might use a field or node sensor to propagate changes through your scene graph; for example, one object might change some value in another, which would cause a node sensor to be triggered, which would cause this new object to make changes to others. This method of updating objects in the scene graph is very user-controllable and is therefore suitable for very complicated relationships of objects, but has the disadvantage of being complicated to control. Next week we will discuss engines in more detail; these objects (some experimentation with which was included in the second problem set) can be used to make fairly simple constraints between the positions of objects that can be constructed once and ignored from then on.

Other types of callbacks

In order to build interactive Inventor applications, it is necessary to understand how to set up general types of Inventor callbacks using ivyscm's convenience routines. Here's the definition of sensor:
(define (sensor constructor callback . node)
  (if (pair? node)
      (constructor (get-scheme-sensor-cb)
		   (void-cast (callback-info callback (car node))))
      (constructor (get-scheme-sensor-cb)
		   (void-cast (callback-info callback)))))
Note that one of the constructors for SoTimerSensor (and all of the other types of sensors) takes the callback and user data which the sensor should use:

NAME
SoTimerSensor - sensor that triggers callback repeatedly at regular intervals
INHERITS FROM
SoSensor > SoTimerQueueSensor > SoTimerSensor
SYNOPSIS
Methods from class SoSensor:

SoTimerSensor()
SoTimerSensor(SoSensorCB *func, void *data)

The "sensor" convenience function sets up the sensor with a "magic" C++ callback (obtained with get-scheme-sensor-cb) which allows a Scheme function to be used as a callback. The Scheme information is contained in the user data argument, and is created with a call to callback-info:

;; callback-info: takes as arguments a procedure (usually of two
;; arguments) to be used as an Inventor callback, and an optional
;; SoNode argument which will be passed as the user data to the
;; Scheme function. (usually unused)
(define (callback-info callback . node)  ;; defined in CGWInit.scm
   ;; Insert magic here
   )
To set up an Inventor callback:
  1. Find the type of the callback you need. For example, sensors take an SoSensorCB (SoSensor's man page), draggers use an SoDraggerCB (SoDragger), and EventCallback nodes (which we will discuss next time) need an SoEventCallbackCB.
  2. Determine the name of the Scheme accessor function for the C++ interface callback. For example, the procedure get-scheme-sensor-cb (which takes no arguments) returns an SoSensorCB; get-scheme-event-callback-cb returns an SoEventCallbackCB. This is the only way to acquire an "So...CB" object in ivyscm.
  3. Call the method of your object (a sensor, dragger, etc.) to set up the callback and user data. The callback is the output of the accessor function, above. The user data is the result (type-cast to a "void" using (void-cast obj)) of calling the callback-info convenience function with the name of your Scheme callback (which must have the same number of arguments as the C++ function from step 1). Optionally, you may pass in a (lambda (user-data obj) ...) directly as the argument to callback-info.

Here's an example of how to set up a ValueChanged callback on an SoTranslate1Dragger:

(define dragger (new-SoTranslate1Dragger))
(define (my-dragger-cb user-data drag)
  (let ((my-dragger (SoTranslate1Dragger-cast drag)))
    ;; ...Now we can use my-dragger as an SoTranslate1Dragger...
    ;; Note we are not using the user-data field.
    (display "I got called\n")))
(-> dragger 'addValueChangedCallback
    (get-scheme-dragger-cb)
    (void-cast (callback-info my-dragger-cb)))
Full explanation of the callback mechanism

Let's look at the "unwrapped" code for the above timer sensor example.
(define pi 3.1415926585)

(define root (new-SoSeparator))
(-> root 'ref)
(define viewer (examiner root))
(-> root 'addChild (new-SoCone))

(define transform (new-SoTransform))
(-> root 'addChild transform)
(-> root 'addChild (new-SoSphere))

(define *steps-per-revolution* 30)
(define *radius* 3.0)

(define sensor-cb-func
  (let ((step-number 0))
    (lambda (user-data sensor)
      (let ((x (* *radius* 
		  (cos (* 2 pi
			  (/ step-number
			     *steps-per-revolution*)))))
	    (z (* *radius* 
		  (sin (* 2 pi
			  (/ step-number
			     *steps-per-revolution*))))))
	(-> (-> transform 'translation) 'setValue x 0.0 z)
	(if (< (1+ step-number) *steps-per-revolution*)
	    (set! step-number (1+ step-number))
	    (set! step-number 0))))))

;; set up callback

(define cb-info (new-SchemeSoCBInfo))
(-> cb-info 'ref)
(-> (-> cb-info 'callbackName) 
    'setValue "sensor-cb-func")

(define timer-sensor
  (new-SoTimerSensor (get-scheme-sensor-cb) 
		     (void-cast cb-info)))
; could alternatively write the above line like this:
;(define timer-sensor (new-SoTimerSensor))
;(-> timer-sensor 'setFunction (get-scheme-sensor-cb))
;(-> timer-sensor 'setData (void-cast cb-info))
(-> timer-sensor 'setInterval
    (new-SbTime (/ 1.0 30.0))) ; repeat 30 times/sec

;; start animating
(-> timer-sensor 'schedule)
In C++, the setFunction method takes as its argument a function pointer; unfortunately, this notion is completely incompatible with the Scheme notion of closures. Instead ivyscm provides wrapper functions written in C++ whose only purpose is to call a provided Scheme function. The call to (get-scheme-sensor-cb) returns a valid function pointer which can be used as the argument to setFunction. This function expects as its data argument an object of type SchemeSoCBInfo. This class has the following data members:

SoSFString callbackName;
SoSFNode affectsNode;

The function that (get-scheme-sensor-cb) provides takes, as all sensor callbacks must, a void pointer as its first argument and a sensor pointer as its second. It first casts the void pointer to a SchemeSoCBInfo, and extracts the function name field. It then calls this Scheme function by name with the first argument being the node named in the affectsNode field, type cast to a void pointer. The second argument is the sensor which called this function.

When attempting to use the callback data (the affectsNode field of the SchemeSoCBInfo class), the following precaution must be taken. Because an SoSFNode field increments the reference count of the node it contains, you can not simply type cast any data type to an SoNode when calling the setValue method of this class. For this reason, when using Scheme it is easier to make all variables that a callback is likely to use global variables, rather than relying on the callback's user data argument.

Note that here the callback must be specified by a string containing its name; it must be defined at the top level in order for it to be visible to C++. The convenience routine callback-info contains a nifty hack to bind a procedure to a name in the global environment; see the source code for more details.

Next lecture

Next time we will discuss two methods of introducing true graphical interactivity into your application: selection and event processing.


Back to the CGW '97 home page

$Id: index.html,v 1.6 1997/01/04 03:51:51 kbrussel Exp $