(Return to list of OpenGL 3.3 tutorials)
Download (135 KB)
6954 downloads. 13 comments
Welcome to another OpenGL Tutorial, 4th in the series. This will be most complex tutorial so far, since we are going to deal with much new concepts, but after having it all done, the result will be 3D scene, with moving and rotating objects. The following topics are covered in this tutorial: Types of coordinates, process of transforming input 3D data to final 2D image using matrices, uniform variables in shader programs and vertical synchronization enabling/disabling. So let's get going, there's much ahead of us.
Our ultimate goal of this tutorial is to render 5 pyramids, which will be flying around, each in its own way. It's gonna be the same pyramid all over again, but transformed
differently. And here comes the first important term (you may have already heard)  Model Coordinates (sometimes referred as object coordinates). As you can see, we've got
our pyramid stored in one VBO, with static  model coordinates. The pyramid is centered around origin, and we are looking at it from front. These coordinates never change,
so VBO with static draw is our best bet.
The goal is to render 5 pyramids, but each in different places. So how can we achieve, that after calling
glDrawArrays 5 times, pyramids will be on different places, when they have static data?
<AUTHOR'S NOTE>
Many of you probably know about matrices and stuff and know how it all works behind, but these tutorials try to teach even those basics, that seem to be trivial for many of you.
If a person with no prior OpenGL knowledge comes across, it can really help him. I started programming OpenGL when I was 13, and all this was new to me, so now I try to write these
tutorials as if 13 year old me would start reading them, he would be able to use OpenGL with minimal math knowledge (even though not fully understand things behind). If you are
not a total newbie and have already experience with OpenGL, you may skip this part.
</AUTHOR'S NOTE>.
We can see, that we are sending the same data (pyramid model coordinates) into our vertex shader. So in order to render in different places, we must alter these incoming positions in vertex shader (that's what new OpenGL is all about). That's why we must tell our vertex shader before each glDrawArrays call, where and how to render our pyramid (how to TRANSFORM incoming data). And a thing, that can store it all, in nice, mathfriendly way are matrices. Matrix, besides one of my alltime fav action movies, is a powerful tool for storing transformation data. I don't want to talk about matrices very much, since it is a whole complex subject, but if you want to learn more about them, take a look at Wikipedia). For now (and for getting things done), we should know, that there are two main matrices, when working with graphics  the modelview matrix and the projection matrix. Now let's imagine we want to render a pyramid TRANSLATED (moved) by 10 to right (move by +10 on X axis). We've got two options (well, we can come up with far more exotic options, but that's not the point ): Either creating VBO with all X coordinates increased by 10 (PLEASE DON'T!), or making a matrix, that will do exactly this after multiplying it with vertex coordinates. And here comes another term: World Coordinates. If we multiply our incoming vertex data with model (not modelview) matrix, we get the positions of vertices in our world. So if I have a model matrix, that translates (moves) pyramid by 10 on X Axis, and I would multiply that matrix with pyramid coordinates, the resulting coordinates will be almost the same, except we would add +10 to each X coordinate.
But that's still not enough. Now we just have everything in the correct place in the world. The last part is us (our camera) moving. Well, now you will learn the bitter truth...
It's not us what's moving. Actually going around in 3D world means, that we are always standing in one place, and the whole
universe moves and rotates around us. That's how it all works and where the last important term comes: EyeSpace Coordinates. OpenGL (and also DirectX) works in a way,
where the spectator (camera) stands at the origin of coordinates system, and to achieve effect of us moving and turning head is to transform whole world
(universe ) in such way, that it would be the same as if we moved. And so if we multiply view matrix with world coordinates,
we get the EyeSpace coordinates, and we can proceed with creation of final image.
You may know that in OpenGL you have a modelview matrix. It's nothing else than view and model matrix combined, i.e. multiplied. First, we apply the view matrix, by
"looking" at the scene (like calling gluLookAt in old OpenGL), and then we multiply the view matrix by model matrix to get
modelview matrix. The latter part of transformation process is transforming those 3D data to 2D final image, but it's beyond the scope of this tutorial, so if you are
interested in learning it deeper, you may have look for example here:
http://www.songho.ca/opengl/gl_transform.html
GLM Library is a headeronly C++ library, that is designed to cooperate with GLSL easily. It's got all GLSL native types, and it's usage is really comfortable. It's got implemented all functions that you were used to in GLU (for example gluLookAt), but in new OpenGL friendly way (it will give you proper matrix for example). You can find it here:
http://glm.gtruc.net/Next step we must do is to integrate the GLM library into Visual Studio 2008, just the way we integrated GLEW in the first tutorial. So after downloading and unpacking the latest version of library somewhere to your computer, provide Visual Studio an include path of that folder. (Note: GLM doesn't have dll and static library, so to use GLM all you have to do is to include headers and you can start using it). It should look like that:
Now we are ready to start using GLM and continue our work with shaders (of course, don't forget to include appropriate GLM headers, you can find them in top of .cpp files). As I said before, now we are going to alter input vertex positions (object coordinates) with matrices in vertex shader. That means, that throughout the execution of vertex shader, it must know modelview and projection matrix. So let's have a look at our new vertex shader code:
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec3 inColor;
smooth out vec3 theColor;
void main()
{
gl_Position = projectionMatrix*modelViewMatrix*vec4(inPosition, 1.0);
theColor = inColor;
}
As we can see, we have added two new variables of type mat4  projectionMatrix and modelviewMatrix. It means it's a 4x4 matrix. These variables have a uniform modifier. This means, that these values are not changed during execution of shader, but they are set before the shader is called (uniform, uniqueform, oneform...). With these variable types we can communicate between shader and C++ program. Now, before we output vertex to latter stages of OpenGL pipeline, we multiply it with matrices. And you may probably already feel, that we set these matrices from our C++ program. Yes, that's right  in our C++ program, we calculate the appropriate matrices with our previously set GLM library, before rendering we upload their data to shader, and then we actually call some render function. The question is how to send these data to shader. OpenGL provides series of functions for doing just that. They all begin with glUniform, or in case of matrices its glUniformMatrix, and then suffixes size of input data and data type (in our case glUniformMatrix4fv, it means that it's a square matrix 4x4, data type is float, and v means that we will provide a pointer to data). After that, the important change in vertex shader is gl_Position setting. Now we multiply incoming position with matrices (note the order, it's important, matrices multiplication isn't commutative, so A*B isn't same as B*A). The important detail here is: Matrices are 4x4, and vertex is treated like a 3x1 matrix (3 rows, 1 column). When matrix multiplication A*B takes place, number of columns in A must equal number of rows in B. That's why we transform our vec3 to vec4, by adding fourth (w) component to obtain vertex in homogeneous coordinates (another important term, if you don't know what it is take a look at Wikipedia at least, or at this article for brief explanation: http://www.songho.ca/math/homogeneous/homogeneous.html)
The render code will show you how it works together with glm:{
// Typecast lpParam to COpenGLControl pointer
COpenGLControl* oglControl = (COpenGLControl*)lpParam;
glClear(GL_COLOR_BUFFER_BIT  GL_DEPTH_BUFFER_BIT);
glBindVertexArray(uiVAO[0]);
int iModelViewLoc = glGetUniformLocation(spMain.getProgramID(), "modelViewMatrix");
int iProjectionLoc = glGetUniformLocation(spMain.getProgramID(), "projectionMatrix");
glUniformMatrix4fv(iProjectionLoc, 1, GL_FALSE, glm::value_ptr(*oglControl>getProjectionMatrix()));
glm::mat4 mModelView = glm::lookAt(glm::vec3(0, 15, 40), glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
// Render rotating pyramid in the middle
glm::mat4 mCurrent = glm::rotate(mModelView, fRotationAngle, glm::vec3(0.0f, 1.0f, 0.0f));
glUniformMatrix4fv(iModelViewLoc, 1, GL_FALSE, glm::value_ptr(mCurrent));
glDrawArrays(GL_TRIANGLES, 0, 12);
// Render translating pyramids
// One on the left
mCurrent = glm::translate(mModelView, glm::vec3(20.0f, 10.0f*float(sin(fRotationAngle*PIover180)), 0.0f));
glUniformMatrix4fv(iModelViewLoc, 1, GL_FALSE, glm::value_ptr(mCurrent));
glDrawArrays(GL_TRIANGLES, 0, 12);
// One on the right
mCurrent = glm::translate(mModelView, glm::vec3(20.0f, 10.0f*float(sin(fRotationAngle*PIover180)), 0.0f));
glUniformMatrix4fv(iModelViewLoc, 1, GL_FALSE, glm::value_ptr(mCurrent));
glDrawArrays(GL_TRIANGLES, 0, 12);
// And one translating and rotating on top
mCurrent = glm::translate(mModelView, glm::vec3(20.0f*float(sin(fRotationAngle*PIover180)), 10.0f, 0.0f));
mCurrent = glm::rotate(mCurrent, fRotationAngle, glm::vec3(1.0f, 0.0f, 0.0f));
glUniformMatrix4fv(iModelViewLoc, 1, GL_FALSE, glm::value_ptr(mCurrent));
glDrawArrays(GL_TRIANGLES, 0, 12);
// And lastly  render scaling pyramid that rotates
float fScaleValue = 1.5f+float(sin(fRotationAngle*PIover180))*0.5f;
mCurrent = glm::translate(mModelView, glm::vec3(0.0f, 10.0f, 0.0f));
mCurrent = glm::scale(mCurrent, glm::vec3(fScaleValue, fScaleValue, fScaleValue));
mCurrent = glm::rotate(mCurrent, fRotationAngle, glm::vec3(1.0f, 0.0f, 0.0f));
mCurrent = glm::rotate(mCurrent, fRotationAngle, glm::vec3(0.0f, 1.0f, 0.0f));
mCurrent = glm::rotate(mCurrent, fRotationAngle, glm::vec3(0.0f, 0.0f, 1.0f));
glUniformMatrix4fv(iModelViewLoc, 1, GL_FALSE, glm::value_ptr(mCurrent));
glDrawArrays(GL_TRIANGLES, 0, 12);
fRotationAngle += appMain.sof(120.0f);
// Add some handlers  with F2, you can toggle FPS showing, with F3 you can toggle VSync
if(Keys::onekey(VK_F2))
{
bShowFPS = !bShowFPS;
if(!bShowFPS)SetWindowText(appMain.hWnd, "04.) Going 3D With Transformations  Tutorial by Michal Bubnar (www.mbsoftworks.sk)");
}
if(Keys::onekey(VK_F3))
{
bVerticalSync = !bVerticalSync;
oglControl>setVerticalSynchronization(bVerticalSync);
}
if(bShowFPS)
{
char buf[55]; sprintf(buf, "FPS: %d, VSync: %s", oglControl>getFPS(), bVerticalSync ? "On": "Off");
SetWindowText(appMain.hWnd, buf);
}
oglControl>swapBuffers();
}
Let's analyze the code above a little. There is another important function there. It's glGetUniformLocation. It has two parameters  first is program name (handle, ID), that we got from calling glCreateProgram, and the second is name of variable in vertex shader. This function returns location of uniform variable inside the program. Simply said  we will use this number to set (or get) data of uniforms.
After we have locations of modelview and projection matrix, we first upload data of projection matrix with function glUniformMatrix4fv. Parameters are in order: variable location (retrieved by glGetUniformLocation), number of matrices to send (in this case it's just this one matrix), third is a boolean whether to transpose matrix upon upload (glm is compatible with GLSL and internally it has all data in the same order as GLSL, so we don't need to transpose matrices upon sending), and the last is the pointer to matrix data. This is done with glm::value_ptr function. I decided to store projection matrix inside OpenGL control, because it's something that will be used always, so you can find it in class definition. The question when is the projection matrix calculated (we just upload the data). Well, I added a new function setProjection3D into OpenGLControl, that does nothing but calls glm::perspective function, which is equivalent to old gluPerspective. Parameters are field of view angle, aspect ratio (width of viewport divided by height of viewport), and distances of near and far clipping planes. And in win_openglApp.cpp, we call the function setProjection3D everytime we receive a WM_SIZE message (that notifies us that window's size has changed). And the last thing to start with 3D rendering is to enable depth testing, that hides objects that are behind other objects. We do this in initScene function. And now that's all that's necessary for rendering 3D scene.
For each rendered object (pyramid in this case) we will construct the modelview matrix. We start by "looking" at the scene. More precise, we create a view matrix by calling glm function lookAt. It's the equivalent of gluLookAt from old GLU library. Its parameters are Eye position, ViewPoint position (point that eye looks upon), and the Up vector. In normal case, it's (0, 1, 0), but for those who don't know what it is, I will try to explain it simply. If you stand normally, you see the world like you're used to. The up vector is (0, 1, 0). But now, rotate your head to right, so that it lies on your right shoulder. Now, the up vector points to right (1, 0, 0), and the whole world is like rotated. That's what up vector is. Hope you got it from this simple, yet I hope effective explanation .
The view matrix won't change through the rest of rendering. Then for each rendered pyramid, we will multiply the view matrix by a model matrix, to obtain modelview matrix.
First pyramid we render is the one in center, that rotates. So our modelview matrix will be the mView*mRotationMatrix. This is all done in calling
glm::rotate function. First parameter is the matrix that will be multiplied. Second is angle we want to rotate by. And the last is the vector of
axis of rotation (it's like calling glRotatef(fRotationAngle, 0.0f, 1.0f, 0.0f)).
When the matrix is setup, we can send its data to our vertex
shader using glUniformMatrix4fv.
Now with both matrices set, we can finally render with glDrawArrays. We then render another 4 pyramids, with new modelview matrix for each. Second
transformation function is glm::translate. Again, first parameter is matrix to multiply, and the second is translation vector (where to move). Third
transformation function is glm::scale. Parameters are same, last parameter is a scale vector (how much to scale data).
The variable fRotationAngle is our main controlling variable. With sines and cosines we calculate scale values and translation values. After
all rendering is done, we update by calling appMain.sof(120.0f). To make this clear, sof stands for Speed Optimized Float (I made that acronym
very long time ago, in year 2005 I think ) and the parameter means by how much per second we want to increase a value. So know,
each second, our angle is increased by 120.0f. Of course, there is some loss in precision, since we don't have infinitely precise numbers in computers, but for sake of these
simple applications it's absolutely sufficient. This function simply returns the input value scaled by how much time has elapsed since the last frame. Let's say if it took
50 ms to render next frame, it will return 120.0f*50.0f/1000.0f (one second has 1000 ms), so it will return 6.0f.
Now no matter how much FPS we have, the scene always looks the same, as time passes by.
With F2 and F3 keys, you can toggle FPS (Frames Per Second) showing and vertical synchronization with newly added function to OpenGL control setVerticalSynchronization. Parameter is true or false  on or off. Vertical sync caps our FPS by limiting it to monitor refresh rate (no matter how much images we generate in second, if monitor isn't able to display them, then it's useless). It's not a bad thing, but for measuring performance of our application we may want to turn it off. I also added FPS counter to OpenGL control, one can retrieve it with getFPS function.
Phew. This tutorial covered many topics. I'm glad it's over, writing this one was neverending . Now you should be able to render things in 3D. You may have noticed one thing. For such pyramid, we actually need only 5 vertices (5 distinct vertices). But in our data, the same values just keep repeating. Isn't there a better way to do this? Of course there is  it's indexed rendering, and we will cover it in another tutorial. We will also add a fullscreen option to our application. And I gave myself a deadline for this tutorial the end of November 2011. I finished it almost week before deadline . I don't know what happened, I usually finish things far beyond the deadline, like the third tutorial came 2 weeks after it . I hope I will keep up that tempo, since my Mana Pool for writing tutorials seems to have increased .
Download (135 KB)
6954 downloads. 13 comments
therandom on 19.02.2014 19:27:51  
How can I rotate a triangle around its own axis? For example, I have a triangle at point 1.0, 1.0, 1.0, how do I rotate it around its own center, not around world axis?

hi on 28.06.2013 13:23:26 
"These variables have a uniform modifier. This means, that these values are not changed during execution of shader, but they are set before the shader is called". Wrong. Please fix that. 
Gooeybots (cparkerd@gmail.com) on 10.02.2013 20:38:07 
Just looking about at diff tutorials and noticed that your coords on the pyramid are wrong. The z plane seems to move as the one at the back seems to be 3 one side and 3 on the other same as the front 
BumbaDawg on 02.10.2012 16:56:17 
Great tutorial thanks! Here's a video of the result http://youtu.be/pc9RLLOFyc4 It'd be nice to see more of the code changes you make between each tutorials. Again, thanks for making those. Would it be possible to make a specific one on shader use only ? :) 
tlog on 05.08.2012 22:07:10 
Is it normal that my FPS is 1 to 2? My computer is 3 years old, but still thats quite sad... :D 
learner on 05.08.2012 16:07:16 
download on two computers, unable to open files 
randomguy on 02.08.2012 18:42:40 
Thank you for these great tutorials, they help me a lot in learning the new openGL ways. 
sigEleven (aidevelopment@gmail.com) on 02.08.2012 01:49:10  
Again, great tutorial, but it would be nice if you expanded on the changes you make from tutorial to tutorial. In this case, you've added to the class, and included some extra files (ex: <ctime>), with *NO* mention of this. It makes it really frustrating when trying to code along from tutorial to tutorial.

d on 31.07.2012 12:35:05 
i tryed to open the .zip archive, but it was broken. please somebody can send me the unzipped file (you can send it to dario.pk2@gmail.com)? 
d on 31.07.2012 12:35:04 
i tryed to open the .zip archive, but it was broken. please somebody can send me the unzipped file (you can send it to dario.pk2@gmail.com)? 
Weerachai on 01.03.2012 14:31:04 
These are great tutorials. Thank you very much. 
1