In this project, the goal is to build a hierarchical 3D model with controls for interacting with it. The project skeleton contains a framework that hides the user interface code. Your model will be specified in a separate source file that will be compiled and linked in with the skeleton code.
Start by getting a fresh copy of the skeleton code. Extract the files, and run modeler.exe. Two windows will pop up, the controls window and the model window.
Let's play around with the application a bit. On the left side of the controls window you'll see a list of the available controls: "X Position", "Y Position", "Z Position", and "Height". Select one of these by clicking on it (or more than one by holding down the shift key or control key while you click). For each selected control, a slider will be displayed on the right side of the controls window. Play with the model a bit by scrubbing the controls back and forth.
Now that you've gotten a feel of the controls interface, let's move our attention to the model window. By clicking and dragging on this window, you can actually manipulate the camera position. The camera controls are as follows. (The descriptions below are admittedly pretty crappy; it's probably better if you just played around with the camera a bit).
Now that we've played around with the sample application, let's get our hands on the code.
Alright, let's open up the modeler.dsw workspace. You'll notice that there are quite a few files in the project. For the purposes of building a model, you will be writing a new file (like the sample.cpp) and linking it in with the existing code. The following are just high level descriptions; we'll be going into more details later.
The modelerapp.* and modelerui.* files handle most of the user interface stuff (sliders, windows, etc). In particular, modelerapp.h defines the ModelerApplication class, which is how you'll be getting and setting control values (such as "X Position" in the sample application).
The modelerdraw.* files provide support for drawing primitive shapes like spheres, boxes, cylinders, and triangles. Furthermore, they provide functions for changing material attributes.
The modelerview.h file defines the ModelerView object, which will be the base class of your model. The ModelerView handles the OpenGL drawing and event handling. modelerview.cpp provides some base functionality, such as setting up lighting and handling the camera controls. If you look at sample.cpp, you'll notice that BoxModel is simply a subclass of ModelerView.
The camera.* files are used by modelerview.* to provide the camera functionality. You would modify this file if you wanted to implement your own version of gluLookAt.
All the rest of the files are pretty much files that you can ignore. If you're doing some sophisticated math, however, you may find vec.h and mat.h useful. You'll also notice that the model is distributed with modelerui.fl; this is the file that generates the user interface. Unless you have a great reason, don't touch this. It'll probably end up being a lot more trouble than it's worth.
Let's learn the process of constructing a model by doing a careful walkthrough of sample.cpp. Open up this file, and go to main.
int main()
{
// Initialize the controls
// Constructor is ModelerControl(name, minimumvalue, maximumvalue,
// stepsize, defaultvalue)
ModelerControl controls[NUMCONTROLS];
controls[XPOS] = ModelerControl("X Position", -5, 5, 0.1f, 0);
controls[YPOS] = ModelerControl("Y Position", 0, 5, 0.1f, 0);
controls[ZPOS] = ModelerControl("Z Position", -5, 5, 0.1f, 0);
controls[HEIGHT] = ModelerControl("Height", 1, 5, 0.1f, 1);
ModelerApplication::Instance()->Init(&createBoxModel, controls, NUMCONTROLS);
return ModelerApplication::Instance()->Run();
}
Each ModelerControl object contains the information necessary to make a control. To create the user interface, we need to first make an array of controls.
ModelerControl controls[NUMCONTROLS];
NUMCONTROLS is actually defined at the top of sample.cpp within an enumeration; this makes it easier to change the number of controls without having to explicitly mess with #defines. We initialize each control as follows.
controls[XPOS] = ModelerControl("X Position", -5, 5, 0.1f, 0);
In the above code snippet, XPOS is defined at in an enumeration at the top of sample.cpp. The constructor for the ModelerControl above basically says the following.
Create a ModelerControl named "X Position" with minumum value -5, maximum value 5, step size 0.1, and default value 0.
We fill the controls array with these values. Afterwards, we initialize the instance of the ModelerApplication with this array. When you call the Init method, ModelerApplication in turn takes this array and creates the appropriate sliders. Then we can simply Run the application.
ModelerApplication::Instance()->Init(&createBoxModel, controls, NUMCONTROLS);
return ModelerApplication::Instance()->Run();
What's this ModelerApplication::Instance() junk, you ask? The short answer is, "don't ask." For you software engineering junkies out there, the slightly longer answer is "since we want to guarantee during the execution of the application that there is only one instance of ModelerApplication, it is implemented using a Singleton design pattern, and thus a ModelerApplication can not be constructed explictly; instead, a static instance is constructed upon the first request for the instance and that same instance is returned on subsequent requests." Got that?
Alright, so what's this &createBoxModel junk, you ask? Well, the ModelerApplication needs to know how to create the sample model, and createBoxModel is simply a function that does just that. We'll get to this part in a bit.
It's time to look at the construction of the actual model.
// To make a BoxModel, we inherit off of ModelerView
class BoxModel : public ModelerView
{
public:
BoxModel(int x, int y, int w, int h, char *label)
: ModelerView(x,y,w,h,label) {}
virtual void draw();
};
As you can see, BoxModel inherits from ModelerView. It overrides the draw method. There are several important parts of the draw method to look at.
void BoxModel::draw() ...
ModelerView::draw(); ...
glTranslated(VAL(XPOS), VAL(YPOS), VAL(ZPOS)); ...
drawBox(1,1,1); ...
ModelerView::draw sets up some default lighting parameters and sets up the projection matrix and camera position. Unless you're playing around with viewpoint controls, you're better off just keeping this call at the top of the draw method of your model.
Notice the call to glTranslated actually uses as the values of VAL(XPOS), VAL(YPOS), and VAL(ZPOS). These are the values of the controls (set by the slider values). VAL(x) is actually just an abbreviated version of (ModelerApplication::Instance()->GetControlValue(x)). All that the GetControlValue method does is return the current value of a control. Note that you can also call the SetControlValue method to set the value of a control (see modelerapp.h for function prototype). You may need to do this if, say, you want a control that controls the values of other controls.
Finally, notice the call to drawBox. If you look at modelerdraw.h, you'll notice that it provides functions that let you set the color and draw primitive shapes, like boxes. You could do this with OpenGL as well; in fact, some OpenGL libraries like GLUT have some pretty ridiculous primitive drawing function, like glutSolidTeapot. There are several reasons why you should stick with the primitives provided in modelerdraw.h.
There are, of course, reasons not to use the primitives in modelerdraw.h.
Finally, let's look at createBoxModel.
ModelerView* createBoxModel(int x, int y, int w, int h, char *label)
{
return new BoxModel(x,y,w,h,label);
}
Don't ask, just cut and paste. If you want to know the gory details, read through the code.
So how do you make your model? Simple. Suppose you're making a model of a dog. Make a new file called dog.cpp or something like that, and basically do everything that sample.cpp did for the BoxModel (including writing main). Add dog.cpp to the project, and remove robotarm.cpp from the project. If you don't, the compiler won't like that you have two main functions. Compile, run, and turn in. Okay, so it's not quite that simple, but hopefully this careful walkthrough of sample.cpp will make the construction of your model as painless as possible so that you can focus on putting together something really cool instead of messing around with link errors.
OpenGL maintains a transformation matrix that is applied to everything it draws. Any transformation call (glRotated, glTranslated, glScaled, etc.) you make is under the hood implemented as a matrix multiplication with the current transformation matrix. Thus, transformations are cumulative. This is very nice for hierarchical modeling; if we just drew the head, we need only apply additional transformations relative to the head to draw the eyes, rather than calculating from scratch where they should be drawn.
However this behavior is not always desirable. Once we drew the eyes, we would want to start back from the head to draw the ears, or we might want to start all the way back at the body to draw the legs. This is where glPushMatrix and glPopMatrix come in. glPushMatrix saves the current transformation matrix on the stack. glPopMatrix replaces the current transformation matrix with the one popped off the top of the stack. So, after we draw the head, we would push to save our transformation thus far. Then, we would apply the necessary transformations, and draw the eyes. In order to draw the ears, we would first pop to get back to the head, then push again so we can return to the head again later, then apply transformations and draw for the ears. After all the various parts of the head were drawn, we could pop again and get back to the body, which we would have pushed at some point before drawing the head. Now we could move on the the legs, etc.
There are certain files that you simply should not touch. All the source code is distributed, partly for educational purposes (it's nice if you can see everything that's going on, especially if something goes wrong and you've got to debug). Keep in mind that you will probably want to use your model for the animator assignment. When we distribute the animator assignment, it will have a modified version of ModelerApp that displays the controls as interpolated curves instead of slider bars. You will be linking in the animator code with your code, and thus changing modelerapp.* will have annoying to disastrous effects. For similar reasons, it's a terrible idea to mess with modelerview.* and modelerui.*. Modelerdraw.cpp may be modified for the raytracer project, so that's not the best place to add primitives. If you are going to add a new primitive, work from the examples in modelerdraw.cpp, but it would be best to either include it in your model's cpp file, or to add a completely seperate file for it/them. You are free to mess with camera.cpp and sample.cpp as well as any files you add to the project. You can mess with camera.h, but keep in mind that modelerview.* depends on camera.*, so be careful.
If you want to do a simple animation, the simplest way is to update your model every time draw is called. Stick to something relatively simple, maybe a wagging tail or a hokey walk. Don't try and go all out with swing dancing goats; trust us, the animator application is much better suited for this task. Anyway, if you update your model any time draw is called, you can enable animation in the controls window and it will call draw repeatedly.
The sample application we've been using just uses a call to the ModelerView::draw method to set up the projection, lighting, and so on. If you're not satisfied with the default lighting, don't mess with modelerview.cpp; instead, just remove the call to ModelerView::draw from your draw method and take care of all the nasty stuff yourself. Take a look at modelerview.cpp to see how it's done.
If you really want to tinker with user interface interaction (perhaps you want your model to respond to keystrokes?), then you'll have to override the handle method as well. If you go this far, you're on your own, this is deep into the nasty user interface toolkit code. For those of you who implemented custom convolution filter interfaces for Impressionist, you know how nasty FLTK can get.
This quarter, Modeler is built on a fresh code base. Therefore, the models from previous quarters won't work. We have however ported a few of our favorites over to the new framework; they are available here for your viewing pleasure.