3D Dynamic Viewing 101

Here we will discuss the basics of how to handle dynamic rotation, panning, and zooming in our framework. Events that trigger these actions are caught by the window system interface (GLFWController, in our case) and then passed up to our generic Controller. Since these events usually need to affect the entire scene, the Controller will massage the data a bit and then pass it off to the ModelView class as opposed to any (or all) specific instance(s).

For reasons we shall discuss in class, rotation and pan requests are accumulated into a 4x4 matrix; zoom requests will be implemented by modifying the field of view.

As you study the material on this page, you should refer to the relevant class and instance variable declarations and method implementations in the Controller and ModelView classes in mvcutil/. Note especially the class variables holding viewing and modeling data in class ModelView and the following Controller methods:

Dynamic View Manipulations Must be Implemented in Eye Coordinates

When clicking and dragging to perform a rotation or a pan, the model on the screen must "follow" the mouse. This means the operation must be performed in eye coordinates. Similarly (although implemented in a somewhat different way), dynamic zooming must also be done so that the results are intuitive and consistent with the current view.

Dynamic Rotation

The user interaction that will trigger a dynamic scene rotation will be a left-click and drag operation. Examine the Controller event handling related to mouse events. You will see a "Left click down" event with no modifier key (e.g., SHIFT key) depressed will let the Controller know that subsequent mouse movements are to be interpreted as rotations. With each incremental movement of the mouse (i.e., pixel by pixel), Controller::handleMouseMotion is called. What follows here is a description of what happens pixel-by-pixel as the mouse is moved with the left button depressed.

It will be critical for the rotation to "follow" the mouse. Thus, for example, if I click and drag to the right, I want to rotate about the screen y-axis; if I click and drag down, I want to rotate about the screen x-axis. This is a frequent source of errors in implementation; note, for example, that if I see a mouse Δx > 0, I want to rotate about the y-axis by an angle proportional to Δx:

double rotY = factor * Δx; // x mouse motion ⇒ rotation about y (and vice versa)

A rotation request will be implemented by creating a 4x4 matrix that encodes the rotation and then concatenating that new matrix onto a matrix that has the accumulation of all prior requests. The order of matrix concatenations is critical. We will use Mdynamic_view on this page to denote the accumulated set of requested transformations. (It will accumulate both rotations and panning operations.) There is a class variable ModelView::dynamic_view that stores this matrix. (See the extracts from the ModelView and Controller class definitions below.)

There are class methods in the cryph utilities that can be used to create the 4x4 matrices needed to perform rotations of various sorts and specified in various ways. (See the "Static Public Member Functions" section of the Matrix4x4 class at the link above.) If we wish to concatenate a matrix corresponding to a rotation about the y-axis of rotY degrees, we do the following (note the order of multiplication):

cryph::Matrix4x4 newRotY = cryph::Matrix4x4::yRotationDegrees(rotY);
ModelView::dynamic_view = newRotY * ModelView::dynamic_view;

This is only half the story since there are (at least) two ways to interpret such a dynamic rotation request, depending on whether we are using Viewing Strategy #1 or Viewing Strategy #2:

  1. Viewing Strategy #1 (eye completely outside of the scene):
    Conceptually you are turning the scene around in front of you. So instead of needing a matrix that rotates about the EC y-axis, we need to rotate about the axis that is parallel to the y-axis, but which passes through the center of attention. So we will first slide the center point to the origin, then do the rotation, then slide it back out:
    cryph::Matrix4x4 newRotY = cryph::Matrix4x4::yRotationDegrees(rotY);
    double d = eye.distanceTo(center);
    cryph::Matrix4x4 preT = cryph::translation(0, 0, d); // WHY?
    cryph::Matrix4x4 postT = cryph::translation(0, 0, -d); // WHY?
    cryph::Matrix4x4 the_newRotY_MatrixIReallyWant = postT * newRotY * preT;
  2. Viewing Strategy #2 (eye inside the scene):
    Conceptually you are inside of the scene and turning your head around to see what is around and behind you. Computing newRotY as first shown above suffices for this case.

