Computer Graphics Workshop '97 Lecture Notes

1/24/97

Today's topics
Engines

As we saw in the second problem set, engines are objects which accept connections from fields as inputs, perform some calculation, and drive the results to their outputs, to which other fields can be connected. Engine networks are useful for performing automatic calculations and animations without user intervention.

Most of the material in this lecture is derived from Chapter 13 of The Inventor Mentor.

Field to field connections and engine outputs

We have seen, again in the second problem set, that fields can be directly connected to one another, as well as hooked up to engines. There are several different types of fields, and Inventor provides some built-in converters which allow fields of one type to be hooked up to fields of another type. The following conversions are supported (from the Inventor Mentor, p. 335-6):

  • Any field type to String
  • String to any field type
  • Between any two of Bool, Float, Long, Short, ULong, UShort
  • Between Color and Vec3f
  • Between Float and Time
  • Between Matrix and Rotation
  • Between Name and Enum
  • Between Rotation and Vec4f (quaternion)
  • From an MF field to its SF version, and from an SF field to its MF version

We have already used one of these conversions; recall that all the inputs and outputs to the Compose and Decompose engines in the second problem set were MF fields, but that we were hooking up single-value, or SF, fields to them.

All outputs from engines are actually SoEngineOutputs, not fields. This means that you can not perform a getValue on the output of an engine. However, engine outputs do have types (which are indicated in parentheses in the manual); you can hook up engine outputs to fields of the appropriate type (subject to the field-to-field conversions listed above).

If you need to get the value of an engine's output and can not structure your scene graph so you can connect the engine's output to one of your nodes' fields, you can create a global field, hook up the engine to that field, and then perform a getValue. (You should NOT instantiate a new field outside of a node, by calling (for example) new-SoSFVec3f.) For example:

;; Example which creates a global field and hooks up
;; an engine output to it, then performs a getValue
;; on the field. Note that this is poor programming
;; style, and wherever possible you should avoid
;; having to perform a getValue on the end of an
;; engine network.

;; Create the global field.
;; Note that now we can use SoDB::getGlobalField
;; with argument (new-SbName "MyField") to get
;; a handle on this field at a later time, but
;; still need to cast it to an SoSFVec3f.
(define my-vec3f-field (SoSFVec3f-cast
			(SoDB::createGlobalField
			 (new-SbName "MyField")
			 (SoSFVec3f::getClassTypeId))))
