Megabyte Softworks
C++, OpenGL, Algorithms

Current series: OpenGL 3.3
(Return to list of OpenGL 3.3 tutorials)

4.) Going 3D With Transformations

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.

OpenGL Transformation Process:

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, math-friendly way are matrices. Matrix, besides one of my all-time 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: Eye-Space 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 Eye-Space 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

GLM Library is a header-only 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.g-truc.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:

Uniform variables

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:

#version 330

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;
}

The render code will show you how it works together with glm:

void renderScene(LPVOID lpParam)
{
// 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 V-Sync

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, V-Sync: %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.

Going 3D

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.

Rendering objects anywhere

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.

Vertical synchronization and FPS

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.

Conclusion

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 .

Name:

E-mail:
(Optional)
Entry:

Enter the text from image:

Smileys

 MolinaroKn (algis234234@gmail.com) on 05.01.2017 20:44:29 http://0daymusic.org/premium.php http://mp3dj.eu/0day/ http://mp3dj.eu/collections/ http://mp3dj.eu/discography/ http://mp3dj.eu/flac-lossless/ http://mp3dj.eu/labels/ http://mp3dj.eu/music-videos/ Maintain the awesome work !! Lovin' it!
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?
 Ryan on 15.08.2014 16:23:10 If you translate it so that its centre is on the origin then perform the rotation and translate it back then it will rotate around its own centre. It is possible to do it around an arbitrary, or its own, axis, but a pre a post translation are how it is normally done.
 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.
 Bob on 12.09.2012 16:22:35 And not mention of glEnable and glViewport in the tutorial... So without code and headache, it doesnt work for newbie like me.
 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.