Welcome back to my OpenGL tutorial, this time we're on the third and final part where we will get to experience some hands-on coding with some of the more basic features of OpenGL to implement something a little more cooler than a simple stationery white square.
I'm going to dump some code that draws a rotating 3D-cube on the screen in various colors. The first thing I'd like to do after that is explain what the code does to get the cube to display on the screen. After that, we can better guide the reader through the process of implementing some extra bits.
At first, this code will draw a rotating 3D-cube, stationery on a black background. And with some extra effort, we'll be animating the cube so that it bounces around inside a larger defined boundary cube. This is like the old screen saver with a bouncing circle along the edges of your monitor. Except this will be a cube.
Starting Point
Copy-and-paste the following code. This is some code that I wrote. Run it to make sure that it compiles, builds and executes properly. It should be a rotating cube.
#include <GL\glew.h>
#include <GL\freeglut.h>
//disables console window so only our application window displays
#pragma comment( linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"" )
//defining variables here
float rot = 0.01;
//draws six faces of a cube with varying colors
void drawCube(void)
{
glBegin(GL_QUADS);
{
glColor3f(0.0f, 1.0f, 0.0f);
glVertex3f(1.0f, 1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glColor3f(1.0f, 0.5f, 0.0f);
glVertex3f(1.0f, -1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(1.0f, -1.0f, -1.0f);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f(1.0f, -1.0f, 1.0f);
glColor3f(1.0f, 1.0f, 0.0f);
glVertex3f(1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f(1.0f, 1.0f, -1.0f);
glColor3f(0.0f, 0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glColor3f(1.0f, 0.0f, 1.0f);
glVertex3f(1.0f, 1.0f, -1.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glVertex3f(1.0f, -1.0f, 1.0f);
glVertex3f(1.0f, -1.0f, -1.0f);
glEnd();
}
}
//draw our cube at its location and rotation angle
void render(void)
{
//clear, reset, recenter, update rotation
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
//snap our cube to its position and rotate it
glTranslatef(-1.5f, 0.0f, -15.0f);
glRotatef(rot, 1, 1, 0);
//draw our cube
drawCube();
//standard call to dump our drawing into the next frame
glutSwapBuffers();
}
//animation for rotating the cube
void animate()
{
//update rotation angle
rot += 1;
rot = rot >= 360 ? 0 : rot;
//refresh screen
glutPostRedisplay();
}
//reshapes the drawing when window is resized
void reshape(int w, int h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0f, (float)w / (float)h, 1, 1000);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
//main entry point
int main(int argc, char** argv)
{
//standard initialization routine
glutInit(&argc, argv);
glutInitWindowPosition(100, 100);
glutInitWindowSize(320, 230);
glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
glutCreateWindow("Simple Shape Drawing");
//color to draw background
glClearColor(0.0, 0.0, 0.0, 0.0);
//enable depth viewing
glEnable(GL_DEPTH_TEST);
//call our display function which actually does some drawing
glutDisplayFunc(render);
glutIdleFunc(animate);
glutReshapeFunc(reshape);
//enters our display functions into a repeating loop
glutMainLoop();
return 0;
}
Explaining Code
Let's dig deeper and see what our code is really doing.
Creating the Window
Let's have a look at the main entry point: the method named main. This serves as a basic naming function for C++ to know where to begin. Inside that method is a pretty standard OpenGL initialization routine. The first
glutInit call initializes the OpenGL engine with the given command line arguments to main. Then, the window position and size is initialized just after. You can modify the values in these if you like.
The
glutInitDisplayMode defines the display mode for the created window. You can play around with these settings if you want. The use of the "|" (bitwise or) operator allows settings to be chained together so that you can specify multiple different settings in the same line. If you remove the
GLUT_DOUBLE, which stands for double buffering, you may notice the cube rotates very hyper, i.e. very less smoothly.
Finally,
glutCreateWindow will form the window with the given title. You can name this however you want. Shortly after,
glEnable(GL_DEPTH_TEST) allows us to view our renderings in 3D by performing a depth test so that only the frontward faces are visible. If you remove this line, you'll notice that you can see all six faces of the cube at once as they're blended together in weird ways.
Calling our Drawers
Next, we reference our method where the actual cube drawing takes place with a trio of methods. The
glutDisplayFunc is what gets called when the window first displays or displays after being gone. The
glutIdleFunc gets called constantly. Lastly, the
glutReshapeFunc is what will be called when changing the size of the window when it is running. Here, we use the display func to store our actual drawing of the cube, and the idle func to animate our cube in its rotational manner.
Lastly, the reshape function is generally used to maintain any aspect ratio of the drawn shapes. In this demo, we use it to also grow or shrink the cube as the window changes size. You can see how this is done in the reshape method, which utilizes a bit of a standard routine. The important call is in
gluPerspective, which sets the field of view to a 45 degree cone, and the aspect ratio to whatever the ratio of the window is. The final two parameters to that method define the clipping points in the depth. Here, anything outside of Z=1 and Z=1000 will be clipped and not drawn.
Drawing the Cube
As stated just a bit ago, our cube will get drawn in the display func, which we've established is the function named render. Going there, we notice the first thing done is to clear the screen and load the identity via
glLoadIdentity. This refers to the identity matrix, in which OpenGL uses a matrix to store the contents of drawn shapes. This must be reset at the beginning of every display func so that it can successfully redraw the intended drawing properly from scratch.
The next thing done is to translate our "position" to the location where we want to draw the cube. Again, this has to do with the matrix OpenGL uses. We will first move to the location, and then we will perform the rotation at that point. When rotation and translation are done in opposite order, the cube will rotate around the point instead of in place at the point. This may be counter-intuitive, but to me it seems backwards. So it may be best to think of these operations as working in reverse.
Then, the cube is drawn in our custom method named drawCube. This is done by telling OpenGL that we're about to begin drawing some "gl_quads" primitives with
glBegin(GL_QUADS). These are four-sized rectangles specified by four vertices. To draw a cube with rectangles, we basically stitch them together on common vertices to define the six faces. This part may seem a bit too "numbery", so you may want to suffice it enough to say, that this sequence of vertex assignments is indeed a cube. However, note at the beginning of each face, we've defined a new color so that our faces are more distinguishable when drawn. Lastly, a call to
glEnd indicates that we're done drawing these primitives. I like to use curly brackets around these sections, but note that they're more for visual code style and are not necessary.
Finally, the end of our render function calls
glutSwapBuffers as part of its double buffering routine to increase the smoothness of the animation.
Animating the Cube
Our last code bit basically is the animate method which is called by our constantly repeating idle func. This part is simple: all we do is increment the rotation angle and keep it bounded cyclically between 0 and 360. The important bit is when
glutPostRedisplay is called to send a signal so that our display function is called to draw our cube.
That should be it! Now that we've reviewed pretty much all the basics of our demo program, we can begin to add a feature to it. Let's try to get this rotating cube to now move and then get it to bounce off walls. Finally, we'll draw the wall we're bouncing the cube off and have ourselves a bit of a nice screensaver type of program.
Adding Features
Now for the fun part. Basically, I'd like to get the cube to bounce off the walls like a screensaver, except in 3D.
More Member Variables
I've already provided the variable for our rotation angle, but let's add several more that we'll need in adding our feature. Place these variables below just below the rotation angle variable, rot, so that these are inside our class file but outside of any methods.
Location, Size, Velocity
First, let's add the location. This will be a 3D-coordinate system location defined as an array of 3 floating point values. The first two represent the position left/right (X) and top/down (Y) in the view. The last value represents the depth (Z). Since we more or less want the view to be some distance away from us, we specify an initial deep depth of -15.
float location[3] = { -1.5f, 0.0f, -15.0f }; //coordinate vector
To control the size of our cube, let's define a scale variable that shrinks or inflates our cube based on its setting. A value of 0.5 halves the size, and a value of 2 will double it. We will see its use later and how it can be used to scale our drawn shapes.
float scale = 0.5;
To animate our cube, we define a 3D-velocity that represents how much our cube moves in every direction. As far as animation goes, this is the amount the cube will move across every second.
float velocity[3] = { 1.0f, 1.0f, 1.0f }; //velocity vector
Boundary Walls
To allow our cube to bounce properly, we'll need to define where the walls are. This is a bounding box with six faces, and so hence we'll have six values to define the location of those planar faces.
float velocity[3] = { 1.0f, 1.0f, 1.0f }; //velocity vector
float lbounds[3] = {-5.0f, -5.0f, -50.0f}; //boundary box lower limits
Animation Timing
Since the animation routine will be called as indeterminate intervals, we'll have to handle animation by checking against the amount of time between calls and scaling our velocity with those elapsed time periods. These below variables will allow us to collect the time since epoch and use the elapsed milliseconds to more smoothly draw our animated cube.
First, add an import with the include tag at the top of the file:
#include <chrono>
Then, add these variables with our others:
__int64 elapsedTimeInMS = 0; //time since last draw
__int64 lastTimeEpoch = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count(); //current time
Updates to Drawing
Since we have added some information to describe the position of our cube, let's update our display function so that the cube is always drawn at its given location. To do this, simply edit our line where the
glTranslatef function is located and pass in our shape information:
glTranslatef(location[0], location[1], location[2]);
As for another update, since we have a variable to store the size or scale of our shape, let's also add that below the call to
glRotatef:
glScalef(scale, scale, scale);
The scale call basically scales what is drawn by a factor along each of the three dimensions. If edited, we could scale unevenly across our three dimensions. For example, a scale of 0.5 will halve the size, and a scale of 2.0 would double it.
Updating Animation
Now that we have some standard location animation variables, we can get to making our cube move. Inside our idle function, that is, over in our animate function, we can make a few adjustments. The first thing we'll want to do is find out how much time has elapsed since our last call to animate. This allows us to smoothly move the cube at its given velocity instead of moving in large hops. Add this code to the top of our animate function:
//get elapsed milliseconds
__int64 timeNow = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
elapsedTimeInMS = timeNow - lastTimeEpoch;
lastTimeEpoch = timeNow;
Next, we can make the position updates using a loop across our three dimensions:
//update position and detect wall bouncing
for (int i = 0; i < 3; i++)
{
location[i] = location[i] + velocity[i] * (1.0/ elapsedTimeInMS);
}
Finally, we can then implement velocity changes when a "bounce" off the boundary wall is detected. To do this, whenever a bounce is detected in that dimension, we simply reverse the velocity along that direction. Add this code after the position updates, but inside our loop:
if (location[i] > ubounds[i] || location[i] < lbounds[i])
{
velocity[i] *= -1; //when wall bounce detected, just simply go in opposite direction
}
The last part of the animation should remain the same. When put all together, it should look like this:
//routine to animate our cube with its velocity and elapsed time since last call
void animate(void)
{
//get elapsed milliseconds
__int64 timeNow = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
elapsedTimeInMS = timeNow - lastTimeEpoch;
lastTimeEpoch = timeNow;
//update position and detect wall bouncing
for (int i = 0; i < 3; i++)
{
location[i] = location[i] + velocity[i] * (1.0/ elapsedTimeInMS);
if (location[i] > ubounds[i] || location[i] < lbounds[i])
{
velocity[i] *= -1; //when wall bounce detected, just simply go in opposite direction
}
}
//update rotation angle
rot += 1;
rot = rot >= 360 ? 0 : rot;
//refresh screen
glutPostRedisplay();
}
Drawing Our Boundary Walls
If you run the code thus far, you should see our cube bounce about its boundary box. The trouble is, we can't see where our boundary walls are and that would be a nice feature. So let's work on that.
What we want to do is draw a wire-frame cube. To do the drawing, we'll do something similar to what was done for drawing our solid cube. Let's add a method for that first:
//draw the boundary box where cube bounces as a wireframe
void drawBounds(void)
{
glColor3f(1.0f, 1.0f, 1.0f);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glBegin(GL_QUADS);
{
glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
glEnd();
}
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
The first thing to note is that we define a polygon mode in
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) to be that of a wireframe, using constants that let us know only draw the lines and not fill to a solid color. The first constant (
GL_FRONT_AND_BACK) tells OpenGL to go ahead and draw all faces, even the ones we can't see. We unset this at the end so that our cube drawing doesn't draw in the same way.
Finally, we can hook up to our new drawBounds method from our display function. This part is simple, but the complexity behind it is a little more sophisticated. After having drawn our little cube, we'll want to undo what we've done to the matrix in our translate, rotate and scale operations. In this way, we can draw the wireframe boundary box in its actual place, devoid of any rotation, scale or translation.
The trick to undoing those operations is in the math. To undo a scale, we'd need an inverse. To undo our translation and rotation, we just need a negative. Additionally, the order here reflects that in the way they were first performed. Since our scale was last to perform, it is first to be undone, and so on.
//undo our snap, rotate and scale
glScalef(1 / scale, 1 / scale, 1 / scale);
glRotatef(-rot, 1, 1, 0);
glTranslatef(-location[0], -location[1], -location[2]);
Afterwards, we can now draw our boundary box and complete the method.
//draw the boundary wall
drawBounds();
Wrapping Up
That should do it. We've finished doing what we set out to do. For reference, here is the completed new code.
#include <GL\glew.h>
#include <GL\freeglut.h>
#include <chrono>
//disables console window so only our application window displays
#pragma comment( linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"" )
//defining variables here
float rot = 0.01;
float scale = 0.5;
float location[3] = { -1.5f, 0.0f, -15.0f }; //coordinate vector
float velocity[3] = { 1.0f, 1.0f, 1.0f }; //velocity vector
float lbounds[3] = {-5.0f, -5.0f, -50.0f}; //boundary box lower limits
float ubounds[3] = { 5.0f, 5.0f, -10.0f }; //boundary box upper limits
__int64 elapsedTimeInMS = 0; //time since last draw
__int64 lastTimeEpoch = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count(); //current time
//routine to animate our cube with its velocity and elapsed time since last call
void animate(void)
{
//get elapsed milliseconds
__int64 timeNow = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
elapsedTimeInMS = timeNow - lastTimeEpoch;
lastTimeEpoch = timeNow;
//update position and detect wall bouncing
for (int i = 0; i < 3; i++)
{
location[i] = location[i] + velocity[i] * (1.0/ elapsedTimeInMS);
if (location[i] > ubounds[i] || location[i] < lbounds[i])
{
velocity[i] *= -1; //when wall bounce detected, just simply go in opposite direction
}
}
//update rotation angle
rot += 1;
rot = rot >= 360 ? 0 : rot;
//refresh screen
glutPostRedisplay();
}
//draw the boundary box where cube bounces as a wireframe
void drawBounds(void)
{
glColor3f(1.0f, 1.0f, 1.0f);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glBegin(GL_QUADS);
{
glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
glVertex3f(lbounds[0], ubounds[1], ubounds[2]);
glVertex3f(lbounds[0], ubounds[1], lbounds[2]);
glVertex3f(lbounds[0], lbounds[1], lbounds[2]);
glVertex3f(lbounds[0], lbounds[1], ubounds[2]);
glVertex3f(ubounds[0], ubounds[1], lbounds[2]);
glVertex3f(ubounds[0], ubounds[1], ubounds[2]);
glVertex3f(ubounds[0], lbounds[1], ubounds[2]);
glVertex3f(ubounds[0], lbounds[1], lbounds[2]);
glEnd();
}
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
//draws six faces of a cube with varying colors
void drawCube(void)
{
glBegin(GL_QUADS);
{
glColor3f(0.0f, 1.0f, 0.0f);
glVertex3f(1.0f, 1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glColor3f(1.0f, 0.5f, 0.0f);
glVertex3f(1.0f, -1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(1.0f, -1.0f, -1.0f);
glColor3f(1.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f(1.0f, -1.0f, 1.0f);
glColor3f(1.0f, 1.0f, 0.0f);
glVertex3f(1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f(1.0f, 1.0f, -1.0f);
glColor3f(0.0f, 0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glColor3f(1.0f, 0.0f, 1.0f);
glVertex3f(1.0f, 1.0f, -1.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glVertex3f(1.0f, -1.0f, 1.0f);
glVertex3f(1.0f, -1.0f, -1.0f);
glEnd();
}
}
//draw our cube at its location and rotation angle
void render(void)
{
//clear, reset, recenter, update rotation
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
//snap our cube to its position, rotate it and scale it
glTranslatef(location[0], location[1], location[2]);
glRotatef(rot, 1, 1, 0);
glScalef(scale, scale, scale);
//draw our cube
drawCube();
//undo our snap, rotate and scale
glScalef(1 / scale, 1 / scale, 1 / scale);
glRotatef(-rot, 1, 1, 0);
glTranslatef(-location[0], -location[1], -location[2]);
//draw the boundary wall
drawBounds();
//standard call to dump our drawing into the next frame
glutSwapBuffers();
}
//reshapes the drawing when window is resized
void reshape(int w, int h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(45.0f, (float)w / (float)h, 1, 1000);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
//main entry point
int main(int argc, char** argv) {
//standard initialization routine
glutInit(&argc, argv);
glutInitWindowPosition(100, 100);
glutInitWindowSize(320, 230);
glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
glutCreateWindow("Simple Shape Drawing");
//color to draw background
glClearColor(0.0, 0.0, 0.0, 0.0);
//more standard things
glEnable(GL_DEPTH_TEST);
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
//call our display function which actually does some drawing
glutDisplayFunc(render);
glutIdleFunc(animate);
glutReshapeFunc(reshape);
//enters our display functions into a repeating loop
glutMainLoop();
return 0;
}
Road Map:
Part 1 - Introduction
Part 2 - Setting up OpenGL in Visual Studio
Part 3 - Implementing The Boundary Bouncing Cube