(define my-interp (new-SoInterpolateVec3f))
(send (send my-interp 'input0) 'setValue 0 0 0)
(send (send my-interp 'input1) 'setValue 1 1 0)
(send (send my-interp 'alpha) 'setValue 0.3)

(send my-vec3f-field 'connectFrom (send my-interp 'output))
(format #t "Interpolator's output was ~s\n"
	(send (send my-vec3f-field 'getValue) 'getValue))
Reference counting and engines

Engines are derived from SoBase, which means that every engine has an associated reference count. The mechanism for maintaining this reference count is different from that of nodes, however. Whenever an output of an engine is connected to a field, that engine's reference count is incremented; whenever this connection is broken, the engine's reference count is decremented. As with nodes, engines are deleted when their reference counts are decremented to zero. For safety, you should ref engines you want to keep around.

Engine networks

There are two main rules to keep in mind when designing an engine network. The first is that it is not possible to use more than one field or engine output as the input to another field. This is called fan-in. However, you can connect a field or engine output to many other fields, so it can act as multiple sources; this is called fan-out.

Loops in engine networks are legal. Inventor's notification mechanism knows when one field caused a chain of updates to occur which led back to itself, and will not cause infinite loops in the update mechanism. For example, we could synchronize two fields by doing the following:
(define trans1 (new-SoTranslation))
(define trans2 (new-SoTranslation))
(send (send trans1 'translation) 'connectFrom
      (send trans2 'translation)) 
(send (send trans2 'translation) 'connectFrom
      (send trans1 'translation)) 
Now whenever the translation field of either trans1 or trans2 changes, the other one will automatically update itself to hold the new value.

There are two advantages of using engines over sensors when possible:

  • Engines allow convenient abstraction of certain types of motions (like the rotating of blades on a helicopter). No user intervention is required once the network is set up.
  • Engines can be read from and written to .iv files along with the rest of the scene graph. For example, you could read in a .iv file which contains some animation, and use that as "static" geometry for characters in a game.

Engines and animation

There are several types of engines which can be composed in a network to perform animations:

  • time source engines (SoElapsedTime, SoOneShot, SoTimeCounter)
  • interpolation engines (SoInterpolateFloat, SoInterpolateVec[2,3,4]f, SoInterpolateRotation)
  • constraint/arithmetic engines (SoCalculator, SoCompose/Decompose engines, SoBoolOperation, SoTransformVec3f, SoComputeBoundingBox)

In order to create animations, we first need a source of time. An SoElapsedTime engine starts counting time when it is created, and can be paused or reset. An SoOneShot adds a duration field which indicates how long the engine will run, as well as a ramp field which goes from 0.0 to 1.0 over the engine's cycle. An SoTimeCounter engine counts integer values over a specified range and frequency. All of these engines use the realTime global field as their source. See the Inventor Mentor examples 13.1.GlobalFlds, 13.2.ElapsedTime, and 13.3.TimeCounter for examples of using the realTime field and the SoElapsedTime and SoTimeCounter engines.

Interpolation engines are good for performing a smooth sweep of an object (or the camera) through space. All of the interpolation engine types take in two fields, input0 and input1, as well as a floating point interpolation factor, alpha. They perform a linear interpolation between their inputs, and send the result to the output output. Here's an example of animating the camera to a new position over a specifiable period of time whenever the mouse is clicked.

;; Interpolation of camera position and rotation on demand.

;; Define scene graph and viewer, and extract camera
(define root (new-SoSeparator))
(send root 'ref)
(define text (new-SoText3))
(send (send text 'string) 'set1Value 0 (new-SbString "Dramatic"))
(send (send text 'string) 'set1Value 1 (new-SbString "Angles"))
(send (send text 'parts) 'setValue SoText3::ALL)
(send (send text 'justification) 'setValue SoText3::CENTER)
(send root 'addChild text)
(define v (examiner root))
(send v 'setViewing 0)
(define cam (send v 'getCamera))

;; Define interpolation engines with two fixed inputs
(define xl-interp (new-SoInterpolateVec3f))
(define rot-interp (new-SoInterpolateRotation))
(send (send cam 'position) 'connectFrom
      (send xl-interp 'output))
(send (send cam 'orientation) 'connectFrom
      (send rot-interp 'output))

;; These initial and final positions were obtained via
;; experimentation, and by extracting the values from
;; the camera's position and orientation fields.

;; Initial position
(send (send xl-interp 'input0) 'setValue
      #(30.7657852172852 7.46131801605225 19.1452522277832))
;; Initial orientation
(send (send rot-interp 'input0) 'setValue
      #(-0.12331560254097 0.413395673036575
			  0.0722599551081657 0.8992640376091))
;; Final position
(send (send xl-interp 'input1) 'setValue
      #(-28.8112678527832 -2.25108289718628 16.6992588043213))
;; Final orientation
(send (send rot-interp 'input1) 'setValue
      #(0.0514174103736877 -0.380638420581818
			   -0.0924163833260536 0.918656527996063))


;; OneShot engine for the animation.
(define one-shot (new-SoOneShot))
;; Three second animation
(send (send one-shot 'duration) 'setValue
      (new-SbTime 3.0))
;; Connect its ramp output to the interpolators' alphas
(send (send xl-interp 'alpha)
      'connectFrom (send one-shot 'ramp))
(send (send rot-interp 'alpha)
      'connectFrom (send one-shot 'ramp))
;; Tell it to hold its final output
(send (send one-shot 'flags)
      'setValue (+ SoOneShot::RETRIGGERABLE
		   SoOneShot::HOLD_FINAL))

;; Mouse button callback to start the animation
(define (mouse-press-cb user-data event-cb)
  (let ((event (send event-cb 'getEvent)))
    (if (= 1 (SO_MOUSE_PRESS_EVENT event BUTTON1))
	(begin
	  (send (send one-shot 'trigger) 'setValue)
	  (send event-cb 'setHandled)))))
(define event-cb (new-SoEventCallback))
(send root 'addChild event-cb)
(send event-cb 'addEventCallback
      (SoMouseButtonEvent::getClassTypeId)
      (get-scheme-event-callback-cb)
      (void-cast (callback-info mouse-press-cb)))
Arithmetic engines like SoCalculator are good for computing paths for objects to follow (see 13.6.Calculator) and for other simple arithmetic operations (for example, assisting in the computation of the ship's path in Combat). The SoBoolOperation engine can be combined with another special engine, SoGate, to enable and disable engine animations. The Inventor Mentor examples 13.4.Gate and 13.5.Boolean demonstrate this.

There are also some nodes which Inventor provides that contain engines and perform automatic animation:

  • SoRotor: spins about a constant axis while varying the angle (see the Inventor Mentor example 13.7.Rotor)
  • SoPendulum: transform node which oscillates between two rotations
  • SoShuttle: transform node which oscillates between two translations
  • SoBlinker: switch node which cycles among its children (see 13.8.Blinker)

While sensors allow more general functions to be performed than engines do (because sensors allow arbitrary program code to be executed in callbacks), engines have the advantage that they automatically update their values; this allows simple animation tasks to be delegated to engines while the rest of the application works on more complicated tasks. You can also create new engine types in C++, but this process is beyond the scope of this course. See the Linkages demonstration (in /mit/iap-cgw/StdInventorDemos/linkages for a more complicated use of engines.

Cameras

A camera is a special type of node which controls how the rendering of a scene graph appears in a window. The cameras in Inventor act in a similar fashion to real-world cameras; they have several parameters which can be adjusted to alter how the resulting picture appears. We will examine some of these parameters as well as the two types of cameras to understand how they can be used to our advantage in application design.

Types of cameras

There are two types of cameras available in Inventor: the perspective camera (SoPerspectiveCamera) and the orthographic camera (SoOrthographicCamera). When you view a scene in an examiner viewer, you can switch between the two types of cameras by clicking on the box-shaped icon near the bottom of the icon list.

A perspective camera views the scene in the same fashion as the human eye; that is, objects farther away from the camera appear smaller. This type of viewing style is known as a perspective projection. An orthographic camera has no perspective; it uses a parallel projection so that objects far away from the camera look the same size as those near the camera. Each type of camera has associated fields whose values can be changed to alter the parameters of the view. An orthographic camera has a field, height, which specifies the height of the view volume; this, combined with the aspect ratio of the camera, specifies how much of the scene fits within the camera view. A perspective camera has a heightAngle field which performs a similar function. Both types of cameras have several fields inherited from the parent class, SoCamera, such as the position, orientation, and aspectRatio of the camera.

Cameras and viewers

When you create an examiner viewer and set its scene graph, the first thing it does is check the scene graph to see whether it contains a camera. If it does, it uses that camera as the one which will render the scene; if not, it automatically adds one to the scene. All cameras only render objects which follow them in the scene graph; for this reason, the camera is usually the first node added to the scene graph. You can get the camera used by a viewer by using the getCamera method. This returns an SoCamera; you can use the isOfType method of the camera to check whether it is a perspective or orthographic camera:
(define viewer (new-SoXtExaminerViewer))

;;; ...Additional code for showing viewer,
;;;    and setting up scene graph...
;;; This is a simple example:
(define root (new-SoSeparator))
(-> root 'ref)
(define draw-style (new-SoDrawStyle))
(-> (-> draw-style 'style) 'setValue SoDrawStyle::LINES)
(-> root 'addChild draw-style)
(-> root 'addChild (new-SoCube))
(-> viewer 'setSceneGraph root)

;;; Extract the camera from the viewer.
(define camera (-> viewer 'getCamera))
(define other-camera '())
(if (= 1 (-> camera 'isOfType
	     (SoPerspectiveCamera::getClassTypeId)))
    (set! other-camera (SoPerspectiveCamera-cast camera))
    (set! other-camera (SoOrthographicCamera-cast camera)))
We can also set the camera type of the viewer manually. Note, as described in the manual page, that the change does not take effect until the next time the viewer's scene graph is set:
(-> viewer 'setCameraType
    (SoOrthographicCamera::getClassTypeId))
(-> viewer 'setSceneGraph root)
In the solutions from problem set 4, the "widget viewer" uses an orthographic camera to make the 3D slider widgets look two-dimensional.

Lights

Up until now, we have ignored the issue of how the scenes we view in the viewer are lit. Inventor provides several types of lights which are designed to emulate various types of real-world lights; however, when designing an interactive game, keep in mind that fewer and simpler lights in your scene will cause it to render faster.

Types of lights

There are three primary types of lights in Inventor; point lights (SoPointLight), spot lights (SoSpotLight), and directional lights (SoDirectionalLight). In all cases, the light itself is invisible; only its effects on other geometry in the scene are visible. A point light emits light in all directions from its center. A spot light has fields which control how quickly the intensity of the light falls off from the center (dropOffRate) and the angle from the ceter after which no light is visible (cutOffAngle). A directional light simulates a light source very far away and sends parallel light rays into the scene; all polygonal faces pointing in a given direction are illuminated uniformly over their surfaces.

Directional lights cause the fastest rendering of shapes, because they do not require shading over the surface of a polygon. Point lights are the next fastest, and spot lights are the slowest. You will probably find it best to use only directional lights, if needed, in your scenes.

Let's look at the difference between these three types of lights:

;;; Example of point, spot, and directional lights.

;;; Three viewers, one per light

(define v1 (new-SoXtExaminerViewer))
(-> v1 'show)
(-> v1 'setHeadlight 0)

(define v2 (new-SoXtExaminerViewer))
(-> v2 'show)
(-> v2 'setHeadlight 0)

(define v3 (new-SoXtExaminerViewer))
(-> v3 'show)
(-> v3 'setHeadlight 0)

;;; Three scene graph roots, one per type of light.

(define point-root (new-SoSeparator))
(-> point-root 'ref)
(define spot-root (new-SoSeparator))
(-> spot-root 'ref)
(define dir-root (new-SoSeparator))
(-> dir-root 'ref)

;;; Group node for holding point light
;;; and associated transform.

(define point-light-group (new-SoTransformSeparator))
(-> point-root 'addChild point-light-group)
(define light-xform (new-SoTransform))
(-> (-> light-xform 'translation) 'setValue 0.5 -0.5 -0.5)
(-> point-light-group 'addChild light-xform)
(define light (new-SoPointLight))
(-> point-light-group 'addChild light)
(-> (-> light 'intensity) 'setValue 1.0)
(define sphere (new-SoSphere))
(-> point-light-group 'addChild sphere)
(-> (-> sphere 'radius) 'setValue 0.1)

;;; Group node for holding spot light
;;; and associated transform.

(define spot-light-group (new-SoTransformSeparator))
(-> spot-root 'addChild spot-light-group)
(define light-xform (new-SoTransform))
(-> (-> light-xform 'rotation) 'setValue
    (new-SbVec3f -0.356368511915207
		 0.0632830709218979
		 -0.932199954986572) 1.46062397956848)
(-> (-> light-xform 'translation) 'setValue 1.6 0.0 2.0)
(-> spot-light-group 'addChild light-xform)
(define light (new-SoSpotLight))
(-> spot-light-group 'addChild light)
(-> (-> light 'intensity) 'setValue 1.0)
(-> (-> light 'dropOffRate) 'setValue 0.1)

;;; Group node for holding directional light
;;; and associated transform.

(define dir-light-group (new-SoTransformSeparator))
(-> dir-root 'addChild dir-light-group)
(define light-xform (new-SoTransform))
(-> (-> light-xform 'translation) 'setValue 0.5 -0.5 -0.5)
(-> dir-light-group 'addChild light-xform)
(define light (new-SoDirectionalLight))
(-> dir-light-group 'addChild light)
(-> (-> light 'intensity) 'setValue 1.0)
(-> (-> light 'direction) 'setValue -1 -1.3 -3.0)

;;; Group node for holding the three cubes.
;;; Shared among the three scene graphs.

(define cube-group (new-SoGroup))

;; Complexity node improves the shading of the cubes
;; at the expense of rendering speed.

(define complexity (new-SoComplexity))
(-> cube-group 'addChild complexity)
(-> (-> complexity 'value) 'setValue 0.7)

(define mat (new-SoMaterial))
(-> cube-group 'addChild mat)
(-> (-> mat 'diffuseColor) 'setValue 0.2 0.2 0.9)
(define xform0 (new-SoTransform))
(-> cube-group 'addChild xform0)
(-> (-> xform0 'rotation) 'setValue
    (new-SbVec3f 0.551838874816895
		 -0.806114614009857
		 -0.213665723800659) 0.859030544757843)
(define cube1 (new-SoCube))
(-> cube-group 'addChild cube1)
(-> (-> cube1 'width) 'setValue 0.25)
(define xform1 (new-SoTransform))
(-> cube-group 'addChild xform1)
(-> (-> xform1 'translation) 'setValue 1.125 0 -1.125)
(define cube2 (new-SoCube))
(-> cube-group 'addChild cube2)
(-> (-> cube2 'depth) 'setValue 0.25)
(define xform2 (new-SoTransform))
(-> cube-group 'addChild xform2)
(-> (-> xform2 'translation) 'setValue 0 -1.125 1.125)
(define cube3 (new-SoCube))
(-> cube-group 'addChild cube3)
(-> (-> cube3 'height) 'setValue 0.25)

(-> point-root 'addChild cube-group)
(-> spot-root 'addChild cube-group)
(-> dir-root 'addChild cube-group)

(-> v1 'setSceneGraph point-root)
(-> v2 'setSceneGraph spot-root)
(-> v3 'setSceneGraph dir-root)
Lights and viewers

The examiner viewer, in addition to adding a camera to the scene if none was found, will also by default add a headlight, which is a directional light pointing in the direction of the camera. (Note that up until now we haven't manually added lights to our scenes, but they have still been illuminated; this is because of the headlight.) The effects of adding additional lights to the scene can be diminished by the presence of the headlight, so you can turn it off using the setHeadlight method:
(-> viewer 'setHeadlight 0)  ;; turns off headlight
(-> viewer 'setHeadlight 1)  ;; turns on headlight
Next lecture

Open Inventor and C++

Back to the CGW '97 home page

$Id: index.html,v 1.7 1997/01/23 02:15:30 kbrussel Exp $