Computer Graphics Workshop '96 Problem Set 4

1/16/96

Problems
Problem 1 - Making a faster torus

If you followed the instructions closely in the last problem set during the creation of a torus, you probably used an SoFaceSet to represent the torus' geometry. However, by computing the vertices for the polygons one at a time, each vertex is computed four times, when it only needs to be computed once.

Restructure your torus to use an SoQuadMesh instead of a face set. This involves the writing of the function compute-torus-points, which instead of returning a list of the coordinates of all the polygons should return a list of the vertices on the torus. You may loop over either the horizontal or vertical dimension first.

Once that is done, you can replace the face set with a quad mesh. This should involve only two steps; telling the program how many rows and columns are in the vertex list. Be extra careful when setting these values, because an "off by one" error will probably cause Inventor to crash. Hint: in order for your torus to show up properly you must repeat the first set of vertices as the last set of vertices in each loop. This makes the number of rows and columns equal to the horizontal or vertical resolution plus one.

After you have finished the above steps and have gotten the torus to render using the quad mesh, you may have to change the vertexOrdering field of your ShapeHints node if your torus shows up very dark on the screen.

Problem 2 - Cannon fodder

In this problem we will see how to use a field sensor to keep track of the movement of some objects in the scene. We will recreate a scene reminiscent of the old game "Cannon Fodder" - there will be a moving target (user-controlled through a HandleBoxManip), and every time the target comes into the line of fire, it turns red.

We give you the basic scene graph you should use here:

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

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

