|
GL Survival Kit for CS559
Michael L. Gleicher
This document tells you some of the basic things you need to know to get
started with OpenGL under Windows. This is not meant to replace the GL book!
The example code for the code snippets, along with project files to compile them
is VS2003, are available under this tree.
There are other useful OpenGL/FlTk examples as well. The
example code page from two years ago has these same examples, with a
little more explanation. The only thing missing are the project and solution
files for VS2003.
Preface 1: GL vs. OpenGL
OpenGL is a graphics toolkit that provides routines for drawing. It runs on a
wide variety of machines. It is the successor to GL (as in plain GL) which was
a graphics toolkit that ran on Silicon Graphics IRIS workstations. I sometimes
use the 2 names interchangeably.
OpenGL is a generalization of GL. To make it more general, it had to
become more complicated.
OpenGL provides routines for drawing graphics within a window. It provides
little help for making the window, providing a user interface, ... For better
or worse, it is designed in a way that leaves the programmer flexibility in how
they organize their programs. How you decide what and when to draw is your
problem.
Preface 2: GLUT vs. FlTk
For doing graphics, it would be nice if you could concentrate on just doing the
relevant drawing, and not worry about user interfaces or the details of a
specific machine. Unfortunately, this is never the case. If nothing else, you
need to put a window up on the screen to draw into. Basically, this means that
your program has to talk to the operating system and windowing system somehow.
For a graphics class, this gives us three choices:
-
We can just not write graphics programs and do it all with pencil and paper.
-
We can force you to learn the specifics of the operating system and windowing
system (for this class, it would be Microsoft Windows).
-
We can rely on a toolkit to provide an easier layer of abstractions.
Since approach 1 wouldn't be any fun, and approach 2 is quite challenging under
windows (and would cause us to spend all of our time learning windows rather
than graphics), we (like most graphics classes) choose #3.
For a graphics class, the goal of the UI toolkit is to help you get a window up
on the screen as easily as possible so you can focus on learning about what to
do inside of the window. A secondary goal is to provide you with enough easy to
use user interface tools to build interactive programs.
GLUT (usually pronounced with an "uh" sound like gluttony) is a simple toolkit
designed to help people build simple OpenGL applications. It provides an
operating system with an independent way to put up a window, respond to
user interface events like mouse clicks, ... It provides very limited support
for user interfaces (or maybe better said, it only supports very limited user
interfaces). GLUT is a remarkably easy way to get a window up on the screen so
that you can draw into it. In the GL Book, they use GLUT because their
goal is to make small programs that focus on drawing (what OpenGL does). In
many graphics courses, they use GLUT because it probably is the easiest way to
get a picture on the screen.
For CS559, we have been recommending and supporting
FlTk, a user interface toolkit. FlTk supports many more features
than GLUT - it is a complete user interface toolkit that allows a programmer to
build complete programs. It has support for windowing, widgets (like buttons
and sliders, even a miniature html browser for making help systems), has a good
model for how to handle events, ...
If your goal is just to get a picture up on the screen with GL (as the graphics
assignments will focus on), GLUT is fine, and FlTk is probably overkill.
However, we still encourage you to use FlTk because:
-
You may want to provide more of a user interface.
-
It isn't too much harder to use FlTk
-
You can use GLUT inside of FlTk.
For the class assignments, you are not required to use FlTk. You can use GLUT if
you prefer. If you have GLUT questions, I can't help you too much because I
haven't used it much. (And the TA has never used it).
Getting Started
The first thing that you'll need to do is set up your project to include the GL
library. Under visual studio, this is done by using the project settings dialog
box.
Note: we're assuming that you already know how to create a new project, and to
use FlTk with it.
To add the gl libraries to your project
-
Open the "Properties" dialog for the project. (either from the menu or by
right-clicking on the project in the workspace browser)
-
Select "all configurations" - this is important since you want the libraries
for both debugging and non-debugging versions of your program. Unlike FlTk,
there are not seperate versions of the library.
-
Go to Linker -> Input -> Additional Dependencies
-
Add the 3 libraries: opengl32.lib glu32.lib glaux.lib
The OpenGL headers are in the standard include paths, so you don't need to
change anything to use them. Remember, however, they must be referenced in
their subdirectory. For example, to use the main gl header file you must add:
#include <GL/gl.h>
to your program. Just including <gl.h> probably won't work. Also,
before reading in gl.h, you need to read in the header file "windows.h", so to
be more precise, your programs should have:
#include <windows.h>
#include <GL/gl.h>
If you use precompiled headers, you may want to put the windows and gl headers
into your precompiled header file as they seem to take a long time to process.
If you don't know what a precompiled header is, now isn't the time to
learn. The idea of a precompiled header is that you process your header files
once, so you don't need to reprocess them each time you compile each source
file. They are a little bit tricky to set up under Visual Studio, and probably
won't save you that much time.
Once you have your project set up, you're ready to start writing FlTk/OpenGL
programs.
General Strategy: when do I get to draw?
The basic idea for using GL and FlTk is that you will define a subclass of the
FlTk Fl_Gl_Window class that knows how to draw what you want. All of your
drawing happens inside of the draw method for this class.
Here is a very simple example example (link
to code):
// simple OpenGL / FlTk program
// written 10/16/99, Michael L Gleicher
#include <FL/Fl_Gl_Window.h>
#include <Fl/Fl.h>
#include <windows.h>
#include <GL/gl.h>
// make a Gl window that draws something:
class MyGlWindow : public Fl_Gl_Window {
public:
MyGlWindow(int x, int y, int w, int h) :
Fl_Gl_Window(x,y,w,h,"My GL Window")
{
}
private:
void draw() { // the draw method must be private
glClearColor(0,0,0,0); // clear the window to black
glClear(GL_COLOR_BUFFER_BIT); // clear the window
glColor3f(1,1,0); // draw in yellow
glRectf(-.5,-.5,.5,.5); // draw a filled rectangle
};
};
// the main routine makes the window, and then runs an event loop
// until the window is closed
main()
{
MyGlWindow* gl = new MyGlWindow(100,100,500,500);
gl->show(); // this actually opens the window
Fl::run();
delete gl;
return 1;
}
Thius program draws a large yellow rectangle in the middle of a black window.
Notice that I did not have to do anything to prepare the window for
drawing, manage it being moved, ... I also didn't have to tell GL when
I was done drawing (FlTk takes care of that).
Also, since in FlTk a window is just like any other widget, they can be placed
inside of one another. (well, I don't think you can place any other widgets
inside of a GL window). So we can do... (link
to code)
// simple OpenGL / FlTk program
// program simple2.cpp
// written 10/16/99, Michael L Gleicher
#include <FL/Fl_Gl_Window.h>
#include <Fl/Fl.h>
#include <windows.h>
#include <GL/gl.h>
#include <Fl/Fl_Double_Window.h>
#include <Fl/Fl_Button.h>
// make a Gl window that draws something:
class MyGlWindow : public Fl_Gl_Window {
public:
float r,g,b;
MyGlWindow(int x, int y, int w, int h) :
Fl_Gl_Window(x,y,w,h,"My GL Window")
{
r = g = 1;
b = 0;
};
private:
void draw() { // the draw method must be private
glClearColor(0,0,0,0); // clear the window to black
glClear(GL_COLOR_BUFFER_BIT); // clear the window
glColor3f(r,g,b); // draw in yellow
glRectf(-.5,-.5,.5,.5); // draw a filled rectangle
};
};
// a simple callback to do something
void changeColor(Fl_Widget* /*button*/, MyGlWindow* myWind)
{
myWind->r = 1 - myWind->r;
myWind->g = 1 - myWind->g;
myWind->b = 1 - myWind->b;
myWind->damage(1);
}
// the main routine makes the window, and then runs an even loop
// until the window is closed
main()
{
Fl_Double_Window* wind =
new Fl_Double_Window(100,100,400,300,"GL Sample");
wind->begin(); // put widgets inside of the window
MyGlWindow* gl = new MyGlWindow(10,10,280,280);
Fl_Button* bt = new Fl_Button(300,10,70,25,"Color");
bt->callback((Fl_Callback*) changeColor, gl);
wind->end();
wind->show(); // this actually opens the window
Fl::run();
delete wind;
return 1;
}
What you should notice about this program is that it places the Gl window inside
of a "Double" window that will take care of doing the double buffering for us.
It allowed us to place widgets around the Gl window (something we couldn't do
with GLUT).
A few other things to notice:
-
When we create the GlWindow inside of another window, we specify its position
inside of the parent window.
-
We don't need to delete the subwindow - in FlTk, the parent takes care of
deleting all of its children.
-
All of the drawing happens in our subclasses draw function.
-
All of the things you've learned to do in an FlTk widget (like define a
"handle" function) can be done with the Fl_Gl_Window.
This last point is very important. If you put drawing commands in other places,
you don't know if they will be drawn in the right window! OpenGL always draws
into the "current" window, but you don't necessarily know where this is! You
may have several windows!
Where do I draw?
Now that you know when to draw, you can think about where. In the example,
I arbitrarily chose the coordiantes for the corner of the rectangle.
(well, it wasn't arbitrary).
As you will learn, the coordinate system is one of the elements of state that GL
keeps (just like it keeps a notion of the current color). In this situation, I
just happened to know that the current coordinate system was going to be having
the X and Y axes have values ranging from -1 to 1. You can't rely on this, so
when you create a draw function, the first thing you should do is to define a
coordinate system. Otherwise, you have to rely on luck that the last thing to
draw gave you a nice coordinate system.
A more correct version of the draw method above would be:
void draw() { // the draw method must be private
glMatrixMode(GL_PROJECTION); // set up coord system
glLoadIdentity(); // reset it
glOrtho(-1,1,-1,1,-1,1); // set it to be -1 to 1 in each axis
glMatrixMode(GL_MODELVIEW); // back to normal mode
glLoadIdentity(); // clear what anyone else did
glClearColor(0,0,0,0); // clear the window to black
glClear(GL_COLOR_BUFFER_BIT); // clear the window
glColor3f(r,g,b); // draw in yellow
glRectf(-.5,-.5,.5,.5); // draw a filled rectangle
};
Notice that I first go into "Projection" mode which means I am defining the
coordinate system (GL allows me to keep the transformations that define the
coordinate system and the transformations that define the objects seperate).
glLoadIdentity replaces the top of the transform stack with the identity
matrix, so we have a known value there. glOrtho defines one kind of coordinate
system (generally its what we want for doing things in 2d).
If you knew that the coordinate system was set correctly, then you wouldn't have
to reset it. FlTK has various ways to help with this, however, I always tend to
be conservative and load a fresh coordinate system everytime I try to
draw.
You should also be warned that this clears out whatever transformation may have
been on the stack. So, if you try to do something that puts something on the
stack first, you may be in for a suprise.
Now you can go off and draw some stuff!
Picking
As part of a user interface, you often want to be able to pick things. OpenGL
helps you do this by having a special drawing mode called "pick" mode. In this
mode, objects aren't actually drawn, but rather, their names are put onto a
list if they are drawn underneath the position of the cursor.
The steps to do picking are:
-
Setup for picking: this includes loading a special coordinate system that is
centered around the cursor, and setting up the special pick mode.
-
Draw the objects (except that they really aren't drawn - they're just tested to
see if their pixels are inside the pick region).
-
See what was hit.
Here's a sample program that we can look at:
(link to code)
// simple OpenGL / FlTk program
// program simple3.cpp - do picking
// written 10/16/99, Michael L Gleicher
#include <FL/Fl_Gl_Window.h>
#include <Fl/Fl.h>
#include <windows.h>
#include <GL/gl.h>
#include <GL/glu.h> // for pick matrix
#include <Fl/Fl_Double_Window.h>
#include <Fl/Fl_Button.h>
#include <stdio.h>
// make a Gl window that draws something:
class MyGlWindow : public Fl_Gl_Window {
public:
int selected;
MyGlWindow(int x, int y, int w, int h) :
Fl_Gl_Window(x,y,w,h,"My GL Window")
{
selected = 0;
}
private:
// for clarity, we break the draw routine into 3 pieces
void draw() { // the draw method must be private
drawClear();
drawSetupTransform();
drawObjects();
};
// clear the screen and the projection transformation
void drawClear() {
glMatrixMode(GL_PROJECTION); // set up coord system
glLoadIdentity(); // reset it
glClearColor(0,0,0,0); // clear the window to black
glClear(GL_COLOR_BUFFER_BIT); // clear the window
}
// notice that this doesn't reset the projection!
// it must be that way so that picking can use it
void drawSetupTransform() {
glOrtho(-1,1,-1,1,-1,1); // set it to be -1 to 1 in each axis
};
// draw 4 rectangles
// notice that they each have "names" for picking
// if the rectangle is selected, draw it in a different color
void drawObjects() {
// we don't assume that we're in the right matrix mode
glMatrixMode(GL_MODELVIEW); // back to normal mode
glLoadIdentity(); // clear what anyone else did
// rectangle 1
glLoadName(1);
if (selected == 1) glColor3f(1,0,0);
else glColor3f(1,1,0);
glRectd(-.7,-.7,-.3,-.3); // draw a filled rectangle
// rectangle 2
glLoadName(2);
if (selected == 2) glColor3f(1,0,0);
else glColor3f(1,1,0);
glRectd(.7,-.7,.3,-.3); // draw a filled rectangle
// rectangle 3
glLoadName(3);
if (selected == 3) glColor3f(1,0,0);
else glColor3f(1,1,0);
glRectd(-.7,.7,-.3,.3); // draw a filled rectangle
// rectangle(4)
glLoadName(4);
if (selected == 4) glColor3f(1,0,0);
else glColor3f(1,1,0);
glRectd(.7,.7,.3,.3); // draw a filled rectangle
};
//
// handle events - basically, ignore everything except a mouse
// push event. use that to select a rectangle
// because this is a window, we must respond to the show event
// otherwise we'll never get shown if we're a subwindow
int handle(int e) {
if (e==FL_PUSH) {
int mx = Fl::event_x();
int my = Fl::event_y();
// do picking
make_current(); // so we're drawing in our window
// set up a special coordinate system centered at the mouse
// remember, FlTk and Gl have opposite Y directions
glMatrixMode(GL_PROJECTION); // set up special coordinate sys
glLoadIdentity();
// we need the size of the OpenGL window, in its own terms
int viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
gluPickMatrix((double)mx,(double)(viewport[3]-my),5,5,viewport);
drawSetupTransform(); // so object appear in right place
// set up for picking - make a place to put the results
GLuint buf[100];
glSelectBuffer(100,buf);
glRenderMode(GL_SELECT);
glInitNames();
glPushName(0);
// now draw (but nothing is really drawn);
drawObjects();
// go back to drawing mode, and see how picking did
int hits = glRenderMode(GL_RENDER);
printf("Hit %d objects\n!");
selected = 0;
// all we care about is the first hit - see the book for
// info on how to get the other info in the hit record
if (hits) {
printf(" the first hit had %d names [%d was first]\n",
buf[0],buf[3]);
selected = buf[3];
}
damage(1);
return 1;
} if (e==FL_SHOW) {
show();
return 1;
}
return 0;
};
};
// the main routine makes the window, and then runs an even loop
// until the window is closed
main()
{
Fl_Double_Window* wind =
new Fl_Double_Window(100,100,300,300,"GL Picking Sample");
wind->begin(); // put widgets inside of the window
MyGlWindow* gl = new MyGlWindow(10,10,280,280);
wind->end();
wind->show(); // this actually opens the window
Fl::run();
delete wind;
return 1;
}
Now for some commentary:
-
Notice how I split the drawing function up into 3 different parts. This
makes it easier when we only want to use part of the drawing function (for
example, when we do picking).
-
When I draw, each object is given a name for picking.
-
Also, you can see that I keep some information around about a "selected"
object. This is often a handy thing to do.
-
Notice that I intercept the FL_SHOW event and handle it. This turns out to
be important for windows. If you don't do this, they don't show up sometimes.
-
The guts of this program is the code that does the picking. Normally, this
would have been made a seperate method called from the handle method, but since
this program does so little...
-
Notice that before I do any drawing in a routine other than "draw" (e.g. in
handle), I need to do a make_current. This is an FlTk method of the window
class that tells GL that we want the drawing operations to happen inside of our
own window, not someone else's.
-
Each "hit" during picking puts a bunch of information into the pick buffer. In
this case, I knew there would only be one object picked, it was easy.
Animation
To make animation happen, you need to use an "idle callback" so that FlTk gives
your application a chance to do work when the computer is just sitting there. I
have encapsulated this functionality into a convenient "time slider" widget
called a "RunButton", the files are RunButton.H,
RunButton.cpp and there's a
sample program that uses it in simple4.cpp.
|