OpenGL Programming/GLStart/Tut2
Tutorial 2: Setting up OpenGL with Windows
editUsing the code from the first lesson (Win32 primer), we are going to set up the Windows program to work with OpenGL. First off, start up the Dev – C++ project that we worked on in the first lesson.
Linking the OpenGL libraries
editWhen you open up your Windows project, go to the top menu bar and click on Project and then Project Options. In the Project Options window, click on the Parameters tab. Under the third window (the “Linker” window) click on the Add Library or Object button. From there navigate to where you installed Dev – C++ (most likely “C:/Dev-Cpp”). When you get there, open up the “lib” folder. In there click once on the libglu32.a file and then hold down the Control key and click on the libopengl32.a file to select them both. Then click the Open button on the bottom of the dialog. Then click the OK button:
Now, going back to your source code, right below the including of the windows header, put these include headers in:
#include <gl/gl.h> #include <gl/glu.h>
These will include the appropriate OpenGL and GLU headers.
Contexts
editNow we need to create two variables called Contexts. A context is a structure that performs certain processes. The two contexts that we are going to deal with are the Device Context and the Rendering Context. The Device Context is a Windows specific context used for basic drawing such as lines, shapes, etc… The Device Context is only capable of drawing 2- dimensional objects. The Rendering Context, on the other hand, is an OpenGL specific context used to draw objects in 3D space. The Device Context is declared with HDC and the Rendering Context is declared with HGLRC. Declare the two context variables like this right below the including of the headers:
HDC hDC; //device context HGLRC hglrc; //rendering context
We are now going to initialize both of these contexts so we can in the end draw something OpenGL related.
In the code, go to the message procedure (WinProc). Right now all we have is one message (WM_DESTROY). What we want is to create the contexts when the program is first opened. The windows message for that is WM_CREATE which is processed when the window first opens up:
case WM_CREATE:
Under that message we have to retrieve the current device context. To do that we set the regular Device Context (hDC) equal to the function GetDC() which takes as one parameter the window handle which we declared at the WinProc declaration (hWnd). This function returns the current Device Context:
hDC = GetDC(hWnd);
For now we will leave the message like this. We will get back to this message later. What we need to do know is set up what is called the Pixel Format of the program.
Pixel Format
editThe Pixel Format is how, when drawing something, the pixels appear on the window. The structure that holds the pixel data is called the PIXELFORMATDESCRIPTOR:
typedef struct tagPIXELFORMATDESCRIPTOR { // pfd WORD nSize; WORD nVersion; DWORD dwFlags; BYTE iPixelType; BYTE cColorBits; BYTE cRedBits; BYTE cRedShift; BYTE cGreenBits; BYTE cGreenShift; BYTE cBlueBits; BYTE cBlueShift; BYTE cAlphaBits; BYTE cAlphaShift; BYTE cAccumBits; BYTE cAccumRedBits; BYTE cAccumGreenBits; BYTE cAccumBlueBits; BYTE cAccumAlphaBits; BYTE cDepthBits; BYTE cStencilBits; BYTE cAuxBuffers; BYTE iLayerType; BYTE bReserved; DWORD dwLayerMask; DWORD dwVisibleMask; DWORD dwDamageMask; } PIXELFORMATDESCRIPTOR;
There are a lot of fields here. The good thing is we only have to fill in a few fields in order to get this structure working. Lets start setting up the pixel format in a new function.
At the top of the code, add in this function call:
void SetupPixels(HDC hDC) {
The reason for taking as parameter a device context is because when we set the pixel format to be working with the window, we need to pass as a parameter to one of the functions the device context of the window.
Now, within the function we just created we declare a variable of type Integer called “pixelFormat”. This variable will hold an index that references the pixel format we are going to create. After that declare a variable of type PIXELFORMATDESCRIPTOR called “pfd” to hold the actual pixel format data:
int pixelFormat; PIXELFORMATDESCRIPTOR pfd;
Now lets start filling in a few of the fields of the pixel format.
The first field we fill in is the nSize field which is set to the size of the structure itself:
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
The next field we will fill in is the flags field called dwFlags. These set certain properties of the pixel format. We will set this to three flags. The first, PFD_SUPPORT_OPENGL, lets the pixel format to be able to draw in OpenGL. The next one, PFD_DRAW_TO_WINDOW, tells the pixel format to draw everything onto the window we provide. The final one, PFD_DOUBLEBUFFER, allows us to create smooth animation by providing two buffers to draw on, which get switched to make animation smooth:
pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER;
The next field we will fill in is the version field, nVersion, which is always set to 1:
pfd.nVersion = 1;
The next field will be the pixel type, iPixelType, which sets the type of colors we want to support. For this we will use PFD_TYPE_RGBA so we get a red, green, blue, and alpha color set (don’t worry about the alpha part yet. We will go over that when the need comes):
pfd.iPixelType = PFD_TYPE_RGBA;
The next field, cColorBits, specifies the number of color bits to use. We will set this to 32:
pfd.cColorBits = 32;
The final field we set, cDepthBits, sets the depth buffer bits. For now set it to 24:
pfd.cDepthBits = 24;
After we set the fields of the pixel format, we need to set the device context to match the newly created pixel format. For this we use the ChoosePixelFormat() function, which takes as the first parameter the device context and as the second parameter the address of the PIXELFORMATDESCRIPTOR structure we created earlier (pfd). We set the integer variable we declared at the beginning of the function (pixelFormat) equal to the return value of this function:
pixelFormat = ChoosePixelFormat(hDC, &pfd);
Now we set the pixel format to the device context with the SetPixelFormat() function that takes as three parameters the device context, the pixel format integer, and the address of the PIXELFORMATDESCRIPTOR structure. Also, we will check to make sure this function worked. This particular function returns a Boolean value depending on whether it was successful or not. We will check if it was. If it was not successful, then we alert the user with a message box and close the program down:
if(!SetPixelFormat(hDC, pixelFormat, &pfd)) { MessageBox(NULL,"Error setting up Pixel Format","ERROR",MB_OK); PostQuitMessage(0); }
Now we end the function since we are done setting up pixels:
}
Now we go back to the WM_CREATE message in the window procedure, and right under the obtaining of the device context (hDC = GetDC(hWnd)) we call the SetupPixels() function and pass as the parameter the device context:
SetupPixels(hDC);
Now remember that rendering context we declared earlier (hglrc). Well, we will now create it using the wglCreateContext() function which takes as parameter the regular device context:
hglrc = wglCreateContext(hDC);
Now we will make the rendering context the current one we will use throughout the program. For that we use the wglMakeCurrent() function which takes as a first parameter the device context and the second parameter the rendering context:
wglMakeCurrent(hDC, hglrc);
Now we are finished with the WM_CREATE message. Make sure to include the break statement at the end of the message:
break;
Remember that WM_DESTROY message we created in the first lesson which is responsible for the exiting of the program? We need to release the rendering context there so there are no memory leaks. So go back to the WM_DESTROY message and we will add to the code there.
First, before we actually delete the rendering context, we have to make sure it is no longer active. For that we use the wglMakeCurrent() function again. This function, if you remember, takes as parameters the device context and the rendering context. For this we pass the device context, but for the rendering context we put in NULL to indicate we don’t want the rendering context to be current:
wglMakeCurrent(hDC,NULL);
Now we are safe to finally release the rendering context. For this we use the wglDeleteContext() function which takes as a single parameter the rendering context to delete:
wglDeleteContext(hglrc);
Make sure after these two function calls you still have the PostQuitMessage() and the break statement as usual. Here is the whole WM_DESTROY message defined:
case WM_DESTROY: wglMakeCurrent(hDC,NULL); wglDeleteContext(hglrc); PostQuitMessage(0); break;
Window Resizing
editResizing happens when someone expands the width and/or height of the window. If we don’t control this, OpenGL will get confused and start drawing things out of whack. So first we will create a function called Resize() that will handle the resizing of the window. This function will take as two parameters the width and height of the window, which I will discuss how we receive those parameters:
void Resize(int width, int height) {
The first thing we have to do in this function is called setting the viewport. The viewport is the portion of the window we want to see the OpenGL drawing going on. The function to set the viewport is called glViewport():
void glViewport( GLint x, GLint y, GLsizei width, GLsizei height );
The first and second parameter, x and y, are the coordinates of the lower – left corner of the viewport. Since we want to see the drawing on the whole window, we set both of these to 0 indicating the lower –left corner of the window. The third and fourth parameter, width and height, is the width and height in pixels of the viewport. Since we want it to cover the whole window, we set it to the width and height parameters that were passed to the Resize() function. Make sure to also cast the third and fourth parameters to the GLsizei data type just to be safe. So here is the glViewport() function with the parameters filled in:
glViewport(0,0,(GLsizei)width,(GLsizei)height);
Now that we got the viewport set up, we need to set up what is called the projection. Projection is basically how the user views everything. There are two types of projection: Orthographic and Perspective. Orthographic is an unrealistic view. To better explain it, when an object is drawn in an orthographic 3D scene, the objects that are placed farther away from another object look like they are the same size, even with the distance taken into account. Perspective, on the other hand, is more realistic such as objects farther away appear smaller than objects closer to the viewer. Now that you got a better idea of projections, lets create one in code. We will use for this lesson the Perspective projection.
To start editing the projection, we need to select the Projection matrix. To do that we use the glMatrixMode() function which takes as a single parameter the matrix we want to edit. To edit the projection matrix, we give the function the value GL_PROJECTION:
glMatrixMode(GL_PROJECTION);
Before we start editing the projection matrix, we need to make sure that the current matrix is the Identity matrix. To do that we call the glLoadIdentity() function which takes no parameters and simply loads the identity matrix as the current matrix:
glLoadIdentity();
To set the perspective projection, we use the gluPerspective() function:
void gluPerspective( GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar );
The first parameter, fovy, is the field of view angle in the y direction in degrees. You can set this to 45 to get a normal angle of view. The second parameter, aspect, is the field of view in the x direction. This is usually set by the ratio of width to height. The third and fourth parameters, zNear and zFar, is depth distance that the viewer can see. We set zNear to 1.0 and zFar to 1000.0 to give the user a view of a lot of depth.
Here are the functions with the aspect constraint options and all the main parameters filled in:
gluPerspective(recalculatefovy(),(GLfloat)width/(GLfloat)height,1.0f,1000.0f); float recalculatefovy() { return std::atan(std::tan(45.0f * 3.14159265358979f / 360.0f) / aspectaxis()) * 360.0f / 3.14159265358979f; } float aspectaxis() { GLFloat outputzoom = 1.0f; GLFloat aspectorigin = 16.0f / 9.0f; GLInt aspectconstraint = 1; /* Sets the aspect axis constraint to maintain the FOV direction when resizing; the first constraint is conditional and maintains horizontal space if below a specific ratio and the other extends vertical space. Default is 0. */ switch (aspectconstraint) { case 1: if (((GLfloat)width / (GLfloat)height) < aspectorigin) { outputzoom *= ((GLfloat)width / (GLfloat)height / aspectorigin) } else { outputzoom *= (aspectorigin / aspectorigin) } break; case 2: outputzoom *= ((GLfloat)width / (GLfloat)height / aspectorigin) break; default: outputzoom *= (aspectorigin / aspectorigin) } return outputzoom; }
Now we have to switch the matrix mode to the model view matrix. Add another call to the glMatrixMode() function, but this time with the parameter of GL_MODELVIEW. The model view matrix holds the object information which we will draw. I will get into more detail about it in later lessons:
glMatrixMode(GL_MODELVIEW);
Now we need to reset the model view matrix by calling the glLoadIdentity() function. After that call, we are finished with the Resize() function:
glLoadIdentity(); }
Now we have to put this Resize() function call in the Window Procedure. The message we will put it in is called WM_SIZE, which gets called whenever the window is resized by the user:
case WM_SIZE:
Now we need some way to keep track of the current window width and height. First off, declare two integer variables called “w” for width and “h” for height right before the switch structure for the messages:
int w,h; switch(msg)
Going back to the WM_SIZE message, we need to set the variables we just created to the current width and height. For this we use the lParam parameter that was passed to the Window Procedure function. To get the width of the window, you use the LOWORD() macro function and put in as a single parameter the lParam variable. It will return the current width of the window. To get the current height of the window, use the HIWORD() macro function, which will return the current height of the window. Finally, pass the two integer variables (w,h) to the Resize() function we created and that will be the end of the WM_SIZE message:
case WM_SIZE: w = LOWORD(lParam); h = HIWORD(lParam); Resize(w,h); break;
Drawing something with OpenGL
editNow that we got OpenGL set up with our program, lets test it out to make sure that it is set up right.
First create a new function called Render(). This function will be responsible for all the OpenGL drawing done in this program:
void Render() {
First thing we do in this function is called buffer clearing. I will discuss buffers in a later lesson, but make sure before you render anything on the screen that you clear the buffers you are using. For this we use the glClear() function which takes as parameters the buffers we want to clear. We will put in GL_COLOR_BUFFER_BIT for the color buffer and GL_DEPTH_BUFFER_BIT for the depth buffer:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Now we have to load the identity matrix using the glLoadIdentity() function so we can start fresh at the origin:
glLoadIdentity();
Right now up to this point, the view we have is centered about the origin and the whole window is 1 unit wide and 1 unit high. A unit is a type of measurement that OpenGL uses. This is basically a user – defined measurement system. The default window width and height is now 1 and 1 units. To gain more units on the window, we need to move along the z axis in the negative direction, meaning moving away from the viewer. All we want to do is move 4 units in the –z direction so the window is 4 units wide and 4 units high. For this we use the glTranslatef() function:
void glTranslatef( GLfloat x, GLfloat y, GLfloat z );
The parameters (x,y,z) specify what axis to move along. Since we want to move along the z axis negative 4 units, we leave the first two parameters as 0.0 and put in –4.0f for the z parameter:
glTranslatef(0.0f,0.0f,-4.0f);
Now we are finally going to draw something on the screen. First off, when drawing something on screen and you don’t specify a color for the object to draw, then OpenGL automatically makes the color of the object white. To fix this, before you draw any objects, use the glColor3f() function which takes as three parameters the red, green, and blue color values. One thing you have to know is that the color values you can put in go from 0.0 to 1.0, not the usual RGB values which go from 0 to 255. For now we will set the color of the object we are going to draw to blue by setting the red and green values to 0.0 and the blue value to 1.0f:
glColor3f(0.0f,0.0f,1.0f);
Now we will finally get to the part where we actually draw something. How OpenGL drawing works is that first you have to specify what type of object you are going to draw. After that you specify the vertices of the object.
To start drawing objects, we need to use the glBegin() function which takes as one parameter the type of object we are going to draw. The glBegin() function tells OpenGL that the statements after this function call are going to be drawing specific. The parameter we will put for this function for now is GL_POLYGON which tells OpenGL that we are going to draw a polygon:
glBegin(GL_POLYGON);
Now we need to specify the vertices that we want connected to form the polygon. For this example, all we are going to draw is a square so all we need is to specify 4 vertices. To draw a vertex we use the glVertex3f() function which takes as three parameters the x, y, and z location of the vertex. The OpenGL window initially was 1 unit wide and 1 unit high. Earlier in the Render() function we used the glTranslatef() function to move 4 units away. So that means that the viewing window is now 4 units wide by 4 units high. The origin starts at the complete center of the window and acts like a standard coordinate system. We will draw the first vertex close to the upper – right corner with the coordinate of (1.0f,1.0f,0.0f), meaning we place the vertex one unit to the right and one unit up:
glVertex3f(1.0f,1.0f,0.0f);
Now we will set the other four corners of the square just like we did with the first vertex:
glVertex3f(-1.0f,1.0f,0.0f); glVertex3f(-1.0f,-1.0f,0.0f); glVertex3f(1.0f,-1.0f,0.0f);
Now to end the drawing, we use the glEnd() function to tell OpenGL we are done with drawing. That also finishes our Render() function:
glEnd(); }
Controlling the Render loop
editNow we have to go back to the WinMain() function and put the Render() function somewhere that it will be called in a loop. First off, under the UpdateWindow() function call in WinMain(), we need to make sure we have the current device context using the GetDC() function we used before:
hDC = GetDC(hWnd);
One other thing we do before we get to the render loop is clear the screen to a certain color. For this we use the glClearColor() function which takes as 4 parameters the red, green, blue and alpha color values. Set these values to all 0 and put the function right under the previous GetDC() function call:
glClearColor(0.0f,0.0f,0.0f,0.0f);
Now we are going to put the Render() function in the WHILE loop we have at the end of WinMain(). Right under the code while(1), put in the Render() function:
while(1) { Render();
The last thing we have to do before we compile this program is swap the buffers. Since we set the pixel format to be double – buffered we use the SwapBuffers() function which takes as a single parameter a device context. Put this function call right after the Render() function call:
SwapBuffers(hDC);
Now we are done writing the setup of OpenGL with Windows. Compile and run the program to get this output: