## 1/17/97

Note: this problem set is even more optional than usual. It is suggested that you try one or the other of these viewers, but not both. Instead, start planning and designing your final project.

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 either a floating point number or an integer; call this max. It returns a "random" float/int 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 max-normalized-diameter)
(randomize-loop (- i 1)))))
```
Note that "max-normalized-diameter" 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:

• minimum and maximum normalized radius: ideally we want to make a "thick shell" of stars, so that there appears to be some depth among them. Good values are about 0.0001 and 1.0. If you like, you can omit the minimum radius, or make a perfectly thin shell of stars. (See below for the algorithm which describes the usage of these parameters.)
• minimum and maximum radius: for our problem we will take these to be 30.0 and 50.0, respectively. If you decide to make a thin shell of stars then choose a value in this neighborhood.

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

• Choose x, y, and z coordinates randomly between -1.0 and 1.0. To do this you must call random with an argument of max-normalized-diameter (2.0) and subtract 1.0 from the result. Note that the argument to random must be a floating point number!
• Convert these coordinates to spherical coordinates. First compute r, the radius of the point from the origin (hint: to take the square root of a number, use the function sqrt. You should discard the point if r is less than the minimum normalized radius or greater than the maximum normalized radius; this creates a uniform distribution of stars in the shell. Next compute theta (horizontal angle) using the atan function with z and x as arguments, and phi (vertical angle) using the asin function with the ratio of y to r as argument.
• Scale up the radius to lie between the minimum and maximum radius (i.e. between 30.0 and 50.0).
• Convert from spherical coordinates back into cartesian coordinates and return a Scheme vector of the newly scaled x, y, and z.

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:

• Get the event out of the event callback node using the getEvent method. Get the normalized position of this event using SoEvent's getNormalizedPosition method. This method takes an SbViewportRegion as argument; use the getViewportRegion method of your ExaminerViewer to get this.
• This method returns an SbVec2f. Extract the x and y coordinates of the mouse using the operator-brackets method, and update the global variables designed for this purpose using set!.
• Calculate the updates to the x and y angles by multiplying the maximum angular velocity by the normalized y and x coordinates. Note that the y position of the mouse controls rotation of the camera about the x axis.
• Create two SbRotations which will represent the incremental rotations about the x and y axes, using the calculated angles.
• Create two matrices (of type SbMatrix), and use the operator= method to set one matrix equal to the rotation about the x axis, and one equal to the rotation about the y axis.
• Multiply the stars' current transformation matrix by the x and y matrices, in that order, using the operator* or operator*= methods. This composes in the incremental rotations with the overall transformation to come up with the new overall transformation matrix.
• Use the setMatrix method of SoTransform to update the transform from the transformation matrix. This is what causes the scene to move in the viewer.

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:

• x-angle and y-angle, which specify how the camera should be rotated
• forward-increment, which indicates how much faster or slower the camera should move upon a left or middle mouse button press, respectively
• forward-speed, the current forward velocity of the camera
• last-normalized-x, last-normalized-y, and last-event, as in the first problem

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:

• Extract the event from the event callback node, and extract the x and y normalized coordinates from the event, as before.
• Calculate the updates to the x and y angles as in the first problem, but this time update the global x-value and y-value variables with these increments.
• Create two SbRotations, one about the x axis and one about the y axis, and plug in the global and values as the angels, respectively. NOTE: you may have to invert the x and/or y angles to obtain the proper result; you should probably get the viewer working first and then change these parameters later.
• Create two SbMatrix objects, and use the setRotate method to set one equal to the x SbRotation, and one to the y SbRotation.
• Set the global rotate matrix equal to the product of the x and y matrices (in that order).
• Create a vector whose x and y components are zero, and whose z component is equal to the forward speed global variable. This is the vector in the camera's coordinate system along which the camera's position will move. Call this vector, for example, zvec.
• To convert this vector to the world coordinate system, obtain the inverse of the global rotation matrix (using the inverse method of SbMatrix) and use the multMatrixVec method with arguments zvec and zvec. This will change the value stored in zvec to be the correct translation.
• Create a final SbMatrix and use the setTranslate method with zvec as argument. This creates the matrix with which we will multiply the global translation matrix in order to obtain the camera's updated translation.
• Use the translation matrix's operator= method to set it equal to the product of the matrix from the last part with itself, in that order.
• Finally, set the global transform's matrix (using the setMatrix method) to the product of the global rotation matrix and the translation matrix, again in that order.
• As cleanup, you should set the global last event flag to #t (to indicate that a mouse event was received), and update the global last normalized x and last normalized y variables.

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/iap-cgw/src/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 above scene "room.iv":

• forward increment: -1
• initial forward speed: -3
• maximum angular velocity: (pi/200)