The newRotY matrix computations and related processing are done in response to handling mouse events. ModelView::addToGlobalRotationDegrees is used to handle the events; ModelView::getMatrices is called by your render methods to retrieve the final dynamic view matrix. This separation of responsibilities (event callback versus display update) is especially relevant when using Viewing Strategy #1. Many rotations are accumulated as the mouse moves around, but we must actually do the pre- and post-translation only on the final accumulated rotation.

Of course there are other "viewing strategies" one might envision that would affect this processing as well. Nevertheless, let's just show representative implementation excerpts from these two methods that would occur for our two viewing strategies:

  1. Viewing Strategy #1:
    // PART 1 in ModelView::addToGlobalRotationDegrees
    cryph::Matrix4x4 newRotY = cryph::Matrix4x4::yRotationDegrees(rotY);
    dynamic_view = newRotY * dynamic_view; // Note the order!

    Then:

    // PART 2 in ModelView::getMatrices
    double d = eye.distanceTo(center);
    cryph::Matrix4x4 preT = cryph::translation(0, 0, d); // WHY?
    cryph::Matrix4x4 postT = cryph::translation(0, 0, -d); // WHY?
    cryph::Matrix4x4 M_ECu = cryph::Matrix4x4::lookAt(eye, center, up);
    mc_ec = postT * dynamic_view * preT * M_ECu;
  2. Viewing Strategy #2:
    // PART 1 in ModelView::addToGlobalRotationDegrees
    cryph::Matrix4x4 newRotY = cryph::Matrix4x4::yRotationDegrees(rotY);
    dynamic_view = newRotY * dynamic_view; // Note the order!

    Then:

    // PART 2 in ModelView::getMatrices
    cryph::Matrix4x4 M_ECu = cryph::Matrix4x4::lookAt(eye, center, up);
    mc_ec = dynamic_view * M_ECu;

The ModelView class does not know which viewing strategy you used when specifying eye, center, and up, so it must be told explicitly what to do when processing dynamic rotations. You can use the ModelView class method setDynamicRotationsAboutCenter to specify this. By default (hence if you do not use this method), ModelView will assume you are using Viewing Strategy #1, and it will apply the pre- and post-translation. If you are using Viewing Strategy #2, call ModelView::setDynamicRotationsAboutCenter(false); (typically once from the set3DViewingInformation function in main.c++).

Dynamic Panning

The user interaction that will trigger a scene pan operation will be a Shift-left-click and drag. Examine the Controller event handling related to mouse events. You will see a "Left click down" event with the SHIFT key depressed will let the Controller know that subsequent mouse movements are to be interpreted as panning the scene. With each incremental movement of the mouse (i.e., pixel by pixel), Controller::handleMouseMotion is called. Each incremental motion will result in a call to ModelView::addToGlobalPan. That routine will update the dynamic view matrix in a manner analogous to what we saw with rotations. It will then be used in getMatrices, again, just as we saw with rotations.

The provided implementation of ModelView::addToGlobalPan will be adequate for our purposes. It does the following. Be sure you understand all of this as there may be homework and/or exam questions related to this.

  1. Convert the incoming translation vector from LDS to EC.
  2. In case this is a 2D application, it updates the 2D EC pan vector.
  3. In case this is a 3D application, it creates the dynamic view matrix in a manner analogous to what we saw with rotations.

View Zooming Using Scroll Wheel

The scroll wheel will be used to zoom in and out. Specifically, scroll wheel up causes the view to zoom in (i.e., objects in view get larger); scroll wheel down causes the view to zoom out (i.e., objects in view get smaller). A scroll wheel event is detected by the GLFW or GLUT Controller subclass which then passes it to the Controller::handleScroll method shown above. That method simply calls ModelView::scaleGlobalZoom to scale a global zoom factor that is used by ModelView::compute2DScaleTrtans for 2D applications or ModelView::getMatrices to alter the xy dimensions of the view frustum for 3D applications.

As with the rest of the material on this page, be sure you completely understand this process.

Final Note

If you want to augment or change mouse handling behavior (or any other event handling behavior), you can just override other GLFWController or Controller methods in your Controller subclass.