Lesson 6: Putting It All Together

We've learned a lot so far. Let's briefly go over the OpenGL we learned in the previous lessons, to make sure we understand everything. If you want, you can skip this lesson, but you might want to solidify everything you've learned thus far.

Since we love spinning objects so much, we want to make a spinning cube with two sides textured, two sides solid-colored, and two sides with a color gradient.

Cube program screenshot

Let's take a look at the source code. We'll briefly go through all of the code (except for the comments at the top).

#include <iostream>
#include <stdlib.h>

#ifdef __APPLE__
#include <OpenGL/OpenGL.h>
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif

#include "imageloader.h"

using namespace std;

Our include files. Plus, we have using namespace std, which we'll have at the top of all our main.cpp files.

const float BOX_SIZE = 7.0f; //The length of each side of the cube
float _angle; //The rotation of the box
GLuint _textureId; //The OpenGL id of the texture

BOX_SIZE is a constant storing the length of each side of the box. _angle stores the angle by which the box is currently rotated. _textureId has the id of the texture we're applying to two of the faces.

void handleKeypress(unsigned char key, int x, int y) {
    switch (key) {
        case 27: //Escape key
            exit(0);
    }
}

Handles keypresses. It exits the program when the user presses ESC.

//Makes the image into a texture, and returns the id of the texture
GLuint loadTexture(Image *image) {
    GLuint textureId;
    glGenTextures(1, &textureId);
    glBindTexture(GL_TEXTURE_2D, textureId);
    glTexImage2D(GL_TEXTURE_2D,
                 0,
                 GL_RGB,
                 image->width, image->height,
                 0,
                 GL_RGB,
                 GL_UNSIGNED_BYTE,
                 image->pixels);
    return textureId;
}

Our function for loading a texture from an Image object.

void initRendering() {
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);
    glEnable(GL_COLOR_MATERIAL);
    
    Image *image = loadBMP("vtr.bmp");
    _textureId = loadTexture(image);
    delete image;
}

Our function for initializing rendering. We enable depth testing, like always, as well as color, lighting, and light source number 0. Then, we load vtr.bmp into an Image object, load it into OpenGL as a texture, and delete the Image object, since we don't need it any more.

void handleResize(int w, int h) {
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0, (float)w / (float)h, 1.0, 200.0);
}

Our function for handling window resizes. It doesn't change very much in our different programs.

void drawScene() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

drawScene is our function for drawing the 3D scene. First, we call glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) to clear information from the last draw, like we always do.

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

We switch to the "normal" transformation mode, and reset transformations so that we are at the origin and are facing in the negative z direction.

    glTranslatef(0.0f, 0.0f, -20.0f);

We move forward 20 units, so that our cube will be 20 units in front of the camera.

    GLfloat ambientLight[] = {0.3f, 0.3f, 0.3f, 1.0f};
    glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambientLight);

We set the scene's ambient light, which shines everywhere, to have an intensity of 0.3.

    GLfloat lightColor[] = {0.7f, 0.7f, 0.7f, 1.0f};
    GLfloat lightPos[] = {-2 * BOX_SIZE, BOX_SIZE, 4 * BOX_SIZE, 1.0f};
    glLightfv(GL_LIGHT0, GL_DIFFUSE, lightColor);
    glLightfv(GL_LIGHT0, GL_POSITION, lightPos);

We set up a light source with an intensity of 0.7 at (-2 * BOX_SIZE, BOX_SIZE, 4 * BOX_SIZE), relative to the center of the cube.

    glRotatef(-_angle, 1.0f, 1.0f, 0.0f);

We rotate by the current angle about the vector (1, 1, 0), in order to produce the cube's spinning motion.

    glBegin(GL_QUADS);
    
    //Top face
    glColor3f(1.0f, 1.0f, 0.0f);
    glNormal3f(0.0, 1.0f, 0.0f);
    glVertex3f(-BOX_SIZE / 2, BOX_SIZE / 2, -BOX_SIZE / 2);
    glVertex3f(-BOX_SIZE / 2, BOX_SIZE / 2, BOX_SIZE / 2);
    glVertex3f(BOX_SIZE / 2, BOX_SIZE / 2, BOX_SIZE / 2);
    glVertex3f(BOX_SIZE / 2, BOX_SIZE / 2, -BOX_SIZE / 2);
    
    //Bottom face
    glColor3f(1.0f, 0.0f, 1.0f);
    glNormal3f(0.0, -1.0f, 0.0f);
    glVertex3f(-BOX_SIZE / 2, -BOX_SIZE / 2, -BOX_SIZE / 2);
    glVertex3f(BOX_SIZE / 2, -BOX_SIZE / 2, -BOX_SIZE / 2);
    glVertex3f(BOX_SIZE / 2, -BOX_SIZE / 2, BOX_SIZE / 2);
    glVertex3f(-BOX_SIZE / 2, -BOX_SIZE / 2, BOX_SIZE / 2);

