Computer Graphics Workshop '96 Problem Set 5

1/19/96

Problems
Problem 1 - Make a stellar viewer

In space, your movement compared to the immense interstellar distances is so tiny that stars appear to be fixed points. If we wanted to simulate flight through space, we could do so by simply creating a shell of stars, placing the camera at the origin, and rotating about the origin in response to the movement of the mouse in the window.

In this problem we will create this type of "stellar viewer" where the only control is rotation about the origin.

The first thing we need to do is make some stars. We will do this by using the random number generator that SCM includes in its Scheme Library. To begin using it, put the following line at the top of your file for this problem:

(require 'random)
This file defines the random function, which takes as argument an integer; call this max. It returns a "random" integer between 0 and max. However, it always starts up in the same initial state: we need to randomize the random function somehow. Here is one function that, by calling random multiple times, serves to randomize its state.
(define (randomize-loop i)
  (if (> (modulo i 173) 0)
      (begin
	(display "Randomizing...\n")
	(random star-resolution)
	(randomize-loop (- i 1)))))
Note that "star-resolution" is explained below. To call this function we would do the following:
(randomize-loop (get-universal-time))
This takes the current time in seconds since January 1, 1970 and uses that as argument to the randomization loop. This is a sufficiently random number for games and computer graphics.

The first thing we are going to do is make a scene graph with some stars in it. We will do this in several steps.

First, write the function generate-star. This function may take several arguments, or it may use global variables:

The algorithm for making a thick shell of stars is as follows:

The function generate-stars repeatedly calls generate-star, and returns a list of Scheme vectors that can be used to set coordinates in a Coordinate3 node.

Next we need to generate colors for the stars. One way of doing this is assuming that all stars will be along the range from deep blue to white, and choosing a random number for each star's red and green color values (they should be the same for this color scheme). Write the function generate-star-color which returns a Scheme vector of red, green, and blue values, and the function generate-star-colors which returns a list of these vectors.

Now we are ready to build the scene graph for the stars. Write the function build-star-subgraph which returns a scene graph with the following structure:

                                    root
                                 (Separator)
                                      |
        ---------------------------------------------------------
        |                |            |             |           |
       bind            style        coords         mat      point-set
(SoMaterialBinding)(SoDrawStyle)(SoCoordinate3)(SoMaterial)(SoPointSet)
When you create this root, ref it; just before you return it, unref it using the unrefNoDelete method. This returns this node to its initial reference count without deleting it, and is useful for keeping everything clean in your program.

The value field of the bind node should be set to SoMaterialBinding::PER_VERTEX, to allow each star to have its own color. The pointSize field of the style node should be set to something larger than 1, to allow the stars to be more visible on the screen. You should use the set-mfield-values! function to set the coordinates and colors in the Coordinate3 and Material nodes, respectively. A good number for the number of stars is probably 1000; many more than that and the interpreter will be calculating for a very long time. Don't forget that the number of specified colors and coordinates must be the same. Finally, set the numPoints field in the point-set node to be equal to the number of stars you generated.

Finally, call build-star-subgraph, ref the node it returns, and view it in a viewer. You should see what looks like a ball of stars, but if you zoom into the inside you will see that it is hollow.

Now we are ready to create the callbacks which will move the stars in the direction indicated by the mouse position.

In order to implement the star viewer, we will insert a Transform node in front of the ball of stars. This transform node will be a global variable, and should be defined at the top of your problem set file. We will continually update the matrix which this transform node contains; this matrix defines how the ball of stars will be rotated. In order to do this we also need a global variable of type SbMatrix; call this variable "transform-matrix". Create this at the top of your file as well, and also set it to be the identity matrix, which means that it will have no effect initially:

(-> transform-matrix 'makeIdentity)
You should also make two global variables, last-normalized-x-position and last-normalized-y-position; we will use them later to allow continuous updates to the viewer. The global variable last-event should be initialized to #f and set to #t when the first Location2Event is received; this prevents the camera from moving until the mouse moves into the window for the first time. The final global variable should be called max-angular-velocity, and is machine-dependent; depending on the frame rate of the viewer, this variable can be increased or decreased. A good starting value is M_PI / 200.0.

Define the procedure mouse-move-cb. This function will be a callback for Location2Events. The arguments are defined on the SoEventCallback manual page:

typedef void SoEventCallbackCB(void *userData, SoEventCallback*node)

Therefore the first argument (which we will not use) will be the user data, and the second will be a pointer to the EventCallback node which received this event.

We now need to figure out how to update the stars' transformation in response to the movements of the mouse. We will do so incrementally in this problem; each time we will figure out the angular velocity of the camera about the x and y axes and update the camera's rotation to reflect that. There will be no translation of the camera in this problem.

The algorithm for updating the global transformation is as follows:

The overall scene graph will look like this:

                          root
                     (SoSeparator)
                           |
      -------------------------------------------
      |              |                          |
  transform    event-callback               star-root
(SoTransform)(SoEventCallback)(Separator from build-star-subgraph)
Set up an event callback for SoLocation2Events (see the class notes for the keyboard event example). The callback's name is "mouse-move-cb". Once this is done, you should be able to click on the arrow icon in the ExaminerViewer (or do this programmatically by evaluating "(-> my-viewer 'setViewing 0)") and move the mouse around to cause the stars to move.

The first problem you will probably notice is that the stars still appear as a ball in the center of the screen; we want to view them from the inside of the shell. Get the camera out of the examiner viewer using the getCamera method and set its position field to (0, 0, 0) after you set the examiner viewer's scene graph.

Currently, when you stop moving the mouse, the stars stop moving as well. This happens because Location2Events are only generated when the mouse moves to a new position. To remedy this, set up an idle callback (using SoIdleSensor) which does the same thing as the mouse movement callback, except it only uses the last normalized x and y positions rather than updating them. It should do nothing until the last-event flag has been set to true, to prevent the camera from moving until the mouse moves into the window for the first time. Once you have done this, you should get continuous motion of the stars.

Problem 2 - Make a fly viewer

The last problem only involved rotations about the origin, not translations; now we will create a different kind of viewer, a "fly viewer", which allows you to fly through scenes. Inventor provides a viewer of this type in SoXtFlyViewer; now we will see how to implement it.

There will be two changes from the structure of the star viewer. The first is that we will use an SoCameraKit instead of a transform to move our scene around; in this problem we will move the camera itself. The second is that instead of composing in incremental rotations to a global rotation matrix, we will keep track of the total x and y rotation angles. This changes the model of movement to prevent rotations about the z axis.

Our scene graph will look like this:

                      root
                 (SoSeparator)
                       |
       -----------------------------------
       |               |                 |
    cam-kit      event-callback     rest-of-scene
 (SoCameraKit)  (SoEventCallback)  (SoSeparator?)
Here is the inialization code for the camera kit. The creation of the above scene graph should be done in the global environment (i.e. not in a procedure) to allow the name cam-kit to be used.
(define cam (SO_GET_PART cam-kit "camera" SoCamera))
(-> (-> cam 'nearDistance) 'setValue 1.0)
(-> (-> cam 'farDistance) 'setValue 10000.0)
(define transform (SO_GET_PART cam-kit "transform" SoTransform))
The camera kit is a node kit, which is an object that manages its own internal scene graph. By using the SO_GET_PART macro we can get access to the children in this scene graph. (We are using a node kit because of a bug in the Inventor camera, and also because by having access to this transform node we can move the camera around the scene in a similar way to that in problem 1.)

In this problem, because we will be translating the camera as well as rotating it, we will have two global matrices; the translation matrix ("translate-matrix") and the rotation matrix ("rotate-matrix"). The translation matrix specifies the location of the camera in world coordinates; the rotation matrix specifies the rotation of the camera about its origin (that is, in which direction the camera is looking). Initialize both of these matrices by calling the makeIdentity method.

You should also create the following global variables:

This time we will use a MouseButtonEvent callback to allow the camera's speed to be changed, as well as the Location2Event callback for orientation of the camera and the IdleSensor to continuously update the view.

The mouse button callback should be straightforward to implement; you can use the SO_MOUSE_PRESS_EVENT macro to detect which button has been pressed. For example:

(if (= 1 (SO_MOUSE_PRESS_EVENT event BUTTON1)) (... more code ...))
The Location2Event and idle sensor callbacks are more tricky; however, as before, they share similar code. The function of the Location2Event callback is as follows:

The idle sensor callback again essentially duplicates the location event callback's code. The only difference is that it uses the global last normalized x and last normalized y variables instead of updating them.

Once you have implemented the above callbacks, you should be able to load a scene graph (example: "/mit/thingworld/Ivy/room.iv") as the "rest of the scene graph" and fly around using the viewer you have created. One thing to watch out for is that because the camera's negative z axis points in front of the camera, a negative forward speed will actually cause the camera to move forward.

As a hint, here are some good initial values for viewing the scene "room.iv" in /mit/thingworld/Ivy/room.iv:


Back to the CGW '96 home page

$Id: index.html,v 1.4 1996/01/19 18:48:09 kbrussel Exp $