Computer Graphics Workshop '97 Lecture Notes
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.
(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 revolutionWhat 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.
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.
(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:
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.
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.
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.
(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:
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:
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)))
(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:
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.
$Id: index.html,v 1.6 1997/01/04 03:51:51 kbrussel Exp $