We draw the top and bottom faces, which are solid-colored. Before giving the coordinates of each face using glVertex3f, we specify their colors and their normals, which have magnitude 1.

    //Left face
    glNormal3f(-1.0, 0.0f, 0.0f);
    glColor3f(0.0f, 1.0f, 1.0f);
    glVertex3f(-BOX_SIZE / 2, -BOX_SIZE / 2, -BOX_SIZE / 2);
    glVertex3f(-BOX_SIZE / 2, -BOX_SIZE / 2, BOX_SIZE / 2);
    glColor3f(0.0f, 0.0f, 1.0f);
    glVertex3f(-BOX_SIZE / 2, BOX_SIZE / 2, BOX_SIZE / 2);
    glVertex3f(-BOX_SIZE / 2, BOX_SIZE / 2, -BOX_SIZE / 2);
    
    //Right face
    glNormal3f(1.0, 0.0f, 0.0f);
    glColor3f(1.0f, 0.0f, 0.0f);
    glVertex3f(BOX_SIZE / 2, -BOX_SIZE / 2, -BOX_SIZE / 2);
    glVertex3f(BOX_SIZE / 2, BOX_SIZE / 2, -BOX_SIZE / 2);
    glColor3f(0.0f, 1.0f, 0.0f);
    glVertex3f(BOX_SIZE / 2, BOX_SIZE / 2, BOX_SIZE / 2);
    glVertex3f(BOX_SIZE / 2, -BOX_SIZE / 2, BOX_SIZE / 2);

We draw the left and right faces. For each face, we specify the normals first. Then, we set the current color to the first color of the gradient, and assign this color to the first two vertices by immediately calling glVertex3f twice. Then, we change the current color to the second color of the gradient, and assign this color to the other two vertices by subsequently calling glVertex3f two more times.

    glEnd();
    
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, _textureId);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glColor3f(1.0f, 1.0f, 1.0f);

Now, we want to apply our texture. We call glEnd to stop drawing quadrilaterals, because some texture functions can't be called in a glBegin-glEnd block. We call glEnable(GL_TEXTURE_2D) to enable OpenGL to apply textures to subsequent polygons. We call glBindTexture(GL_TEXTURE_2D, _textureId) to tell OpenGL that we want to apply the texture with id _textureId. We call glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) ande glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) to have OpenGL use blurry, rather than blocky, texture mapping. Then, we call glColor3f(1.0f, 1.0f, 1.0f) so that OpenGL won't try to change the color of our texture.

    glBegin(GL_QUADS);
    
    //Front face
    glNormal3f(0.0, 0.0f, 1.0f);
    glTexCoord2f(0.0f, 0.0f);
    glVertex3f(-BOX_SIZE / 2, -BOX_SIZE / 2, BOX_SIZE / 2);
    glTexCoord2f(1.0f, 0.0f);
    glVertex3f(BOX_SIZE / 2, -BOX_SIZE / 2, BOX_SIZE / 2);
    glTexCoord2f(1.0f, 1.0f);
    glVertex3f(BOX_SIZE / 2, BOX_SIZE / 2, BOX_SIZE / 2);
    glTexCoord2f(0.0f, 1.0f);
    glVertex3f(-BOX_SIZE / 2, BOX_SIZE / 2, BOX_SIZE / 2);
    
    //Back face
    glNormal3f(0.0, 0.0f, -1.0f);
    glTexCoord2f(0.0f, 0.0f);
    glVertex3f(-BOX_SIZE / 2, -BOX_SIZE / 2, -BOX_SIZE / 2);
    glTexCoord2f(1.0f, 0.0f);
    glVertex3f(-BOX_SIZE / 2, BOX_SIZE / 2, -BOX_SIZE / 2);
    glTexCoord2f(1.0f, 1.0f);
    glVertex3f(BOX_SIZE / 2, BOX_SIZE / 2, -BOX_SIZE / 2);
    glTexCoord2f(0.0f, 1.0f);
    glVertex3f(BOX_SIZE / 2, -BOX_SIZE / 2, -BOX_SIZE / 2);
    
    glEnd();

We draw the last two faces. For each, we specify the normal and then alternate between specifying the texture coordinates of a vertex and the actual coordinates of a vertex.

    glDisable(GL_TEXTURE_2D);

Now, we're done drawing textures, so we disable them. That way, the next time we draw something, OpenGL won't automatically apply our texture.

    glutSwapBuffers();
}

We send the scene to the window.

//Called every 25 milliseconds
void update(int value) {
    _angle += 1.0f;
    if (_angle > 360) {
        _angle -= 360;
    }

Here's the update function, which we're going to have GLUT call every 25 milliseconds. First, we increase the angle by 1 and, to try to keep the _angle variable low, we decrease it by 360 if it's greater than 360.

    glutPostRedisplay();

We tell GLUT that our scene has changed, and it should be redrawn.

    glutTimerFunc(25, update, 0);
}

We tell GLUT to call update again in 25 milliseconds.

int main(int argc, char** argv) {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
    glutInitWindowSize(400, 400);

We initialize GLUT.

    glutCreateWindow("Putting it All Together - videotutorialsrock.com");

We tell GLUT to create our window.

    initRendering();

We call our initRendering function to initialize some OpenGL rendering stuff.

    glutDisplayFunc(drawScene);
    glutKeyboardFunc(handleKeypress);
    glutReshapeFunc(handleResize);

We tell GLUT what functions to use to draw our scene, handle key presses, and resize the window.

    glutTimerFunc(25, update, 0);

We tell GLUT to call our timer function in 25 milliseconds.

    glutMainLoop();
    return 0;
}

We tell GLUT to start doing everything.

That's it. You may want to try the exercises for this lesson to get a little more practice with the basics of OpenGL.

Next is "Part 2: Topics in 3D Programming".