OpenGL Tutorial
- Lesson Four -
Abstract: In this lesson, we will enhance our program by performing simple transformations, adding colors and handling window resizing messages. We will also learn how to run an OpenGL program in full-screen mode. 1. Let it rotate! So far all the excercises we had are static. The key point of using OpenGL, however, is to create dynamic graphics. So in this section, we will throw some dynamics into the program. To be more specific, what we are going to do here in this section is to make the triangle created in the previous lesson rotating. The code segment is very simple (as usual we will focus on the part that is different from the previous lesson), but the structure and the logic of the program can be used in much more general situations. Notice that even though the triangle we had in the previous lesson looks static, it is drawn repetitively by the (busy) message loop. Now if instead of drawing the same identical triangle again and again in the loop, we give it a slightly different orientation each time, then the overall visual effect will be the same as that of a rotating triangle. That is exactly what we will do. The following code demonstrates how we do it: float angle = 0.0f; // A global variable for the anlge of rotation // Do OpenGL rendering void MyRendering() { // Reset the back buffer glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); angle = angle + 0.1f; if (angle >= 360.0f) { angle = 0.0f; } glRotatef(angle, 0.0f, 0.0f, 1.0f); // Drawing - on the back buffer glBegin(GL_TRIANGLES); glVertex3f(0.0f, 0.0f, 0.0f); glVertex3f(1.0f, 0.0f, 0.0f); glVertex3f(1.0f, 1.0f, 0.0f); glEnd(); // Swap the back buffer with the front buffer SwapBuffers(global_hdc); }
A snapshot of the running triangle is shown in Fig-1 (with the color effect from the next section also applied). Only the rendering function is shown here since that's where the modifications go. A global variable angle is introduced to keep track of the orientation of the triangle. It is initialized to zero. In the rendering function MyRendering, we increase the angle by 0.1, this way each time the triangle is rendered (namely the MyRendering function is called), it will be rotated 0.1 degree relative to its previous configuration. When the angle reaches a full circle - 360 degrees, we set it back to zero (if we don't do this, the program will still run, but the float variable angle will grow unbounded and eventually run out of range). The actual rotation of the triangle is performed using the glRotatef function. If you have experience in analytic geometry, you probably have learned that there are always two ways to perform a transformation to a geometric object: you either rotate the object directly, or rotate the coordinate system while keeping the object fixed in the system. What glRotatef does is the latter, namely it rotates the whole coordinate system and leaves the geometric object fixed in the coordinate system. The beauty of this approach is you don't need to change the code - those enclosed in between the glBegin/glEnd pair - that draws the geometric object (because the drawing is relative to the coordinate system). glRotatef takes the angle of rotation (in degrees) as its first argument. The other three arguments specifies the direction of the axis of rotation (the z-axis in our case). There are several other transformation functions available in the core OpenGL library, for instance the glTranslatef function for translating objects and the glScalef function for scaling objects. I will leave it for you to try out those transformations. One thing you might ask about the program is: how fast will the triangle rotate? If you look at the code, you will find nothing about time control. In the message loop (the while loop) that repetitively invoke the MyRendering function, we could have setup a timer that controls the frequency of calling the rendering function. We omitted this for simplicity, so it is totally up to the computer system (especially its video card) that runs the program to determine how fast MyRendering will be repeated. If you find the triangle rotates too fast or too slow on your system, feel free to set the increment of angle to a different value. 2. Adding color Now that we all probably have stared too long on our little black-and-white triangle, it is the time to dress it with some color. This can be done by a single line of code that invokes a function called glColor3f, which sets the color for the rendering context (therefore affects all the renderings that follow). glColor3f takes three obvious arguments that represent the RGB (Red, Green, Blue) values (scaled into the range of 0.0f - 1.0f) of the color. In our example, the color is set to be red (1.0f, 0.0f, 0.0f). glColor3f is only one of the many OpenGL functions that manipulate colors, check out the reference materials at the end of the lesson if you are interested in other similar functions. The updated MyRendering function is shown below: // Do OpenGL rendering void MyRendering() { // Reset the back buffer glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); angle = angle + 0.1f; if (angle >= 360.0f) { angle = 0.0f; } glRotatef(angle, 0.0f, 0.0f, 1.0f); // Drawing - on the back buffer glColor3f(1.0f, 0.0f, 0.0f); glBegin(GL_TRIANGLES); glVertex3f(0.0f, 0.0f, 0.0f); glVertex3f(1.0f, 0.0f, 0.0f); glVertex3f(1.0f, 1.0f, 0.0f); glEnd(); // Swap the back buffer with the front buffer SwapBuffers(global_hdc); } 3. Window resizing If you played the program a little, you may have noticed that when you resize the application window, the triangle doesn't adjust itself to fit the new window size. This is because we haven't wrote the code to handle the window resizing message - the WM_SIZE message - that is sent to the window procedure when the size of the application window is changed. We will do it in this section. To catch the WM_SIZE message, we simply add a new branch - the WM_SIZE handler - to the switch statement in the window procedure: // Window procedure - the message handler LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { HDC hdc = NULL; HGLRC hrc = NULL; PAINTSTRUCT ps; switch (msg) { case WM_CREATE: // Get a handle to the device context hdc = BeginPaint(hwnd, &ps); global_hdc = hdc; // Setup pixel format for the device context SetupPixelFormat(hdc); // Create a rendering context associated to the device context hrc = wglCreateContext(hdc); // Make the rendering context current wglMakeCurrent(hdc, hrc); break; case WM_CLOSE: // De-select the rendering context wglMakeCurrent(hdc, NULL); // Release the rendering context wglDeleteContext(hrc); // Release the device context EndPaint(hwnd, &ps); PostQuitMessage(0); break; case WM_SIZE: height = HIWORD(lParam); width = LOWORD(lParam); glViewport(0, 0, width, height); break; default: return DefWindowProc(hwnd, msg, wParam, lParam); } return 0; } The WM_SIZE handler is fairly simple in our case. as we mentioned in Lesson One, each Windows message carries some message-dependent extra information in the data fields wParam and lParam. In the case of a WM_SIZE message, the (32-bit double word) lParam contains the width and the height (both are 16-bit values) of the new window in its lower and higher halves and can be retrieved using two macro functions: LOWORD and HIWORD. Once these informations are retrieved, we can set the rendering area (also called the viewport) to fit the new window size. This is done by the glViewport function that takes the coordinate of the lower-left corner (defaults to 0, 0), the width and the height of the viewport as arguments. All the renderings after this will fit into the new window size. Due to the 2-D characteristics of our example, we have ignored the complexity of setting up a perspective view and updating the projection matrix (we haven't even introduced those concepts so far). We will come back to these pieces when discussing truely 3-D examples. 4. Full-screen OpenGL Many OpenGL programs, especially 3-D games, are running in the so-called full-screen mode in which the application window takes the whole screen. In this section, we will learn how to setup a full-screen mode for our program. At a fairly low level, Windows uses a data structure called DEVMODE to keep information about the device initialization and environment of an output device (screen in our case). We never mentioned this data structure before because the default values it carries work just fine for all our previous examples that run in a standard window. To setup a full-screen mode, however, we have to make changes to several data fields in DEVMODE. These changes need to be done in the WinMain function before the creation of the application window (which in this case should take up the whole screen). The updated WinMain function is shown below: // WinMain function - the entry point int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow) { HWND hwnd; MSG msg; WNDCLASS wndclass; // Specify a window class wndclass.style = 0; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = "ExampleClass"; // Register the window class RegisterClass(&wndclass); DEVMODE screenSettings; // Specify values for relevant data fields screenSettings.dmPelsWidth = 640; screenSettings.dmPelsHeight = 480; screenSettings.dmBitsPerPel = 16; // Indicate which fields are manually initialized screenSettings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL; // Apply changes ChangeDisplaySettings(&screenSettings, CDS_FULLSCREEN); // Create a window based on the window class hwnd = CreateWindow("ExampleClass", "Example", WS_POPUP, 0, 0, 640, 480, NULL, NULL, hInstance, NULL); // Display the window on the screen ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); // An alternative message loop bool quit = false; while(!quit) { PeekMessage(&msg, hwnd, NULL, NULL, PM_REMOVE); if (msg.message == WM_QUIT) { quit = true; } else { MyRendering(); TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam; } Let's go through the code line by line as what we have been doing all the time. We declare our own copy of the DEVMODE structure and assign values to three data fields that are relevant to us: dmPelsWidth, dmPelsHeight and dmBitsPerPel, which contain the (horizontal and vertical) screen resolution and the color depth for the full-screen mode (you can use your favorite values instead of the values used in the example). Since DEVMODE contains many other fields, and we don't want to incidentally overwrite those other values by the un-initialized data fields from our copy of the DEVMODE structure. Therefore it is important that we indicate which fields should pick up the manually defined values. This is done by setting up the relevant bit flags in a variable called dmFields (which itself is a data field in DEVMODE). Each bit flags in dmFields specifies whether certain members of the DEVMODE structure have been initialized. If a field is initialized, its corresponding bit flag is set, otherwise the bit flag is clear. Since we only need to set the the resolution and color depth, so dmFields contains only those bits. Finally, we change the setting of the display with the new DEVMODE by using the ChangeDisplaySettings function. This function takes the handle to the DEVMODE structure whose data fields have been changed by us as its first argument. The second argument it takes is a flag that indicates how the graphics mode should be changed. The value we use - CDS_FULLSCREEN - removes the taskbar from the screen, and is the least obtrusive one (what this means is all the changes we make will be temporary, the display setting will go back to whatever mode it was when the program exits). The ChangeDisplaySettings will check the dmFields field and extract only those fields from our DEVMODE structure that are indicated by dmFields. Once again, I would like to say that in a real program, it is always helpful to write some error checking code to make sure everything goes fine as expected. The ChangeDisplaySettings function, for instance, will return DISP_CHANGE_SUCCESSFUL on success, you can check this value and do something if it is not equal to DISP_CHANGE_SUCCESSFUL. Once the setting is successfully changed, we then continue the standard procedure of using the CreateWindow function to create the application window. Of course, the size of the application window should match the full-screen resolution we setup before, which is 640 × 480, and we also change the style of the window to WS_POPUP which means there will be no border for this window. Another thing I would like to mention is that our program won't exit gracefully, so if you run the program, you won't be able to stop it by, say, pressing the ESC key. To exit the program, you will need to kill the process (to do so, press Ctrl-Alt-Del, select it from the task manager and end it). You are strongly encouraged to write a handler for the program so when you press the ESC key, it exits. That's all for this lesson. The concept and code are both pretty simple, but the technique and code structure introduced in the lesson are standard and can be used in other programs. What's next? To be announced ... References
|