CS 428 - Fall 2011
Project 1: 3D Viewing

Due electronically:
Wednesday, October 5, 12 noon


Check this page frequently for updates and clarifications
(Significant changes or clarifications are marked with [Update])


Description

It's often tricky to place a camera in a Computer Graphics environment, since you often can't see what you're doing, except by looking "through the lens". If the viewed objects are tiny, you can often spend a lot of time searching through empty space.

One solution to this (found in modeling and rendering software) is to provide a second view, which shows the camera in the scene. This is alongside a camera view of the objects. Above, is a side-by-side view of a camera's view volume, with the resulting view on the right.

Another solution is to simply provide the ability to point the camera at a particular (selected) object. But even with this, it can still be hard to get the view you want without any visual feedback of where the camera is, relative to the viewed object (other than the "through the lens" view).

Objective

This project will help you understand the geometry of 3D viewing, and the transformations needed to implement it. You will also learn how to do viewing and transformations in OpenGL, use the matrix stack, as well as how to use the OpenGL clipping planes.

Program

You will be provided with skeleton code for this program, which supplies the necessary user interface and main program structure. You must develop the code for the actual viewing, transformations and OpenGL. You will basically be filling in the missing parts in functions (which are marked with "// ...") and replacing parts that are labeled as temporary code. In its current state, the program doesn't do much. Note: you should remove any temporary code you encounter, once you start that relevant part.

The skeleton code for this project can be found (on the iLab machines) in the directory
      ~decarlo/428/proj1
Also included is a Makefile and a README.txt file describing the code structure and how to compile and run the program. Note that this program uses the vecmath library (unlike homework 1), so if you didn't set this up already, then you'll need to do this now.

Handing in

Hand in the following:

  1. all of your java files (no class files please)
  2. your makefile
  3. a description file descrip.txt (see below)
The description file should be a very brief description of what you changed and added. Put your name at the top of this file. It should briefly describe all modifications to the skeleton code you performed, as well as describe the additional code you wrote for the assignment. If you implemented any additional/optional features or anything else special, you must state what you did in this file (to get credit).

You will need to create a tar archive to hand in:

   tar cf handin.tar Makefile *.java descrip.txt
Here are the instructions on how to hand in this file (same as in homework 1).

This assignment is a bit vague so that you can make a lot of the design decisions yourself. There are several approaches to the problems here. Use your best judgement to try and get the most useful result. Ask for help or clarification when you need it.

Viewing

There are two cameras in our setup; the world camera, and the view camera. Each has its own camera transformation. However, the world camera can "see" the view camera's view-volume. Both cameras have a corresonding 3D viewing window into the world. Each of these windows maintains a separate OpenGL matrix stack, viewport, projection, etc... Let's refer to the left window in the above image as the world view, and the right window in the above image as the camera view.

The viewed object (a cube) will also be drawn with respect to its own object coordinate system using a modeling transformation. A method for drawing the cube is in the class Cube.

To summarize, there are two cameras -- each with its own projection. And there are three coordinate systems -- one for each of the cameras (one for the world view, one for the camera view), and one for the viewed object (the cube). The transformations for the cameras are called viewing transformations. These three transformations are stored using the classes World, Camera and Obj. Additional information about the drawing state is also stored here (such as whether the camera is orthographic or perspective). Each of these static classes have accessor methods to retrieve the current transformation parameters. For instance, World.tx() is the x-axis component of the translation.

In the classes WorldView and CameraView, there are methods called draw(), projection() and transformation(). All of the code you will write will be either in these methods, or called directly from them. For viewing:

You'll be using the variables width and height defined in the parent class (SimpleGLCanvas) that holds the current size of the drawing area. (Watch out for bugs caused by integer division here, as width and height are both integers.)

In these functions, you will update the projection matrix (not the model view matrix); you'll use glMatrixMode to ensure this. You must use either glFrustum or glOrtho here. No other method of setting the projection (such as gluPerspective) will be accepted. Also, be sure to preserve the aspect ratios to eliminate viewport distortion when the window is resized (when you get this working, the cube in the window should appear square when viewed head on, no matter how you resize the window). Basically, the aspect ratio of the view frustrum must equal the aspect ratio of the window. There is some temporary code in CameraView.projection() that does this now, so it will be easier for you to get the program working.

The values and ranges of the scrollbars in the user interface are chosen to best work when the front of your view volume is about 2 units wide (from -1 to 1, for example), such as in the first assignment, where the call was like:
    gl.glOrtho(-1.0, 1.0, -1.0, 1.0, near, far).
Of course, you'll have to adjust for the aspect ratio.

Keep the range close to [-1,1] as the initial camera position, as well as the slider ranges were chosen to accomodate this range. If you use a larger range (such as [-width,width] which can be in the hundreds, as width is in pixels), then your cube will appear as a tiny speck.

Here are two example views of the world and camera windows (in each, world is on the left, camera on the right):
      Perspective view

      Orthographic view

The other two functions are:

And of course, both of the above views will require the object coordinate transformation as well, to draw the cube. You apply this by calling Cube.transform(), and draw the cube by calling Cube.draw(). The only operations you should be using inside transform are translates, rotates or scales.

Also take a look at the class SimpleGLCanvas. You'll notice that the current window width and height are stored (for use in projection()). Also, the screen is cleared for you, and the current OpenGL error status is checked (if you perform any invalid OpenGL operations, a message will be displayed if you ran the program with the -debug switch).

Transformations

There are many ways of specifying the position of a camera. For this assignment, the camera will be positioned using the "position", but the orientation of the camera is described using three Euler angles: rotation amounts around the X, Y and Z axes.

