OpenGL Tutorial #1
-- "Getting started on the way to..." --
Introduction
Welcome! This new tutorial series is all about coding under OpenGL. If you don't know the needs or uses for OpenGL, read my "Introduction To OpenGL" article, you should really read that before you start this series anyway. Let's get started.
What first?
Not knowing where to start is a problem. :) Let's start by including the right header files for the program:
#include <windows.h>
#include <gl/gl.h>
#include <gl/glu.h>
#include <gl/glaux.h>
Not too hard, that just includes the windows header file and the OpenGL header files.
Now, we need to link in the OpenGL libraries, just link in opengl32.lib, glu32.lib and glaux.lib.
What next? (i)
Now we'll lay down some stuff you'll need for every OpenGL program, btw: I'm really sick of typing "OpenGL", it'll have to be "ogl" from now on. ogl uses what are called "Render Contexts" to write to a window. Normally, we get Device Contexts to play with a window using GetDC. Now we get a Device context, and then use it to get a Render Context, you'll see more on this later. These are declared as global variables:
HGLRC hRC; // OpenGL Render Context
HDC hDC;
Now we'll create a function to initialise OpenGL for us, set what shading we want, whether we want to use a Z-buffer and that kind of thing.
void InitOpenGL()
{
// If we wanted Z-buffering we would say so, or if we wanted flat shading
// etc we would say so here.
// Set clearing the background to black
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClearDepth(1.0); // Enable clearing of the depth buffer
glDepthFunc(GL_LESS); // The kind of depth testing to do.
glEnable(GL_DEPTH_TEST); // Enable it!
// Set the matrix mode, so the next commands are on the Projection Matrix
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0f, (GLfloat) WidthOfWindow / (GLfloat) HeightOfWindow,
0.1f, 100.0f);
// Now switch to the Model View Matrix.
glMatrixMode(GL_MODELVIEW);
return;
}
Ok, so you're confused. glClearColor takes four arguments, red, green, blue and alpha. The arguments are floating point numbers between 0 and 1, 0 is darkest, 1 is brightest, 0.5 is half bright, etc. So we can specify a colour easily enough now, leave the alpha argument alone for now, all you need are the r, g and b arguments to get all the colours. It sets the colour with which the screen will be cleared with from now on.
glClearDepth(1.0) just says to OpenGL, "please allow user to clear depth buffer", 1.0 is the argument to say that.
Now enable this "depth buffer", what the hell is that anyway? a depth buffer is OpenGL slang for a Z buffer.
What's all this Matrix stuff?
Firstly, we set the active matrix by saying: glMatrixMode( a nice matrix ); Above we use the Model View matrix and the Projection matrix, model view is what it sounds like you use it to store information about your 3d scene meshes eg. angle of rotation. The projection matrix holds information about how the 3d scene should be viewed, aspect ratio etc. The function glLoadIdentity(), resets a matrix, for example on our model view matrix, if we wanted to reset the rotation of our meshes we would call it.
gluPerspective takes 4 arguments: Field Of View (in degrees), which we set to 45 degrees, then the aspect ratio of the window we're working in. Then the distance of our z-near and z-far clipping planes from the viewer, z-near is first then z-far, these are NOT z coordinates, they are distances.
Notes on this section (i)
(i) OpenGL works with floating point numbers all the time, for color setting and even for simple arguments like in glClearDepth, we should try and get used to working with them so we're comfortable using OpenGL.
(ii) Also, using glEnable on GL_DEPTH_TEST, setting the depth function and cl -earing the depth buffer are all defaulted to automatically by OpenGL, I've put them in because later on you'll need to know what's going on, you can even leave out glClearColor, and the background will default to white.
What next? (ii)
Ok... well done for getting this far. I think it might be good to draw our 3d OpenGL scene at some stage, let's write a short function.
void DrawScene() // This function draws our 3D scene.
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
return;
}
Not complex, we just clear the depth buffer and the screen, and then call glLoadIdentity(), that moves us back to the origin (0, 0, 0) and sets the x, y and z axes pointing in the usual directions, we do this in case we had moved the viewer and/or made him/her look up or down, or something similar. The GL_COLOR_BUFFER_BIT clears the screen, and the GL_DEPTH_BUFFER_BIT clears the depth buffer, ORing them together does both.
Ok, we're finished render related coding for now, all we have to do is learn how to set up OpenGL.
Setting Up OpenGL under Micro$oft WinDOwS
This isn't exciting and I hate Windows. We set up an "OpenGL window". I will NOT explain much about the Windows code, only what is different to the tutorial by Frenzy in Hugi 14, read that tutorial if you can't get a window up on the screen in Windows, it's a great tutorial.
What comes first is the pixel format stuff, we declare a "pixel format descriptor" here. The pixel format of a window is what it supports, like, double buffering, what bit depth, z buffering, bit depth of z buffer etc.
There are *lots* of values in the pixel format descriptor we can fill but I zero most of them and set the ones I want.
PIXELFORMATDESCRIPTOR PFD; // PFD stands for Pixel Format Descriptor
memset(&PFD, 0, sizeof(PFD));
PFD.nSize = sizeof(PIXELFORMATDESCRIPTOR);
PFD.nVersion = 1;
PFD.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
PFD.iPixelType = PFD_TYPE_RGBA;
PFD.cColorBits = 16;
PFD.cDepthBits = 16;
PFD.dwLayerMask = PFD_MAIN_PLANE;
First we set PFD.nSize to the size of the class/structure thingy. Now we set the version (PFD.nVersion) just set it to 1.
The next thing we set is dwFlags, they are:
1) We are drawing to a window
2) Support OpenGL (?? duh!?)
3) Support double buffering, this means page flipping.
Now we set the pixel type (iPixelType), use PFD_TYPE_RGBA for now, I mean, you can change it, it just depends on what you want.
PFD.cColorBits is just the BPP you want to use.
PFD.cDepthBits is the number of bits to use for your depth (z) buffer, by setting it to 16 we are getting a 16 bit z buffer, i.e. one with 2^16 possible different z values.
PFD.dwLayerMask to PFD_MAIN_PLANE is setting this PFD to be the main drawing plane or screen/window to draw to.
Great! but what do we do with that PFD... well, when our window is created we choose a pixel format that matches our one or maybe a close match, note: this must be done after creating the window because we need to pass GetDC the handle of our window.
Now we're going to write a function to set up that stuff I just mentioned. Here's how it goes:
void SetupScreenDetails()
{
GLuint PixelFormat;
hDC = GetDC(hWnd); // Get Mr. DC, hWnd is the handle of our GL window.
// Get the closest match to pixel format specified above
PixelFormat = ChoosePixelFormat(hDC, &PFD); // pass DC of window and PFD
if(!PixelFormat)
{
// Bring up one of those annoying windows
MessageBox(NULL, "Could not find suitable pixel format",
"Pixel Format Error", MB_OK);
// send the quit message :)
PostQuitMessage(0);
}
// Try and SET the pixel format on the window using the DC, the pixel
// format we just found and the PFD
if(!SetPixelFormat(hDC, PixelFormat, &PFD)) // DC, pixel format used, PFD
{
// Bring up one of those annoying windows
MessageBox(NULL, "Can't set the pixel format",
"Pixel Format Error", MB_OK);
PostQuitMessage(0);
}
By the end of that segment of code we've tricked windows into letting us do our cool OpenGL thing - almost. When I said above set the "closest match" to the pixel format, I'm only repeating something I've read in the Windows documentation, I don't know why it's always referred to as the closest match...besides the obvious which seems stupid.
Now we'll just set up our render contexts (this continues from above code):
hRC = wglCreateContext(hDC); // Get a render context from our DC
if(!hRC)
{
MessageBox(NULL, "Can't create render context.", "Render Context Error",
MB_OK);
PostQuitMessage(0);
}
if(!wglMakeCurrent(hDC, hRC)) // Make this render context the current one.
{
MessageBox(NULL, "Can't set active render context.",
"Render Context Error", MB_OK);
PostQuitMessage(0);
}
// change video mode.
return;
}
Ok a little explaination of the above might help, I think most of the pixel format stuff is ok, it's really self explanatary. About the render contexts, as I said above, first you get your standard old friendly Device Context, then you use wglCreateContext on your DC to create a render context (much cooler than a DC), then we make it the current one so OpenGL can draw to it, using wglMakeCurrent. It's just something OpenGL uses to write to the window.
We need yet another function now, the function called when our window is resized.
void ResizeOpenGLApp(int Width, int Height)
{
if(!Height) return;
glViewport(0, 0, Width, Height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0f, Width / (GLfloat) Height, 0.1f, 100.0f);
glMatrixMode(GL_MODELVIEW);
return;
}
glViewport just sets the active drawing area (our "view port"), then the code is the same as above, just do the perspective stuff.
Finally, when the program quits we should try and tidy up after ourselves, so, here is another function:
void CloseOpenGL()
{
wglMakeCurrent(hDC, NULL); // Make the DC of the window the current RC
wglDeleteContext(hRC); // Get rid of our RC
ReleaseDC(hWnd, hDC); // Release the DC of our window
// restore video mode.
return;
}
Notes on this section (ii)
(i) This is an important section of the tutorial, look through it carefully.
(ii) I have left changing of the screen resolution up to you, I want this code to be compatible, and sometimes that can go wrong. Just use DEVMODE to do it.
What next? (iii)
Congratulations! You've made it all the way to here!
Now all we need to do is code our WinMain function, this is the easiest part. First we need a window, this is pretty standard EXCEPT, we need to specify a few things, when we are creating the window class, remember WNDCLASS? For the style we need to add CS_OWNDC, so the DC isn't shared across applications (you'll see this in the code). When creating the window we need to add WS_CLIPCHILDREN | WS_CLIPSIBLINGS, to the third argument in the CreateWindow function, they are required for OpenGL apps.
In the message loop, we do the following:
DrawScene(); // This is the function we coded above
SwapBuffers(hDC);
The SwapBuffers function takes an argument of the window to draw to, remember we set in our PFD to do page flipping (double buffering), well that means to actually see our stuff we have to flip it onto the screen from the offscreen buffer.
Et voila! you have a blank screen coloured black! Amazing...
Final Notes and Overview
(i) Most importantly for the last section, is glancing at the appropriate area of the source code, because I didn't want to write out all the window initialisation code.
(ii) The code above was typed in from the top of my head, so it mightn't be perfectly correct (syntax errors maybe), see the source code for the full correct source.
(iii) Don't be afraid to just cut and paste some of this code into your own programs, it's not ripping, as long as you understand it and the author wants you to play with it, obviously in the future you'll use your own code, but for now, while learning it'd be a good idea.
Conclusion
If you get bored of looking at a blank screen, maybe you should read my next tutorial in this issue, what about a rotating shaded 3d object? Please correct me on the mistakes I made in this tutorial, I hope this wasn't too long, or too incorrect... feedback...