INTRODUCTION
Original Author: Damiano Vitulli
Translation by: Click here
In the previous lesson, we constructed the first section of our rendering pipeline: how to acquire the object's data and store all the data in a structure. Today, we will complete points 2 and 3:
- Acquire the object's data and store all the data in a structure (Completed!)
- Transformations to place the objects in the world (modeling and viewing transformations)
- Rendering the scene on the 2d screen (projection transformation, viewport transformation, back face culling, color and depth buffers)
Then we will introduce the graphic libraries of OpenGL and the utility libraries of GLUT. At the end of this lesson we will be able to show a rotating solid object on our screen!
TRANSFORMATIONS TO PLACE THE OBJECTS IN THE WORLD (modeling and viewing transformations)
After defining the object's structure, we must apply some transformations to all the vertices we have before we can show it on the screen. We want to create a world, or to be more exact, (being mad about space simulators) a whole universe.
The first transformation is the MODELING TRANSFORMATION. Since our intention is to create spaceships that, until proven otherwise, are not static objects ;-), we have to transform the object's local coordinate system (that is relative to the central position of each object) to an absolute coordinate system (that is relative to the center of the 3D universe ;-) ). In other words we must translate the object by adding the current object's position in the universe (which may continue to change if there is motion) to the local vertices' coordinates.
Next is the VIEWING TRANSFORMATION. Our biggest aim, of course is to explore this universe by moving ourselves through it as we like, making the monitor a free moving video camera. How can we do this kind of transformation? The answer is relatively simple. In fact, you can simply consider the camera to always be at position 0,0 with no rotations. And then? Simple! We take whatever transformation we would have to apply to the camera to obtain a certain motion and we apply the opposite transformation to all the objects instead of moving the camera. Suppose for example that we wanted to move our point of view towards the object +10 points on the Z axis and to look at the object from above (rotating on the x axis 40 degrees). What really happens in most graphic engines is that the object has made a translation of -10 on the Z axis and then a rotation of -40 degrees on the X axis. This greatly simplifies the management of the engine because the video camera will always be at the origin.
RENDER THE SCENE ON THE 2D SCREEN (back face culling, projection and viewing transformation, color and depth buffer)
The next operation to perform is the BACK FACE CULLING. This means we are going to exclude the triangles that are not visible, that is the faces on the back sides of the objects. We can save a lot of rendering time this way because the drawing function will draw only half of the total triangles. OpenGL will do this action for us.
The next transformation is the PROJECTION TRANSFORMATION. At this point we need to "squash" a 3d scene onto a 2d screen. We need to simulate the Z axis because our poor monitor only has two axes X and Y. The easiest way to carry out this type of translation is to divide all the point's (x,y) coordinates relative to their Z component. The effect of this procedure is to compress the distant points so that they seem to approach the central point x = 0 and y = 0. This is exactly what happens in GL. Many of you may know this as "perspective projection".
The last transformation is the VIEWPORT TRANSFORMATION. All it does is convert all the points that will be used to the current viewport resolution.
A buffer is a zone of memory in which we can save some data. The OpenGL buffers are regions precisely as large as our viewport. For example, if we open a window 640 x 480 in size we allocate a buffer: 640 x 480 = 307200 pixels. That means, for 16 bit color mode: 307200*16 = 4915200 bits. That corresponds to about 614 Kbytes of video memory!
OpenGL has two main buffers: the COLOR BUFFER (that can be single or double) and the DEPTH BUFFER.
The COLOR BUFFER is what we see on the screen and where the end of all the drawing operations go. After having carried out all the geometrical calculations, OpenGL begins to fill this buffer pixel for pixel, filling our triangles. If the scene is animated the COLOR BUFFER is drawn and deleted every frame. The COLOR BUFFER is often used in dual mode operation, called DOUBLE BUFFER, which is what we will use. Dual mode operation consists of displaying one buffer while the other buffer is cleaned and filled with the next frame. Once this operation is completed the buffers will be exchanged. Using this technique, the resulting animation is practically free of flicking.
Now, suppose that there are two triangles in our scene one behind the other, both visible. In this case, the order in which the triangles are drawn is very important. If we draw the triangle nearest to our point of view first and then the more distant one, the pixels of the near triangle will be covered by the far one, creating an unpleasant effect. One technique to avoid this is to order all the visible triangles by their Z vertices. Then draw them in order, from the more distant to the nearest triangle. This technique is called THE PAINTER'S ALGORITHM. We will not use this method because OpenGL supplies us with a more efficient tool: the DEPTH BUFFER. This buffer has the same dimensions as the COLOR BUFFER but instead of containing the pixel's colors it contains information about the depth of every pixel on the Z axis. Saving this information is very important and for a very simple reason. When we are going to draw our triangles pixel for pixel on the screen, we first do a test to see if the pixel to print is closer than the pixel already stored in the Z buffer. If it is closer then we update the depth buffer with the new value and write to the COLOR BUFFER. If it is not closer we don't consider that pixel for drawing. This produces excellent results!
We don't need to worry about coding these operations "by hand" because our great GL library will do all the calculations for us. OpenGL will also carry out all the low-level drawing operations including painting our triangles and applying colors to them as well as lighting and mapping effects.
We don't have to do anything? So, what are we doing here? Wasting our time? No! We need to supply OpenGL with all the information it needs so that it can make all those calculations, interface with the video card and perform all the low level operations using (if it is present) the 3d hardware acceleration.
FINALLY, OPENGL!
OpenGL is a library that lets us interface with the graphics hardware. It has a series of functions to draw points, lines and polygons, and it carries out all the calculations necessary for the illumination, shading and transformation of the vertices. Glut instead is a utility library used to interface OpenGL with the window system. It allows us to create a window independent of the platform used (Windows or Linux). It also handles the keyboard input.
The structure of our OpenGL program is divided into different sections:
- Init function: used to boot OpenGL and to init the modeling, viewing and projection matrices. We can also put all the init operations we require in this function.
- Resize function: called every time the user starts the program or changes the output-window resolution. This is necessary to communicate the new viewport size to OpenGL.
- Keyboard function: called every time the user presses a key.
- Drawing function: clears all the buffers (color and depth). All the modeling, viewing and projection transformations are carried out and the scene is drawn. Finally the 2 color buffers are exchanged.
- Main Cycle: an infinite loop, which calls all our functions every frame.
A typical OpenGL function looks like: glFunctionName(GL_TYPE arguments).
For Glut functions we have: glutFunctionName(arguments).
OpenGL also has custom types to help portability. These types start with the prefix "GL" and are followed by "u" (for the unsigned values) and by the type (float, int etc.). For example, we can use GLfloat or GLuint to define variable types similar to the "float" and "unsigned int" types in C.
HEADERS
The first thing to do is to include all the necessary headers: windows.h (for the windows users) and glut.h
#include <windows.h> #include <GL/glut.h>
By including glut.h we have also indirectly included gl.h and glu.h (the OpenGL headers). It's also very important to set up the linker options in the compiler so that it includes the libraries opengl32.lib, glu32.lib and glut32.lib
Now, we must declare a function which initializes OpenGL.
INIT FUNCTION
void init(void) { glClearColor(0.0, 0.0, 0.2, 0.0); glShadeModel(GL_SMOOTH); glViewport(0,0,screen_width,screen_height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0f,(GLfloat)screen_width/(GLfloat)screen_height,1.0f,1000.0f); glEnable(GL_DEPTH_TEST); glPolygonMode (GL_FRONT_AND_BACK, GL_FILL); }
Let's start to analyze the code:
- void glClearColor( GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); specifies the red, green, blue, and alpha values used by glClear to clear the color buffers. We use dark blue as a background color so we assign 0.2 as the blue component. The other colors are set to 0.0 and so is the alpha value (I will explain the meaning of this component in another tutorial). I forgot to tell you that in OpenGL the effective working range for the parameters is 0-1. So, we can mix all the components to create any color we want. For example, to create a yellow background we must set the red component to 0 and the green and blue component both to 1.0.
- void glShadeModel( GLenum mode ); specifies a value representing a shading technique (the way OpenGL will fill the triangles). If we use GL_FLAT for "mode" then each triangle will be drawn in FLAT SHADING mode and the color will be uniform (the same color for each pixel of the triangle). If we use GL_SMOOTH instead, OpenGL will make linear interpolations between the colors of the triangle's vertices. This technique is also called GOURAUD SHADING.
- void glViewport( GLint x, GLint y, GLsizei width, GLsizei height); sets the dimensions of the current viewport for the viewport transformation.
- void glMatrixMode( GLenum mode); tells OpenGL which matrix is the current active matrix for matrix operations. What is a matrix? We have not yet spoken about matrices. This subject is too complicated for now and we will dedicate a complete tutorial to it later. You only need to know that a matrix is an object used by OpenGL to make all the geometrical transformations for now. In "mode" we can insert the value GL_PROJECTION to make projective transformations or GL_MODELVIEW to make viewing and modeling transformations. Once we specify the current active matrix, we can modify it as we like.
- void glLoadIdentity( void ); resets the current active matrix (GL_PROJECTION in our case) by loading the identity matrix.
- void gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble near,GLdouble far); do you remember the projection transformation? This is where we put it into practice. OpenGL fills the current active matrix (GL_PROJECTION) using the values specified in gluPerspective. In "fovy" we must specify the angle of the field of view, in "aspect" the aspect ratio, in "near" and "far" the minimum and maximum distance of the clipping planes.
- void glEnable( GLenum cap ); enables various capabilities. In this situation we have enabled the Z Buffer (DEPTH BUFFER).
- void glPolygonMode( GLenum face, GLenum mode ); draws our polygons as points, lines or filled (using GL_POINT,GL_LINES or GL_FILL as a parameter).
RESIZE FUNCTION
This function is very similar to "init". It clears the buffers, redefines our viewport, and redisplays our scene.
void resize (int width, int height) { screen_width=width; screen_height=height; glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glViewport(0,0,screen_width,screen_height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0f,(GLfloat)screen_width/(GLfloat)screen_height,1.0f,1000.0f); glutPostRedisplay (); }
Here are the functions we haven't covered:
- void glClear( GLbitfield mask ); clears the buffers specified in "mask". We can insert more than one buffer separating them with the OR logical operator "|". In our case we clear both the color and the depth buffer.
- The call to glViewport is necessary here to redefine the new viewport with the new values stored in screen_width and screen_height.
- void glutPostRedisplay(void); This is a glut function. It calls whatever routine we insert in the glutDisplayFunc function (called in our Main function), in order to redraw the scene.
KEYBOARD FUNCTIONS
We will define two keyboard functions: one to handle the ASCII character input ("r" and "R" and a blank character ' ') and another to handle the directional keys:
void keyboard (unsigned char key, int x, int y) { switch (key) { case ' ': rotation_x_increment=0; rotation_y_increment=0; rotation_z_increment=0; break; case 'r': case 'R': if (filling==0) { glPolygonMode (GL_FRONT_AND_BACK, GL_FILL); filling=1; } else { glPolygonMode (GL_FRONT_AND_BACK, GL_LINE); filling=0; } break; } }
We use three variables to make our object rotate around the desired axis: rotation_x_increment, rotation_y_increment and rotation_z_increment. We can reset all these variables using the spacebar and pause our object's movement at it's current position. We can also change the drawing mode for our polygons to outlined or filled with the key "r" or "R".
void keyboard_s (int key, int x, int y) { switch (key) { case GLUT_KEY_UP: rotation_x_increment = rotation_x_increment +0.005; break; case GLUT_KEY_DOWN: rotation_x_increment = rotation_x_increment -0.005; break; case GLUT_KEY_LEFT: rotation_y_increment = rotation_y_increment +0.005; break; case GLUT_KEY_RIGHT: rotation_y_increment = rotation_y_increment -0.005; break; } }
This function is very similar to the last one but it handles the directional keys. Notice the Glut constants GLUT_KEY_UP, GLUT_KEY_DOWN, GLUT_KEY_LEFT and GLUT_KEY_RIGHT that identify their respective directions and increase or decrease our rotational values.
DISPLAY FUNCTION
Ladies and gentlemen, the function you've been waiting for: the drawing function!
void display(void) { int l_index; glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0,0.0,-50); rotation_x = rotation_x + rotation_x_increment; rotation_y = rotation_y + rotation_y_increment; rotation_z = rotation_z + rotation_z_increment; if (rotation_x > 359) rotation_x = 0; if (rotation_y > 359) rotation_y = 0; if (rotation_z > 359) rotation_z = 0; glRotatef(rotation_x,1.0,0.0,0.0); glRotatef(rotation_y,0.0,1.0,0.0); glRotatef(rotation_z,0.0,0.0,1.0);
The first part of this function clears the color and depth buffer and applies the viewing and modeling transformations. We set the modelview matrix as the current active matrix using glMatrixMode with GL_MODELVIEW. Then, we initialize this matrix each frame with a call to glLoadIdentity.
- void glTranslatef( GLfloat x, GLfloat y, GLfloat z ); moves our object in 3D space. This function multiplies the model matrix by a translation matrix defined using the x,y,z parameters. The letter "f" at the end of the name indicates that we are using float type values, instead of using the letter "d" which indicates double type parameters. We use glTranslate to move the object 50 points forward in this case. Do you remember the video camera and the viewing transformation? Well, we can consider this operation as a translation of -50 for our video camera. This movement is necessary because we must move a small distance away from the object so that we can see it. Once you compile this project you can try to modify the Z value just to see how it affects the distance.
- void glRotatef( GLfloat angle, GLfloat x, GLfloat y, GLfloat z ); This function multiplies the current matrix by a rotation matrix. It's used to apply rotations of angle degrees around the vector (x,y,z). The variables rotation_x, rotation_y and rotation_z store the rotation values of the object. We consider this transformation a model transformation because we only rotate the object and not the point of view. As you can see, the differences between modeling and viewing transformations are not so evident. However, we don't need to worry about this now. We will face this subject in the video camera tutorial.
glBegin(GL_TRIANGLES); for (l_index=0;l_index<12;l_index++) { glColor3f(1.0,0.0,0.0); glVertex3f( cube.vertex[ cube.polygon[l_index].a ].x, cube.vertex[ cube.polygon[l_index].a ].y, cube.vertex[ cube.polygon[l_index].a ].z); glColor3f(0.0,1.0,0.0); glVertex3f( cube.vertex[ cube.polygon[l_index].b ].x, cube.vertex[ cube.polygon[l_index].b ].y, cube.vertex[ cube.polygon[l_index].b ].z); glColor3f(0.0,0.0,1.0); glVertex3f( cube.vertex[ cube.polygon[l_index].c ].x, cube.vertex[ cube.polygon[l_index].c ].y, cube.vertex[ cube.polygon[l_index].c ].z); } glEnd(); glFlush(); glutSwapBuffers(); }
The second part of the display function uses the functions glBegin and glEnd. These two commands mark the vertices that define a graphic primitive.
- void glBegin( GLenum mode ); indicates the beginning of a vertex-data list that define a geometric primitive. In the "mode" parameter we can insert the type of primitive we are going to draw. There are ten available types (GL_TRIANGLES, GL_POYLGON, GL_LINES etc.). We use GL_TRIANGLES because we want to draw our cube using 12 triangles. We do this by starting a "for" loop in which we make 3 calls to glVertex3f and glColor3f.
- void glVertex3f( GLfloat x, GLfloat y, GLfloat z ); specifies the x,y,z coordinates of a vertex. The "3f" indicates that there are three parameters of type float. If you only need to work with values x and y of type double, the correct command is: void glVertex2d( GLdouble x, GLdouble y ). We insert our object's geometrical data in each parameter.
- void glColor3f( GLfloat red, GLfloat green, GLfloat blue ); specifies the current active color. We define a different color (red 1,0,0 - green 0,1,0 - blue 0,0,1) for each vertex. Notice how the OpenGL shading mode GL_SMOOTH affects the final colors of the triangle. We see that the colors are interpolated from vertex to vertex so that they change gradually. That's very nice too see!
- void glEnd( void ); ends our object definition.
- void glFlush( void ); used to force OpenGL to draw the scene. The color and the depth buffers are filled and the scene is now visible. Finally!
- void glutSwapBuffers(void); exchanges the back buffer (where all the drawing functions work) with the front buffer (what we see in our window) when in double buffered mode. If we are not in double buffered mode this function has no effect.
THE MAIN FUNCTION
int main(int argc, char **argv) {
The following four functions of the glut library allow us to create our window for the graphic output.
glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); glutInitWindowSize(screen_width,screen_height); glutInitWindowPosition(0,0); glutCreateWindow("www.spacesimulator.net - 3d engine tutorials: Tutorial 2");
- void glutInit(&argc, argv); initializes the GLUT library. We need to call this before calling any other glut functions.
- void glutInitDisplayMode(unsigned int mode); sets the display mode. The glut constants GLUT_DOUBLE, GLUT RGB, GLUT DEPTH define the double buffered mode, the RGB color mode and depth buffer respectively.
- void glutInitWindowSize(int width, int height); and void glutInitWindowPosition(int x, int y); used to set the dimensions and the initial position of the output window
- int glutCreateWindow(char *name); creates our output window.
To define the callbacks we use:
glutDisplayFunc(display); glutIdleFunc(display); glutReshapeFunc (resize); glutKeyboardFunc (keyboard); glutSpecialFunc (keyboard_s); init(); glutMainLoop(); }
- void glutDisplayFunc(void (*func) (void)); specifies the function to call when the window needs to be redisplayed, that is when there is a glutPostRedisplay call or when there is an error reported by the window system.
- void glutIdleFunc(void (*func) (void)); sets the idle callback function: the function called every time there are no window system events. This means that our idle function "display" is continuously called, unless window events are received. This will keep our animation working.
- void glutReshapeFunc(void (*func) (void)); sets the reshape callback function.
- void glutKeyboardFunc(void (*func) (void)); keyboard callback function for ASCII characters.
- void glutSpecialFunc(void (*func) (void)); keyboard callback function for non-ASCII characters.
- void glutMainLoop(void); starts the glut infinite loop, with event processing.
CONCLUSIONS
So? Yes, that's really all there is! I know, it was hard work but look out! You are now able to create a real 3d rotating object! That means you made your first 3d engine!
In the next lesson, we will study how to do texture mapping.
SOURCE CODE
The Source Code of this lesson can be downloaded from the Tutorials Main Page