The rotation around the X-axis (called pitch) is applied first, followed by the rotation around the Y-axis (called yaw), followed by the rotation around the Z-axis (called roll). This is then followed by a translation of the camera to its position.

Specifically, this means that the resulting matrix for the view camera is:

Here, the V superscripts signify that these are parameters of the view camera (i.e. they are not exponents).

This transformation (with different parameters, with W superscripts) is the same the world camera transformation W.

This is the definition of V. It seems backwards: it describes the transformation from the canonical camera to the world. Don't start adding in spurious minus signs in this formula to "fix" it because you think it seems backwards: it's supposed to be this way!

There are several benefits to this order. With the translation happening last, it is more intuitive to the user, since it is not affected by the rotation. Once you get this working, briefly try putting the translation first, and see what it's like (try it when the roll is 180 degrees). For the same reason, it is worth having the roll after the pitch and yaw, so that they remain more axis-aligned. However, the pitch does affect the yaw -- this sort of thing can't be completely avoided with ordered rotations.

These rotations are in sequence -- the X rotation will move the Y and Z axes of the object; and Y rotation will also move the Z axis. You should be rotating around the transformed axes (which are still the Y and Z axes in the transformed frame). This means that once you perform any X rotation at all, your Y and Z rotation will no longer be rotating around the Y and Z axes of the object, but those of the containing coordinate system. The solution here is like what we described in class, and ends up being about the simplest thing you can do. (By the way -- when you get this working, you can see the effect of "Gimbal lock" by setting your Y rotation to 90 degrees, and notice how both the X and Z rotations become redundant for this value! Try it for 80 degrees, too. Turns out this isn't just a problem for CG; look here if you're curious.)

For the object transformation M, the order of application is basically the same, with scaling applied first:

View volume

In the class ViewVolume, draw the view volume (for the world camera) using lines to show its boundaries. In the examples here, the front (near) plane of the view volume is drawn with blue lines, with a thick blue line on top. This helps determine the orientation of the volume. Make sure you draw your view volume in a way that reveals which is the front and top (which can be hard to tell in a perspective view, at times).

You need to write a function to draw a view volume (as a bunch of lines) using OpenGL. Then, when you call this function, you need to have the appropriate transformation on the matrix stack. Look in the textbook, or at the manpages for glOrtho and glFrustum for a description of where the view volume actually is, for these calls. You will need certain values that are recorded in Camera from CameraView.projection().

Clipping

As you know, the primitives drawn in the camera view window are clipped: geometry outside of the view volume is discarded. The sides of the view volume represent the sides of the window, but there is also the front and back of the view volume as well (near and far planes) -- that's six planes in total.

In order to illustrate whether an object is inside the view volume, you will be drawing clipped geometry in the world window, as in the following:
      Cube entirely inside view volume: no clipping
      Cube partially clipped
      Cube entirely outside: completely clipped
Note that in the above pictures, clipped objects are drawn in wireframe. This is an extra feature (described below). Without this extension, nothing should be drawn outside the clipping region. And only the part of the object that is inside the view volume is drawn. Notice how what you see in the camera view matches up exactly with what you see being clipped in the world view. If it doesn't, it's likely the transformation you're using to place the view volume isn't consistent with how everything else is drawn.

Basically, you must set up six clipping planes: one for each of the view volume sides. See the review of OpenGL clipping for how to do this in OpenGL.

Drawing options

The user interfaces allows for the specification of whether the world window has its geometry clipped or not (the value of which is determined using the World.isClipping() method). Be sure only to clip the geometry in the world window when this is true.

Also in the user interface, the user can choose between perspective and orthographic cameras. You can determine the current choice using the Camera.isPerspective() method. When the user makes this choice, CameraView.projection() is called so you can update the camera viewing projection, and WorldView.draw() is called so you can draw the updated view volume. It's up to you to use the correct projection for the camera window, and also that the view volume has the right shape.

Vector math

In computing the location of the clipping planes (used in drawing the view volume), you might find it necessary to do some basic vector operations (subtraction, cross product, etc...). The javax.vecmath package has been installed, and its documentation is available on the course web page. It's part of the Java3D API.

For this assignment, you'll probably use the Vector3d and Point3d classes.

Don't write code that implements vector operations (add, subtract, cross product, ...); use these classes. Note that certain operations (like cross product) only are defined for vectors, not for points. If you can't figure out how to use this stuff, ask! We'll be using it on the remaining assignments.

This package is already installed on iLab and properly configured in the course setup script. If you followed the instructions on the lab info page for setting things up, then you have already installed vecmath.jar (and added it to your classpath).

Bug

The world and camera view windows each have their own redraw callback function. In order for the program to work properly, the camera view callback should be called first, since the window coordinates there must be conveyed to the Camera class by Camera.setCurrentView. Otherwise, the view volume cannot be drawn properly. However, the order windows are redrawn has changed with the JDK version, and also seems to depend on the platform. Unfortunately, we haven't set aside the time to figure out how to address this properly. (If you happen to know, drop us an email!)

If they are drawn incorrectly on your system, then the easily observed behavior is that the view volume will be drawn using the previous camera window size (which is not set yet when the program starts up, and changes whenever the window is resized). Most obvious is that it view volume will be missing on the first redraw, and out-of-date whenever the window is resized. But after that (such as when adjusting the sliders), everything should look fine.

If you observe this behavior, please flip the value of View.camera_window_added_first and see if that fixes the problem. (The code is set up so the bug does not occur in the iLab, at least as far as we have observed.)

Extensions

The following is a list of optional extensions (required if you are a CS graduate student).

Hints


428 Home