(define cannon-sep (new-SoSeparator))
(-> root 'addChild cannon-sep)

; (define manip (new-SoTrackballManip))
; (-> root 'addChild manip)

(define cannon-base (new-SoCube))
(-> (-> cannon-base 'width) 'setValue 4)
(-> cannon-sep 'addChild cannon-base)

(define cannon-xform (new-SoTransform))
(-> cannon-sep 'addChild cannon-xform)
(-> (-> cannon-xform 'translation) 'setValue 0 4.0 0)

(define cannon-tube (new-SoCylinder))
(-> cannon-sep 'addChild cannon-tube)
(-> (-> cannon-tube 'height) 'setValue 6.0)

(define target-sep (new-SoSeparator))
(-> root 'addChild target-sep)

(define target-manip (new-SoHandleBoxManip))
(-> target-sep 'addChild target-manip)
(-> (-> target-manip 'translation) 'setValue 5 15 2)

(define target-mat (new-SoMaterial))
(-> target-sep 'addChild target-mat)
; (-> (-> target-mat 'diffuseColor) 'setValue 0.8 0.8 0.8) 
;; (this is the default value)

(define target (new-SoSphere))
(-> (-> target 'radius) 'setValue 0.5)
(-> target-sep 'addChild target)

(-> viewer 'setSceneGraph root)
Hook up a field sensor to the translation field of target-manip. The callback for this sensor should get the current translation out of the target-manip node and figure out whether the target is in the line of fire. The line of fire goes up along the y axis (that is, the y axis is in the center of the tube of the cannon). When the target is in the line of fire, it should turn red.

You may handle border cases in any way you like, but the target should definitely not be red when it is behind the cannon.

This is a short problem and should not require more than about 20 lines of code.

Problem 3 - User interface for the torus

In this problem we will create a new type of "slider widget" object in Scheme. This object will consist of a text object for its title and (floating point) value, and an SoTranslate1Dragger for modifying the value. We will then use this new object to create an interactive user interface for the torus. This is a long problem, and we recommend you get started on it early.

As in the case of the torus, we will be using the SICP-like object system defined in problem set 3. You may want to save the following definitions in a file and load them in at the top of your problem st file:

;;; SICP-ish simple object system for dealing similarly
;;; with Scheme and C++ objects using "send"

(define (send object message . args)
  (if (C++-object? object)
      (eval `(-> ,object ',message ,@args))
      (let ((method (get-method object message)))
	(if (not (no-method? method))
	    (apply method (cons object args))
	    (error "No method named" message)))))

(define (get-method object message)
  (object message))

(define (no-method method)
  'no-method)

(define (no-method? method)
  (eq? method 'no-method))
The scene graph for the ValueSlider will look like this:

                   root
              (SoSeparator)
                    |
      -----------------------------
      |             |             |
    font          text         dragger
  (SoFont)      (SoText3)(SoTranslate1Dragger)
The SoFont and SoText3 nodes are used in tandem to provide 3D textual annotation in the scene. The text node will be used to print the title text and the current value for the ValueSlider. If you want the text to show up on the left side of the dragger, as opposed to above it, you will need to insert an SoTransform node between the text and dragger nodes to offset the position of the dragger from the text.

Create the above scene graph. Note that the SoText3 node contains an SoMFString field called string: This contains the text to be displayed. You can change the visible text by calling the set1Value method of SoMFString. Note that this method takes an SbString as its second argument, so you must do the following, for example (using the above "send" syntax):

(send (send text 'string) 'set1Value 0 (new-SbString 
					"First line of text"))
(send (send text 'string) 'set1Value 1 (new-SbString
					"Second line of text"))
Adjust the font size and the vertical position of the dragger (using its translation field) until the dragger and text are in proportion. (Using the default values, the dragger will be much smaller than the text.) You can change the justification of the text by changing the justification field of the text node. Later on you will be printing the value of the slider using this text; you can get a string out of a floating-point value by calling the standard Scheme number->string function.

Once you have these parameters down, you are ready to write the constructor for a new ValueSlider object. This object will take at least the following parameters:

The beginning of the new-ValueSlider function will look like the following:

(define (new-ValueSlider min-value max-value current-value title)
  (define root (new-SoSeparator))
  (send root 'ref)

  ;; Build scene graph and set up initial position of dragger
Once you have built the scene graph for the ValueSlider inside the constructor, you are ready to implement the methods for the object.
  (lambda (message)
    (cond 
     ((eq? message 'getGeometry)
      (lambda (self)
	root))
Remember that all methods take a pointer to the current object as their first argument.

The getGeometry method returns a pointer to the root of the scene graph of the ValueSlider object. This can then be added to other scene graphs as a "fully encapsulated" new type of Inventor object.

Write the getValue method, which returns the current-value instance variable.

In order for this new object to be useful, we need to have three features:

To implement all three of these items, we must have some notification when the dragger has been moved. We will therefore add a value changed callback to the dragger, whose purpose will be to call the updateValue method of our ValueSlider. Look at the manual page for SoDragger. Note the definition of an SoDraggerCB:

typedef void SoDraggerCB(void *userData, SoDragger *dragger)

Therefore a Scheme dragger callback function must take two arguments, the user data field (which we will not use) and a pointer to the relevant dragger. Recall the method we introduced for setting up callbacks from within Scheme: we use the C++ "wrapper function" as the function pointer (obtained with the call here of (get-scheme-dragger-cb)), and a SchemeSoCBInfo node as the data for this callback. This node contains the information about which Scheme callback should be called.

Write the method getCallbackInfo, which takes the name of the callback as argument. It should create a new SchemeSoCBInfo node, increment its reference count, set the value of its callbackName field to the passed callback name, and return this node.

Write the getValueChangedCallback method, which takes no arguments (that is, it takes only the "self" pointer as argument). This method will return the function to be called whenever the dragger is moved. Since this function is contained within the ValueSlider object, it can call all the methods of this specific ValueSlider instance. To sum up, this method should return a function of two arguments (user-data and dragger); the body of this function should be the call

(send self 'updateValue)
Because of some poor design decisions (for which I apologize), all Scheme callbacks to be used by Inventor must be referenced by name; that is, there must be a name in the global environment pointing to the callback function. Therefore setting up the actual value changed callback for the dragger is a two step process.

NOTE: the following paragraph has been obsoleted. Please see the one following it for the correct problem.

Write the addValueChangedCallback method. This method takes the callback name as argument. It calls the getCallbackInfo method with the callback name, and calls the addValueChangedCallback method of the dragger object to set up the callback:
(send dragger 'addValueChangedCallback (get-scheme-dragger-cb) 
      (void-cast callback-info))
Write the addValueChangedCallback method. This method takes the callback name as argument. It calls the getCallbackInfo method with the callback name, and calls the addMotionCallback method of the dragger object to set up the callback:
(send dragger 'addMotionCallback (get-scheme-dragger-cb) 
      (void-cast callback-info))
Now we are ready to write the updateValue method, which is called by the value changed callback. This method takes no arguments. It should get the value translation field (an SbVec3f), and get the x coordinate out of this vector (you can use the operator-brackets method for this). It should change the current-value variable using set!, and then update the value displayed in the text object.

At this point you should be able to create a new ValueSlider widget, call the getGeometry method to get the root of its scene graph, and view that in a viewer. Note that the text does not change yet; we have not set up the value changed callback. Since doing so is a kludge, this part of the problem is a freebie:

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

(define my-slider (new-ValueSlider 0 10 5 "My slider"))
(send viewer 'setSceneGraph (send my-slider 'getGeometry))

;; get the callback defined in the global environment
(define my-slider-callback
  (send my-slider 'getValueChangedCallback))

;; pass in this definition to the addValueChangedCallback method
(send my-slider 'addValueChangedCallback "my-slider-callback")
Now the text should update when the slider moves.

We still have two more goals to fulfill: constraining the movement of the slider and getting it to update some other object's field. We will do the second of these first. Create a variable (in the same place as the "Build scene graph" code) called callback-list, which should be initialized to '(), the empty list. Write a method, addOutputCallback, which takes a function, callback-function, as argument, and adds it to the callback list.

Now add a section to the updateValue method which calls each of the functions in the callback list with current-value as their argument.

At this point you should be able to hook up a slider to, say, the minor radius of the torus, using something like the code here:

;; creation of the viewer, torus, etc.

(define minor-radius-slider
  (new-ValueSlider 0 10 3 "Minor radius"))
(define minor-radius-slider-callback
  (send minor-radius-slider 'getValueChangedCallback))
(send minor-radius-slider 'addValueChangedCallback
      "minor-radius-slider-callback")

;; callback which changes torus' minor radius size
(define (torus-minor-radius-callback new-value)
  (send my-torus 'setMinorRadius new-value))
;; adding this callback to the ValueSlider
(send minor-radius-slider 'addOutputCallback
      torus-minor-radius-callback)
Whenever the value of the slider changes, the torus' minor radius will change to this new value.

NOTE: the following paragraph has been obsoleted. Please see the one following it for the corrected problem. (The method described in the following paragraph only works for Inventor 2.1, which is not available on Athena.)

The final improvement we need to add is the constraint that the slider must remain between the minimum and maximum values. We can do this within the updateValue method as well; all we need to do is boundary check the horizontal component of the translation and set it to the maximum or minimum if either is exceeded. However, a precaution must be taken: if you change this value inside the value changed callback, it may trigger another value changed callback, which could cause an infinite loop. To prevent this, you should disable all other value changed callbacks for the dragger when setting the dragger's translation from within the value changed callback. You can do this with the enableValueChangedCallbacks method of SoDragger; this method takes an SbBool as argument and returns an SbBool which was its old value. NOTE that the manual page may say that the enableValueChangedCallbacks method takes NO arguments; this is WRONG! Reenable them after you set the dragger's translation.

The final improvement we need to add is the constraint that the slider must remain between the minimum and maximum values. We can do this within the updateValue method as well. all we need to do is boundary check the horizontal component of the translation and set it to the maximum or minimum if either is exceeded. However, a precaution must be taken: if you change this value inside a value changed callback, it may trigger another value changed callback, which could cause an infinite loop. In addition, there seem to be problems with a Scheme value changed callback causing itself to get called even once. For this reason, we have used a motion callback in the addValueChangedCallback method (see above); this gets called when the mouse cursor moves while dragging. Since there is no way to move the mouse pointer within this program, we run no risk of having our motion callback call itself. Add the functionality to the updateValue method to boundary check the horizontal component of the dragger's translation, and set the ValueSlider's internal notion of the current value as well as the translation of the dragger.

Now you should have a complete slider widget which can be used to affect "fields" in other Scheme objects. Hook up one widget to the major radius of a torus and another to the minor radius. Other additions you may want to add are scaling factors for the widget (so the value will be some multiple of its translation) and integer-valued sliders (to be able to affect the resolution of the torus without doing the conversion in the callback).


Back to the CGW '96 home page

$Id: index.html,v 1.4 1996/01/23 02:45:48 kbrussel Exp $